mkdocs-permalinks-plugin 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mkdocs_permalinks_plugin/__init__.py +0 -0
- mkdocs_permalinks_plugin/plugin.py +253 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/LICENSE +19 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/METADATA +23 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/RECORD +10 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/WHEEL +5 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- mkdocs_permalinks_plugin-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_plugin.py +194 -0
|
File without changes
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
import logging
|
|
4
|
+
import urllib.parse
|
|
5
|
+
|
|
6
|
+
from mkdocs.plugins import BasePlugin
|
|
7
|
+
import mkdocs.utils
|
|
8
|
+
from mkdocs.config import config_options
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(f"mkdocs.plugins.{__name__}")
|
|
11
|
+
log.addFilter(mkdocs.utils.warning_filter)
|
|
12
|
+
|
|
13
|
+
# For Regex, match groups are:
|
|
14
|
+
# 0: Whole markdown link e.g. [Alt-text](url)
|
|
15
|
+
# 1: Alt text
|
|
16
|
+
# 2: Full URL e.g. url + hash anchor
|
|
17
|
+
# 3: Filename e.g. filename.md
|
|
18
|
+
# 4: File extension e.g. .md, .png, etc.
|
|
19
|
+
# 5. hash anchor e.g. #my-sub-heading-link
|
|
20
|
+
AUTOLINK_RE = r'\[([^\]]+)\]\((([^)/]+\.(md|png|jpg))(#.*)*)\)'
|
|
21
|
+
|
|
22
|
+
# For Regex, match groups are:
|
|
23
|
+
# 0: Whole roamlike link e.g. [[filename#title|alias|widthxheight]]
|
|
24
|
+
# 1: Link content e.g. filename#title|alias|widthxheight
|
|
25
|
+
ROAMLINK_RE = r"""\[\[(.*?)\]\](?!\])"""
|
|
26
|
+
|
|
27
|
+
# Regex to match both inline and fenced codeblocks
|
|
28
|
+
CODEBLOCK_RE = r'(```.*?```|`.*?`)'
|
|
29
|
+
|
|
30
|
+
class AutoLinkReplacer:
|
|
31
|
+
def __init__(self, base_docs_url, page_url):
|
|
32
|
+
self.base_docs_url = base_docs_url
|
|
33
|
+
self.page_url = page_url
|
|
34
|
+
|
|
35
|
+
def __call__(self, match):
|
|
36
|
+
# Name of the markdown file
|
|
37
|
+
filename = urllib.parse.unquote(match.group(3).strip())
|
|
38
|
+
|
|
39
|
+
# Absolute URL of the linker
|
|
40
|
+
abs_linker_url = os.path.dirname(
|
|
41
|
+
os.path.join(self.base_docs_url, self.page_url))
|
|
42
|
+
|
|
43
|
+
# Find directory URL to target link
|
|
44
|
+
rel_link_url = ''
|
|
45
|
+
# Walk through all files in docs directory to find a matching file
|
|
46
|
+
for root, dirs, files in os.walk(self.base_docs_url, followlinks=True):
|
|
47
|
+
for name in files:
|
|
48
|
+
# If we have a match, create the relative path from linker to the link
|
|
49
|
+
if name == filename:
|
|
50
|
+
# Absolute path to the file we want to link to
|
|
51
|
+
abs_link_url = os.path.dirname(os.path.join(root, name))
|
|
52
|
+
# Constructing relative path from the linker to the link
|
|
53
|
+
rel_link_url = os.path.join(
|
|
54
|
+
os.path.relpath(abs_link_url, abs_linker_url),
|
|
55
|
+
filename)
|
|
56
|
+
if rel_link_url == '':
|
|
57
|
+
log.warning(f"AutoLinksPlugin unable to find {filename} in directory {self.base_docs_url}")
|
|
58
|
+
return match.group(0)
|
|
59
|
+
|
|
60
|
+
# Construct the return link by replacing the filename with the relative path to the file
|
|
61
|
+
if (match.group(5) == None):
|
|
62
|
+
link = match.group(0).replace(match.group(2), rel_link_url)
|
|
63
|
+
else:
|
|
64
|
+
link = match.group(0).replace(match.group(2),
|
|
65
|
+
rel_link_url + match.group(5))
|
|
66
|
+
return link
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RoamLinkReplacer:
|
|
70
|
+
def __init__(self, base_docs_url, page_url):
|
|
71
|
+
self.base_docs_url = base_docs_url
|
|
72
|
+
self.page_url = page_url
|
|
73
|
+
|
|
74
|
+
def simplify(self, filename):
|
|
75
|
+
""" ignore - _ and space different, replace .md to '' so it will match .md file,
|
|
76
|
+
if you want to link to png, make sure you filename contain suffix .png, same for other files
|
|
77
|
+
but if you want to link to markdown, you don't need suffix .md """
|
|
78
|
+
return re.sub(r"[\-_ ]", "", filename.lower()).replace(".md", "")
|
|
79
|
+
|
|
80
|
+
def gfm_anchor(self, title):
|
|
81
|
+
"""Convert to gfw title / anchor
|
|
82
|
+
see: https://gist.github.com/asabaylus/3071099#gistcomment-1593627"""
|
|
83
|
+
if title:
|
|
84
|
+
title = title.strip().lower()
|
|
85
|
+
title = re.sub(r'[^\w\u4e00-\u9fff\- ]', "", title)
|
|
86
|
+
title = re.sub(r' +', "-", title)
|
|
87
|
+
return title
|
|
88
|
+
else:
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def unresolved_text(self, filename, title, alias):
|
|
92
|
+
"""Return the plain text to keep when no target file can be found."""
|
|
93
|
+
if alias:
|
|
94
|
+
return alias
|
|
95
|
+
return filename + title
|
|
96
|
+
|
|
97
|
+
def parse_link_content(self, content):
|
|
98
|
+
filename = ""
|
|
99
|
+
title = ""
|
|
100
|
+
alias = ""
|
|
101
|
+
width = ""
|
|
102
|
+
height = ""
|
|
103
|
+
|
|
104
|
+
parts = content.split("|")
|
|
105
|
+
target = parts[0].strip()
|
|
106
|
+
modifiers = parts[1:]
|
|
107
|
+
|
|
108
|
+
if modifiers:
|
|
109
|
+
size = modifiers[-1].strip()
|
|
110
|
+
size_match = re.fullmatch(r"(\d+)(?:x(\d+))?", size)
|
|
111
|
+
if size_match:
|
|
112
|
+
width = size_match.group(1) or ""
|
|
113
|
+
height = size_match.group(2) or ""
|
|
114
|
+
modifiers = modifiers[:-1]
|
|
115
|
+
if modifiers:
|
|
116
|
+
alias = "|".join(modifiers)
|
|
117
|
+
|
|
118
|
+
if target.startswith("#"):
|
|
119
|
+
title = target
|
|
120
|
+
elif "#" in target:
|
|
121
|
+
filename, heading = target.split("#", 1)
|
|
122
|
+
title = "#" + heading
|
|
123
|
+
filename = filename.strip()
|
|
124
|
+
else:
|
|
125
|
+
filename = target
|
|
126
|
+
|
|
127
|
+
return filename, title, alias, width, height
|
|
128
|
+
|
|
129
|
+
def __call__(self, match):
|
|
130
|
+
# Name of the markdown file
|
|
131
|
+
filename, title, alias, width, height = self.parse_link_content(match.group(1))
|
|
132
|
+
format_title = self.gfm_anchor(title)
|
|
133
|
+
|
|
134
|
+
# Absolute URL of the linker
|
|
135
|
+
abs_linker_url = os.path.dirname(
|
|
136
|
+
os.path.join(self.base_docs_url, self.page_url))
|
|
137
|
+
|
|
138
|
+
# Find directory URL to target link
|
|
139
|
+
rel_link_url = ''
|
|
140
|
+
# Walk through all files in docs directory to find a matching file
|
|
141
|
+
if filename:
|
|
142
|
+
if '/' in filename:
|
|
143
|
+
if 'http' in filename: # http or https
|
|
144
|
+
rel_link_url = filename
|
|
145
|
+
else:
|
|
146
|
+
rel_file = filename
|
|
147
|
+
if not '.' in filename: # don't have extension type
|
|
148
|
+
rel_file = filename + ".md"
|
|
149
|
+
|
|
150
|
+
abs_link_url = os.path.dirname(os.path.join(
|
|
151
|
+
self.base_docs_url, rel_file))
|
|
152
|
+
# Constructing relative path from the linker to the link
|
|
153
|
+
rel_link_url = os.path.join(
|
|
154
|
+
os.path.relpath(abs_link_url, abs_linker_url), os.path.basename(rel_file))
|
|
155
|
+
if title:
|
|
156
|
+
rel_link_url = rel_link_url + '#' + format_title
|
|
157
|
+
else:
|
|
158
|
+
for root, dirs, files in os.walk(self.base_docs_url, followlinks=True):
|
|
159
|
+
for name in files:
|
|
160
|
+
# If we have a match, create the relative path from linker to the link
|
|
161
|
+
if self.simplify(name) == self.simplify(filename):
|
|
162
|
+
# Absolute path to the file we want to link to
|
|
163
|
+
abs_link_url = os.path.dirname(os.path.join(
|
|
164
|
+
root, name))
|
|
165
|
+
# Constructing relative path from the linker to the link
|
|
166
|
+
rel_link_url = os.path.join(
|
|
167
|
+
os.path.relpath(abs_link_url, abs_linker_url), name)
|
|
168
|
+
if title:
|
|
169
|
+
rel_link_url = rel_link_url + '#' + format_title
|
|
170
|
+
if rel_link_url == '':
|
|
171
|
+
log.warning(f"PermaLinksPlugin unable to find {filename} in directory {self.base_docs_url}")
|
|
172
|
+
return self.unresolved_text(filename, title, alias)
|
|
173
|
+
else:
|
|
174
|
+
rel_link_url = '#' + format_title
|
|
175
|
+
|
|
176
|
+
# Construct the return link
|
|
177
|
+
# Windows escapes "\" unintentionally, and it creates incorrect links, so need to replace with "/"
|
|
178
|
+
rel_link_url = rel_link_url.replace("\\", "/")
|
|
179
|
+
|
|
180
|
+
if filename:
|
|
181
|
+
if alias:
|
|
182
|
+
link = f'[{alias}](<{rel_link_url}>)'
|
|
183
|
+
else:
|
|
184
|
+
link = f'[{filename+title}](<{rel_link_url}>)'
|
|
185
|
+
else:
|
|
186
|
+
if alias:
|
|
187
|
+
link = f'[{alias}](<{rel_link_url}>)'
|
|
188
|
+
else:
|
|
189
|
+
link = f'[{title}](<{rel_link_url}>)'
|
|
190
|
+
|
|
191
|
+
if width and not height:
|
|
192
|
+
link = f'{link}{{ width="{width}" }}'
|
|
193
|
+
elif not width and height:
|
|
194
|
+
link = f'{link}{{ height="{height}" }}'
|
|
195
|
+
elif width and height:
|
|
196
|
+
link = f'{link}{{ width="{width}"; height="{height}" }}'
|
|
197
|
+
|
|
198
|
+
return link
|
|
199
|
+
|
|
200
|
+
def redact_codeblocks(markdown):
|
|
201
|
+
""" Redact codeblocks to avoid processing links inside codeblocks """
|
|
202
|
+
codeblock_re = re.compile(CODEBLOCK_RE, re.DOTALL)
|
|
203
|
+
redacted_blocks = {}
|
|
204
|
+
|
|
205
|
+
def replacer(match):
|
|
206
|
+
key = f"\x01__CODEBLOCK_{len(redacted_blocks)}__\x02"
|
|
207
|
+
redacted_blocks[key] = match.group(0)
|
|
208
|
+
return key
|
|
209
|
+
|
|
210
|
+
redacted_markdown = codeblock_re.sub(replacer, markdown)
|
|
211
|
+
return redacted_markdown, redacted_blocks
|
|
212
|
+
|
|
213
|
+
def restore_codeblocks(markdown, redacted_blocks):
|
|
214
|
+
""" Restore redacted codeblocks """
|
|
215
|
+
for key, block in redacted_blocks.items():
|
|
216
|
+
markdown = markdown.replace(key, block)
|
|
217
|
+
|
|
218
|
+
return markdown
|
|
219
|
+
|
|
220
|
+
class PermaLinksPlugin(BasePlugin):
|
|
221
|
+
config_scheme = (
|
|
222
|
+
('ignore_codeblocks', config_options.Type(bool, default=True)),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def on_page_markdown(self,
|
|
226
|
+
markdown,
|
|
227
|
+
page,
|
|
228
|
+
config,
|
|
229
|
+
site_navigation=None,
|
|
230
|
+
**kwargs):
|
|
231
|
+
|
|
232
|
+
# Getting the root location of markdown source files
|
|
233
|
+
base_docs_url = config["docs_dir"]
|
|
234
|
+
|
|
235
|
+
# Getting the page url that we are linking from
|
|
236
|
+
page_url = page.file.src_path
|
|
237
|
+
|
|
238
|
+
# Redact codeblocks
|
|
239
|
+
redacted_blocks = {}
|
|
240
|
+
if self.config['ignore_codeblocks']:
|
|
241
|
+
markdown, redacted_blocks = redact_codeblocks(markdown)
|
|
242
|
+
|
|
243
|
+
# Look for matches and replace
|
|
244
|
+
markdown = re.sub(AUTOLINK_RE,
|
|
245
|
+
AutoLinkReplacer(base_docs_url, page_url), markdown)
|
|
246
|
+
markdown = re.sub(ROAMLINK_RE,
|
|
247
|
+
RoamLinkReplacer(base_docs_url, page_url), markdown)
|
|
248
|
+
|
|
249
|
+
# Restore codeblocks
|
|
250
|
+
if self.config['ignore_codeblocks']:
|
|
251
|
+
markdown = restore_codeblocks(markdown, redacted_blocks)
|
|
252
|
+
|
|
253
|
+
return markdown
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Matthew Marchetti
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mkdocs-permalinks-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An MkDocs plugin
|
|
5
|
+
Home-page: https://github.com/Mattlock0/mkdocs-permalinks-plugin
|
|
6
|
+
Author:
|
|
7
|
+
Author-email: visionofhonor@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: mkdocs
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: mkdocs >=1.0.4
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
22
|
+
|
|
23
|
+
An MkDocs plugin that automagically generates relative links and convert links for foam and obsidian between markdown pages
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mkdocs_permalinks_plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
mkdocs_permalinks_plugin/plugin.py,sha256=ePLOmu40Ho-DbG6onAJ-3QN3cMHB-K6crzez_aIUW-k,9939
|
|
3
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
tests/test_plugin.py,sha256=pKeZ9kQHAMv7NqT9XzpfW2FvMaiXo4tCrU-oWGV8YUc,7711
|
|
5
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/LICENSE,sha256=TgPExXd9i1aGN11nz1N_jZVzUsZBdsIfvDCGYVU1LFs,1080
|
|
6
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/METADATA,sha256=YYcZ6xDR-8Wk31a6VJlv3RvtUUbVHQwwT0k8hVy10vY,855
|
|
7
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
8
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/entry_points.txt,sha256=lKDJfVVxspi_yhtPsA0Mh80wjQ1jFXn15BfP06ZPZtY,79
|
|
9
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/top_level.txt,sha256=TeA-b4N3MvhuRK9U6tY8hQiYRDlfYpsPE834N0WwEMY,31
|
|
10
|
+
mkdocs_permalinks_plugin-0.1.0.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_plugin.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from mkdocs.structure.files import File
|
|
6
|
+
from mkdocs.structure.pages import Page
|
|
7
|
+
from mkdocs_permalinks_plugin.plugin import PermaLinksPlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def temp_directory():
|
|
12
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
13
|
+
yield temp_dir
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def config(temp_directory):
|
|
18
|
+
return {"docs_dir": temp_directory}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def site_navigation():
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def page(temp_directory):
|
|
28
|
+
os.mkdir(os.path.join(temp_directory, "test"))
|
|
29
|
+
file_path = os.path.join(temp_directory, "test", "test.md")
|
|
30
|
+
with open(file_path, "w", encoding="utf8") as f:
|
|
31
|
+
f.write("# Heading identifiers in HTML")
|
|
32
|
+
with open(os.path.join(temp_directory, "demo (t).md"), "w", encoding="utf8") as f:
|
|
33
|
+
f.write("# Demo Page")
|
|
34
|
+
with open(os.path.join(temp_directory, "image.png"), "w", encoding="utf8") as f:
|
|
35
|
+
f.write("# Image Page")
|
|
36
|
+
with open(os.path.join(temp_directory, "image (1).png"), "w", encoding="utf8") as f:
|
|
37
|
+
f.write("# Image Page")
|
|
38
|
+
with open(os.path.join(temp_directory, "41m+ZoNoWqL._AC_UF894,1000_QL80_.jpg"), "w", encoding="utf8") as f:
|
|
39
|
+
f.write("# Image Page")
|
|
40
|
+
os.mkdir(os.path.join(temp_directory, "software"))
|
|
41
|
+
with open(
|
|
42
|
+
os.path.join(temp_directory, "software", "git_flow.md"), "w", encoding="utf8"
|
|
43
|
+
) as f:
|
|
44
|
+
f.write("# Git Flow")
|
|
45
|
+
|
|
46
|
+
return Page(
|
|
47
|
+
title="Test Page",
|
|
48
|
+
file=File(file_path, temp_directory, temp_directory, False),
|
|
49
|
+
config={},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def converter(temp_directory, config, site_navigation, page):
|
|
54
|
+
def c(markdown, ignore_codeblocks=True):
|
|
55
|
+
plugin = PermaLinksPlugin()
|
|
56
|
+
plugin.load_config({'ignore_codeblocks': ignore_codeblocks})
|
|
57
|
+
return plugin.on_page_markdown(markdown, page, config, site_navigation)
|
|
58
|
+
|
|
59
|
+
return c
|
|
60
|
+
|
|
61
|
+
###############################################################################{}
|
|
62
|
+
## Text Links
|
|
63
|
+
###############################################################################{}
|
|
64
|
+
|
|
65
|
+
def test_converts_basic_link(converter):
|
|
66
|
+
assert converter("[[Git Flow]]") == "[Git Flow](<../software/git_flow.md>)"
|
|
67
|
+
|
|
68
|
+
def test_converts_link_with_slash(converter):
|
|
69
|
+
assert converter("[[software/Git Flow]]") == "[software/Git Flow](<../software/Git Flow.md>)"
|
|
70
|
+
|
|
71
|
+
def test_converts_link_with_anchor_only(converter):
|
|
72
|
+
assert converter("[[#Heading identifiers]]") == "[#Heading identifiers](<#heading-identifiers>)"
|
|
73
|
+
|
|
74
|
+
def test_converts_link_with_anchor(converter):
|
|
75
|
+
assert converter("[[Git Flow#Heading]]") == "[Git Flow#Heading](<../software/git_flow.md#heading>)"
|
|
76
|
+
|
|
77
|
+
def test_converts_link_with_parenthesis(converter):
|
|
78
|
+
assert converter("[[demo (t)]]") == "[demo (t)](<../demo (t).md>)"
|
|
79
|
+
|
|
80
|
+
def test_converts_link_with_parenthesis_and_space(converter):
|
|
81
|
+
assert converter("[demo (t)](<../demo%20(t).md>)") == "[demo (t)](<../demo%20(t).md>)"
|
|
82
|
+
|
|
83
|
+
def test_converts_link_with_spaces_in_text(converter):
|
|
84
|
+
assert converter('[[Git Flow|Title With Spaces]]') == '[Title With Spaces](<../software/git_flow.md>)'
|
|
85
|
+
|
|
86
|
+
def test_converts_link_with_punctuation_in_text(converter):
|
|
87
|
+
assert converter('[[Git Flow|Title, with. Punctuation!]]') == '[Title, with. Punctuation!](<../software/git_flow.md>)'
|
|
88
|
+
|
|
89
|
+
def test_converts_link_with_square_brackets_in_text(converter):
|
|
90
|
+
assert converter('[[Git Flow|Title [with] square [brackets]]]') == '[Title [with] square [brackets]](<../software/git_flow.md>)'
|
|
91
|
+
|
|
92
|
+
def test_converts_link_with_single_quotes_in_text(converter):
|
|
93
|
+
assert converter("[[Git Flow|Title 'with' single 'quotes']]") == "[Title 'with' single 'quotes'](<../software/git_flow.md>)"
|
|
94
|
+
|
|
95
|
+
def test_converts_link_with_double_quotes_in_text(converter):
|
|
96
|
+
assert converter('[[Git Flow|Title "with" double "quotes"]]') == '[Title "with" double "quotes"](<../software/git_flow.md>)'
|
|
97
|
+
|
|
98
|
+
def test_converts_link_with_fragment_identifier(converter):
|
|
99
|
+
assert converter('[[Git Flow#section|Title]]') == '[Title](<../software/git_flow.md#section>)'
|
|
100
|
+
|
|
101
|
+
def test_unresolved_link_becomes_plain_text(converter):
|
|
102
|
+
assert converter("[[Missing Page]]") == "Missing Page"
|
|
103
|
+
|
|
104
|
+
def test_unresolved_link_with_alias_becomes_alias_text(converter):
|
|
105
|
+
assert converter("[[Missing Page|Readable Title]]") == "Readable Title"
|
|
106
|
+
|
|
107
|
+
###############################################################################{}
|
|
108
|
+
## Code Blocks (ignore_codeblocks=True, default)
|
|
109
|
+
###############################################################################{}
|
|
110
|
+
|
|
111
|
+
def test_dont_converts_link_inside_inline_code_block(converter):
|
|
112
|
+
input_md = 'Here is a link `[[Git Flow]]` inside code'
|
|
113
|
+
assert converter(input_md) == input_md
|
|
114
|
+
|
|
115
|
+
def test_dont_converts_link_inside_fenced_code_block(converter):
|
|
116
|
+
input_md = '''Here is a link
|
|
117
|
+
```
|
|
118
|
+
[[Git Flow]]
|
|
119
|
+
```
|
|
120
|
+
inside code'''
|
|
121
|
+
|
|
122
|
+
assert converter(input_md) == input_md
|
|
123
|
+
|
|
124
|
+
def test_dont_converts_link_inside_fenced_code_block_with_inline_code(converter):
|
|
125
|
+
input_md = '''Here is a link
|
|
126
|
+
```
|
|
127
|
+
`[[Git Flow]]`
|
|
128
|
+
```
|
|
129
|
+
inside code'''
|
|
130
|
+
|
|
131
|
+
assert converter(input_md) == input_md
|
|
132
|
+
|
|
133
|
+
###############################################################################{}
|
|
134
|
+
## Code Blocks (ignore_codeblocks=False)
|
|
135
|
+
###############################################################################{}
|
|
136
|
+
|
|
137
|
+
def test_converts_link_inside_inline_code_block_when_ignore_false(converter):
|
|
138
|
+
input_md = 'Here is a link `[[Git Flow]]` inside code'
|
|
139
|
+
expected = 'Here is a link `[Git Flow](<../software/git_flow.md>)` inside code'
|
|
140
|
+
assert converter(input_md, ignore_codeblocks=False) == expected
|
|
141
|
+
|
|
142
|
+
def test_converts_link_inside_fenced_code_block_when_ignore_false(converter):
|
|
143
|
+
input_md = '''Here is a link
|
|
144
|
+
```
|
|
145
|
+
[[Git Flow]]
|
|
146
|
+
```
|
|
147
|
+
inside code'''
|
|
148
|
+
|
|
149
|
+
expected = '''Here is a link
|
|
150
|
+
```
|
|
151
|
+
[Git Flow](<../software/git_flow.md>)
|
|
152
|
+
```
|
|
153
|
+
inside code'''
|
|
154
|
+
|
|
155
|
+
assert converter(input_md, ignore_codeblocks=False) == expected
|
|
156
|
+
|
|
157
|
+
def test_converts_link_inside_fenced_code_block_with_inline_code_when_ignore_false(converter):
|
|
158
|
+
input_md = '''Here is a link
|
|
159
|
+
```
|
|
160
|
+
`[[Git Flow]]`
|
|
161
|
+
```
|
|
162
|
+
inside code'''
|
|
163
|
+
|
|
164
|
+
expected = '''Here is a link
|
|
165
|
+
```
|
|
166
|
+
`[Git Flow](<../software/git_flow.md>)`
|
|
167
|
+
```
|
|
168
|
+
inside code'''
|
|
169
|
+
|
|
170
|
+
assert converter(input_md, ignore_codeblocks=False) == expected
|
|
171
|
+
|
|
172
|
+
###############################################################################{}
|
|
173
|
+
## Images
|
|
174
|
+
###############################################################################{}
|
|
175
|
+
def test_converts_basic_image_link(converter):
|
|
176
|
+
assert converter("![[image.png]]") == ''
|
|
177
|
+
|
|
178
|
+
def test_converts_crazy_image_link(converter):
|
|
179
|
+
assert converter("![[41m+ZoNoWqL._AC_UF894,1000_QL80_.jpg|Edimax EW-7811un 802.11n WiFi Adapter]]") == ''
|
|
180
|
+
|
|
181
|
+
def test_converts_image_link_with_width(converter):
|
|
182
|
+
assert converter("![[image.png|600]]") == '{ width="600" }'
|
|
183
|
+
|
|
184
|
+
def test_converts_image_link_with_width_and_height(converter):
|
|
185
|
+
assert converter("![[image.png|600x800]]") == '{ width="600"; height="800" }'
|
|
186
|
+
|
|
187
|
+
def test_converts_image_link_with_title_and_width(converter):
|
|
188
|
+
assert converter("![[image.png|Image|600]]") == '{ width="600" }'
|
|
189
|
+
|
|
190
|
+
def test_converts_image_link_with_parenthesis_title_and_width(converter):
|
|
191
|
+
assert converter("![[image (1).png|Image|600]]") == '.png>){ width="600" }'
|
|
192
|
+
|
|
193
|
+
def test_converts_image_link_with_parenthesis_title_width_and_height(converter):
|
|
194
|
+
assert converter("![[image (1).png|Image|600x200]]") == '.png>){ width="600"; height="200" }'
|