Skip to content

Commit 34a1d75

Browse files
authored
Fixed a Bug Where Table of Contents Wasn't Linking Correctly (#164)
* Added a test to catch the user's bug * Created a fix for the bug * Cleaned up code * Removed unused import * Updated package version * Updated changelog * Expanded testing * Updated poetry packages and changed requirements to 3.9+ * Reworked workflow * Fixed a strange test case * Updated version history * Fixed a linter issue * Cleaned up poetry file * Updated python support table
1 parent 1a2f8ee commit 34a1d75

File tree

9 files changed

+466
-593
lines changed

9 files changed

+466
-593
lines changed

.github/workflows/deploy.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: true
1212
matrix:
13-
python-version: ["3.8"]
13+
python-version: ["3.9"]
1414
poetry-version: ["1.4"]
1515
os: ["ubuntu-latest"]
1616

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
fail-fast: true
1212
matrix:
13-
python-version: ["3.8", "3.9", "3.10", "3.11"]
13+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1414
poetry-version: ["1.4"]
1515
os: [ubuntu-latest, macos-latest, windows-latest]
1616

docs/python-support.csv

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Python,3.11,3.10,3.9,3.8
2-
SnakeMD >= 2.0,Yes,Yes,Yes,Yes
3-
SnakeMD 0.12 - 0.15,Yes,Yes,Yes,Yes
4-
SnakeMD < 0.12,,Yes,Yes,Yes
1+
Python,3.13,3.12,3.11,3.10,3.9,3.8
2+
SnakeMD >= 2.2.1,Yes,Yes,Yes,Yes,Yes,
3+
SnakeMD 2.0 - 2.2.0,,,Yes,Yes,Yes,Yes
4+
SnakeMD 0.12 - 0.15,,,Yes,Yes,Yes,Yes
5+
SnakeMD < 0.12,,,,Yes,Yes,Yes

docs/version-history.rst

+12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ as follows:
1313
v2.x
1414
----
1515

16+
* v2.2.1 [:pr:`#164`]
17+
18+
* Fixed a bug where headings with special characters would not link properly in the table of contents
19+
* Dropped support for Python 3.8 due to deprecation
20+
* Added support for Python 3.12 and 3.13
21+
22+
* v2.2.0 [:pr:`#152`, :pr:`153`, :pr:`#159`]
23+
24+
* Added documentation throughout the repo
25+
* Expanded testing
26+
* Added CSVTable Template and accompanying documentation
27+
1628
* v2.2.0b1 [:pr:`#140`, :pr:`#142`, :pr:`#143`, :pr:`#144`, :pr:`#145`, :pr:`#146`, :pr:`#149`]
1729

1830
* Expanded the Element requirements to include :code:`__repr__()` for developer friendly strings

poetry.lock

+410-575
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+7-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
[tool.poetry]
33
name = "SnakeMD"
44
description = "A markdown generation library for Python."
5-
version = "2.2.0"
5+
version = "2.2.1"
66
license = "MIT"
77

