Skip to content

Commit bf0dea6

Browse files
authored
Support pymdownx.tabbed for pymdown-extensions v8+ (#24)
1 parent db13572 commit bf0dea6

File tree

3 files changed

+217
-115
lines changed

3 files changed

+217
-115
lines changed

README.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,42 @@ This is effectively an extended Markdown format, but is intended to degrade grac
77

88
1. Install the plugin:
99

10-
```
11-
pip install mkdocs-codeinclude-plugin
12-
```
10+
```
11+
pip install mkdocs-codeinclude-plugin
12+
```
1313
1414
2. Add `codeinclude` to the list of your MkDocs plugins (typically listed in `mkdocs.yml`):
1515
16-
```yaml
17-
plugins:
18-
- codeinclude
19-
```
16+
```yaml
17+
plugins:
18+
- codeinclude
19+
```
20+
21+
3. The plugin should be configured use an appropriate form of tabbed fences, depending on the version of
22+
[`pymdown-extensions`](https://github.com/facelessuser/pymdown-extensions) that is installed.
23+
Tabbed fences provide a 'title' for code blocks, and adjacent code blocks will appear as a multi-tabbed code block.
24+
25+
a. For version 8.x of `pymdown-extensions`, use the following or leave blank (default):
26+
27+
```yaml
28+
plugins:
29+
- codeinclude:
30+
title_mode: pymdownx.tabbed
31+
```
32+
33+
b. For version 7.x or lower of `pymdown-extensions`, use the following:
34+
```yaml
35+
plugins:
36+
- codeinclude:
37+
title_mode: legacy_pymdownx.superfences
38+
```
39+
40+
c. If no tabbed fences should be used at all:
41+
```yaml
42+
plugins:
43+
- codeinclude:
44+
title_mode: none
45+
```
2046
2147
## Usage
2248

codeinclude/plugin.py

Lines changed: 139 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import re
1+
import enum
22
import os
3+
import re
34
import shlex
45
import textwrap
56
from dataclasses import dataclass
67
from typing import List
78

9+
import mkdocs
810
from mkdocs.plugins import BasePlugin
9-
from codeinclude.resolver import select
11+
1012
from codeinclude.languages import get_lang_class
13+
from codeinclude.resolver import select
1114

1215
RE_START = r"""(?x)
1316
^
@@ -35,136 +38,164 @@
3538
"""
3639

3740

38-
class CodeIncludePlugin(BasePlugin):
39-
def on_page_markdown(self, markdown, page, config, site_navigation=None, **kwargs):
40-
"Provide a hook for defining functions from an external module"
41-
42-
blocks = find_code_include_blocks(markdown)
43-
substitutes = get_substitutes(blocks, page)
44-
return substitute(markdown, substitutes)
45-
46-
4741
@dataclass
4842
class CodeIncludeBlock(object):
4943
first_line_index: int
5044
last_line_index: int
5145
content: str
5246

5347

54-
def find_code_include_blocks(markdown: str) -> List[CodeIncludeBlock]:
55-
ci_blocks = list()
56-
first = -1
57-
in_block = False
58-
lines = markdown.splitlines()
59-
for index, line in enumerate(lines):
60-
if re.match(RE_START, lines[index]):
61-
if in_block:
62-
raise ValueError(
63-
f"Found two consecutive code-include starts: at lines {first} and {index}"
64-
)
65-
first = index
66-
in_block = True
67-
elif re.match(RE_END, lines[index]):
68-
if not in_block:
69-
raise ValueError(
70-
f"Found code-include end without preceding start at line {index}"
71-
)
72-
last = index
73-
content = "\n".join(lines[first : last + 1])
74-
ci_blocks.append(CodeIncludeBlock(first, last, content))
75-
in_block = False
76-
return ci_blocks
77-
78-
7948
@dataclass
8049
class Replacement(object):
8150
first_line_index: int
8251
last_line_index: int
8352
content: str
8453

8554

86-
def get_substitutes(blocks: List[CodeIncludeBlock], page) -> List[Replacement]:
87-
replacements = list()
88-
for ci_block in blocks:
89-
replacement_content = ""
90-
for snippet_match in re.finditer(RE_SNIPPET, ci_block.content):
91-
title = snippet_match.group("title")
92-
filename = snippet_match.group("filename")
93-
indent = snippet_match.group("leading_space")
94-
raw_params = snippet_match.group("params")
95-
96-
if raw_params:
97-
params = dict(token.split(":") for token in shlex.split(raw_params))
98-
lines = params.get("lines", "")
99-
block = params.get("block", "")
100-
inside_block = params.get("inside_block", "")
101-
else:
102-
lines = ""
103-
block = ""
104-
inside_block = ""
55+
class CodeIncludePlugin(BasePlugin):
10556

106-
code_block = get_substitute(
107-
page, title, filename, lines, block, inside_block
108-
)
109-
# re-indent
110-
code_block = re.sub("^", indent, code_block, flags=re.MULTILINE)
57+
config_scheme = (
58+
(
59+
"title_mode",
60+
mkdocs.config.config_options.Choice(
61+
choices=["none", "legacy_pymdownx.superfences", "pymdownx.tabbed"],
62+
default="pymdownx.tabbed",
63+
),
64+
),
65+
)
11166

112-
replacement_content += code_block
113-
replacements.append(
114-
Replacement(
115-
ci_block.first_line_index, ci_block.last_line_index, replacement_content
67+
def on_page_markdown(self, markdown, page, config, site_navigation=None, **kwargs):
68+
"""Provide a hook for defining functions from an external module"""
69+
70+
blocks = self.find_code_include_blocks(markdown)
71+
substitutes = self.get_substitutes(blocks, page)
72+
return self.substitute(markdown, substitutes)
73+
74+
def find_code_include_blocks(self, markdown: str) -> List[CodeIncludeBlock]:
75+
ci_blocks = list()
76+
first = -1
77+
in_block = False
78+
lines = markdown.splitlines()
79+
for index, line in enumerate(lines):
80+
if re.match(RE_START, lines[index]):
81+
if in_block:
82+
raise ValueError(
83+
f"Found two consecutive code-include starts: at lines {first} and {index}"
84+
)
85+
first = index
86+
in_block = True
87+
elif re.match(RE_END, lines[index]):
88+
if not in_block:
89+
raise ValueError(
90+
f"Found code-include end without preceding start at line {index}"
91+
)
92+
last = index
93+
content = "\n".join(lines[first : last + 1])
94+
ci_blocks.append(CodeIncludeBlock(first, last, content))
95+
in_block = False
96+
return ci_blocks
97+
98+
def get_substitutes(
99+
self, blocks: List[CodeIncludeBlock], page
100+
) -> List[Replacement]:
101+
replacements = list()
102+
for ci_block in blocks:
103+
replacement_content = ""
104+
for snippet_match in re.finditer(RE_SNIPPET, ci_block.content):
105+
title = snippet_match.group("title")
106+
filename = snippet_match.group("filename")
107+
indent = snippet_match.group("leading_space")
108+
raw_params = snippet_match.group("params")
109+
110+
if raw_params:
111+
params = dict(token.split(":") for token in shlex.split(raw_params))
112+
lines = params.get("lines", "")
113+
block = params.get("block", "")
114+
inside_block = params.get("inside_block", "")
115+
else:
116+
lines = ""
117+
block = ""
118+
inside_block = ""
119+
120+
code_block = self.get_substitute(
121+
page, title, filename, lines, block, inside_block
122+
)
123+
# re-indent
124+
code_block = re.sub("^", indent, code_block, flags=re.MULTILINE)
125+
126+
replacement_content += code_block
127+
replacements.append(
128+
Replacement(
129+
ci_block.first_line_index,
130+
ci_block.last_line_index,
131+
replacement_content,
132+
)
116133
)
134+
return replacements
135+
136+
def get_substitute(self, page, title, filename, lines, block, inside_block):
137+
# Compute the fence header
138+
lang_code = get_lang_class(filename)
139+
header = lang_code
140+
title = title.strip()
141+
142+
# Select the code content
143+
page_parent_dir = os.path.dirname(page.file.abs_src_path)
144+
import_path = os.path.join(page_parent_dir, filename)
145+
# Always use UTF-8, as it is the recommended default for source file encodings.
146+
with open(import_path, encoding="UTF-8") as f:
147+
content = f.read()
148+
149+
selected_content = select(
150+
content, lines=lines, block=block, inside_block=inside_block
117151
)
118-
return replacements
119-
120-
121-
def get_substitute(page, title, filename, lines, block, inside_block):
122-
# Compute the fence header
123-
lang_code = get_lang_class(filename)
124-
header = lang_code
125-
title = title.strip()
126-
if len(title) > 0:
127-
header += f' tab="{title}"'
128-
129-
# Select the code content
130-
page_parent_dir = os.path.dirname(page.file.abs_src_path)
131-
import_path = os.path.join(page_parent_dir, filename)
132-
# Always use UTF-8, as it is the recommended default for source file encodings.
133-
with open(import_path, encoding="UTF-8") as f:
134-
content = f.read()
135-
136-
selected_content = select(
137-
content, lines=lines, block=block, inside_block=inside_block
138-
)
139152

140-
dedented = textwrap.dedent(selected_content)
153+
dedented = textwrap.dedent(selected_content)
141154

142-
return f"""
155+
if self.config.get("title_mode") == "pymdownx.tabbed" and len(title) > 0:
156+
return f"""
157+
=== "{title}"
143158
```{header}
144159
{dedented}
145160
```
146161
147162
"""
163+
elif (
164+
self.config.get("title_mode") == "legacy_pymdownx.superfences"
165+
and len(title) > 0
166+
):
167+
return f"""
168+
```{header} tab="{title}"
169+
{dedented}
170+
```
148171
149-
150-
def substitute(markdown: str, substitutes: List[Replacement]) -> str:
151-
substitutes_by_first_line = dict()
152-
# Index substitutes by the first line
153-
for s in substitutes:
154-
substitutes_by_first_line[s.first_line_index] = s
155-
156-
# Perform substitutions
157-
result = ""
158-
index = 0
159-
lines = markdown.splitlines()
160-
while index < len(lines):
161-
if index in substitutes_by_first_line.keys():
162-
# Replace the codeinclude fragment starting at this line
163-
substitute = substitutes_by_first_line[index]
164-
result += substitute.content
165-
index = substitute.last_line_index
172+
"""
166173
else:
167-
# Keep the input line
168-
result += lines[index] + "\n"
169-
index += 1
170-
return result
174+
return f"""
175+
```{header}
176+
{dedented}
177+
```
178+
179+
"""
180+
181+
def substitute(self, markdown: str, substitutes: List[Replacement]) -> str:
182+
substitutes_by_first_line = dict()
183+
# Index substitutes by the first line
184+
for s in substitutes:
185+
substitutes_by_first_line[s.first_line_index] = s
186+
187+
# Perform substitutions
188+
result = ""
189+
index = 0
190+
lines = markdown.splitlines()
191+
while index < len(lines):
192+
if index in substitutes_by_first_line.keys():
193+
# Replace the codeinclude fragment starting at this line
194+
substitute = substitutes_by_first_line[index]
195+
result += substitute.content
196+
index = substitute.last_line_index
197+
else:
198+
# Keep the input line
199+
result += lines[index] + "\n"
200+
index += 1
201+
return result

0 commit comments

Comments
 (0)