88
authors = [
@@ -17,10 +17,11 @@ documentation = "https://www.snakemd.io/en/latest/docs/"
1717
classifiers=[
1818
"Development Status :: 5 - Production/Stable",
1919
"License :: OSI Approved :: MIT License",
20-
"Programming Language :: Python :: 3.8",
2120
"Programming Language :: Python :: 3.9",
2221
"Programming Language :: Python :: 3.10",
2322
"Programming Language :: Python :: 3.11",
23+
"Programming Language :: Python :: 3.12",
24+
"Programming Language :: Python :: 3.13",
2425
"Operating System :: OS Independent",
2526
"Topic :: Documentation :: Sphinx",
2627
]
@@ -29,7 +30,7 @@ classifiers=[
2930
Changelog = "https://www.snakemd.io/en/latest/version-history/"
3031

3132
[tool.poetry.dependencies]
32-
python = "^3.8"
33+
python = "^3.9"
3334

3435
[tool.poetry.group.test.dependencies]
3536
coverage = "^7.2"
@@ -45,7 +46,7 @@ sphinx_rtd_theme = "^1.2"
4546
black = "^23.3"
4647
isort = "^5.12"
4748
pydocstringformatter = "^v0.7"
48-
pylint = "^2.17"
49+
pylint = "^3.3"
4950

5051
# Pytest settings
5152
[tool.pytest.ini_options]
@@ -71,7 +72,7 @@ fail_under = 95
7172
# Black formatting settings
7273
[tool.black]
7374
line-length = 88
74-
target-version = ['py38', 'py39', 'py310', 'py311']
75+
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
7576

7677
# Pylint settings
7778
[tool.pylint.format]
@@ -87,7 +88,7 @@ disable = [
8788
# isort setttings
8889
[tool.isort]
8990
profile = "black"
90-
py_version = 311
91+
py_version = 313
9192

9293
# pydocstringformatter settings
9394
[tool.pydocstringformatter]

snakemd/templates.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import csv
99
import logging
1010
import os
11+
import re
1112

1213
from .elements import Element, Heading, Inline, MDList, Table
1314

@@ -157,6 +158,16 @@ def __str__(self) -> str:
157158
def __repr__(self) -> str:
158159
return f"TableOfContents(levels={self._levels!r})"
159160

161+
@staticmethod
162+
def _convert_heading_to_anchor(text: str):
163+
"""
164+
A helper method for generating anchor text.
165+
Technique is borrowed from the python markdown's
166+
toc extension.
167+
"""
168+
anchor = re.sub(r'[^\w\s-]', '', text).strip().lower()
169+
return re.sub(r'[-\s]+', "-", anchor)
170+
160171
def _get_headings(self) -> list[Heading]:
161172
"""
162173
Retrieves the list of headings from the current document.
@@ -172,7 +183,7 @@ def _get_headings(self) -> list[Heading]:
172183

173184
def _assemble_table_of_contents(
174185
self, headings: list[Heading], position: int
175-
) -> tuple(MDList, int):
186+
) -> tuple[MDList, int]:
176187
"""
177188
Assembles the table of contents from the headings in the document.
178189
@@ -186,10 +197,12 @@ def _assemble_table_of_contents(
186197
level = headings[i].get_level()
187198
table_of_contents = []
188199
while i < len(headings) and headings[i].get_level() >= level:
189-
if headings[i].get_level() == level:
200+
heading_text: str = headings[i].get_text()
201+
heading_level: int = headings[i].get_level()
202+
if heading_level == level:
190203
line = Inline(
191-
headings[i].get_text(),
192-
link=f"#{'-'.join(headings[i].get_text().lower().split())}",
204+
heading_text,
205+
link=f"#{self._convert_heading_to_anchor(heading_text)}",
193206
)
194207
table_of_contents.append(line)
195208
i += 1

tests/elements/test_quote.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from snakemd import Code, Heading, HorizontalRule, MDList, Quote
1+
from snakemd import Code, Heading, HorizontalRule, MDList, Quote, Raw
22

33
# Constructor tests
44

@@ -48,6 +48,5 @@ def test_quote_mdlist():
4848

4949
def test_repr_can_create_object():
5050
quote = Quote("")
51-
exec("from snakemd import Raw")
5251
obj = eval(repr(quote))
5352
assert isinstance(obj, Quote)

tests/templates/test_table_of_contents.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import markdown
2+
13
from snakemd.document import Document
24
from snakemd.templates import TableOfContents
35

@@ -15,6 +17,16 @@ def test_table_of_contents_one_section():
1517
doc.add_heading("Section", level=2)
1618
toc.load(doc.get_elements())
1719
assert str(toc) == "1. [Section](#section)"
20+
assert markdown.markdown(str(doc), extensions=['toc']) == "<h2 id=\"section\">Section</h2>"
21+
22+
23+
def test_table_of_contents_one_section_special_chars():
24+
doc = Document()
25+
toc = TableOfContents()
26+
doc.add_heading("Section's", level=2)
27+
toc.load(doc.get_elements())
28+
assert str(toc) == "1. [Section's](#sections)"
29+
assert markdown.markdown(str(doc), extensions=['toc']) == "<h2 id=\"sections\">Section's</h2>"
1830

1931

2032
def test_table_of_contents_many_sections():

0 commit comments

Comments
 (0)