Skip to content

Commit 5e2fc5b

Browse files
authored
Merge pull request #4 from atteggiani/3-add_prefix_input
Added prefix input and tests
2 parents 4caef2b + 32163e6 commit 5e2fc5b

File tree

8 files changed

+229
-27
lines changed

8 files changed

+229
-27
lines changed

.github/workflows/build.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ jobs:
1818
python-version: "3.11"
1919
- name: 🏗 Install build dependencies
2020
run: |
21-
python -m pip install wheel --user
21+
python -m pip install build --user
2222
- name: 🔨 Build a binary wheel and a source tarball
2323
run: |
24-
python setup.py sdist bdist_wheel
24+
python -m build
2525
- name: ⬆ Upload build result
2626
uses: actions/upload-artifact@v4
2727
with:
@@ -39,17 +39,34 @@ jobs:
3939
python-version: "3.11"
4040
- name: 🏗 Set up dev dependencies
4141
run: |
42-
pip install pre-commit
42+
pip install -r requirements-dev.txt
4343
- name: 🚀 Run pre-commit
4444
run: |
4545
pre-commit run --all-files --show-diff-on-failure
4646
47+
test:
48+
name: 🧪 Run tests
49+
runs-on: ubuntu-latest
50+
steps:
51+
- uses: actions/checkout@v3
52+
- name: 🏗 Set up Python 3.11
53+
uses: actions/setup-python@v4
54+
with:
55+
python-version: "3.11"
56+
- name: 🏗 Set up dev dependencies
57+
run: |
58+
pip install -r requirements-dev.txt
59+
- name: 🚀 Run pytest
60+
run: |
61+
pytest
62+
4763
publish-on-pypi:
4864
name: 📦 Publish tagged releases to PyPI
4965
if: github.event_name == 'release' && github.repository == 'OctoPrint/mkdocs-site-urls'
5066
needs:
5167
- build
5268
- pre-commit
69+
- test
5370
runs-on: ubuntu-latest
5471
environment: release
5572
permissions:

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
default_language_version:
2-
python: python3.11
2+
python: python3
33
repos:
44
- repo: https://github.com/pre-commit/pre-commit-hooks
55
rev: v4.4.0

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,34 @@ plugins:
5555
Be advised that in case of any customization on your part you need to include the default attributes as well if you want
5656
to keep them, as the default list will not be included automatically anymore.
5757

58+
If `site:` as the prefix does not work for you for any reason, you can also configure that,
59+
e.g.
60+
61+
```yaml
62+
plugins:
63+
- site-urls:
64+
prefix: "relative:"
65+
```
66+
67+
This can also be used to interpret absolute URLs like `/example/file.png` as relative,
68+
by setting the `prefix` to `/`.
69+
5870
## How it works
5971

6072
The plugin hooks into the [`on_page_content` event](https://www.mkdocs.org/dev-guide/plugins/#on_page_content)
6173
and replaces all URLs in the configured attributes (by default `href`, `src` or `data`) in the rendered HTML with the corresponding site-relative URLs.
6274

75+
## Development
76+
77+
1. Create a venv & activate it, e.g. `python -m venv venv && source venv/bin/activate`
78+
2. Install the dev requirements: `pip install -r requirements-dev.txt`
79+
3. Install the `pre-commit` hooks: `pre-commit install`
80+
81+
You can run the tests with `pytest`.
82+
83+
To build the docs, install their dependencies as well (`pip install -r requirements-docs.txt`),
84+
then run `mkdocs build`.
85+
6386
## License
6487

6588
This project is licensed under the MIT license, see the [LICENSE](https://github.com/OctoPrint/mkdocs-site-urls/blob/main/LICENSE) file for details.

mkdocs_site_urls/__init__.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,41 @@
88
logger = mkdocs.plugins.get_plugin_logger(__name__)
99

1010

11-
class SiteUrlsConfig(mkdocs.config.base.Config):
11+
class Config(mkdocs.config.base.Config):
1212
attributes = c.Type(list, default=["href", "src", "data"])
13-
14-
15-
class SiteUrlsPlugin(mkdocs.plugins.BasePlugin[SiteUrlsConfig]):
16-
def on_pre_build(self, *, config: MkDocsConfig) -> None:
17-
self._regex = re.compile(
18-
r"(" + "|".join(self.config["attributes"]) + r')="site:([^"]+)"',
19-
re.IGNORECASE,
20-
)
13+
prefix = c.Type(str, default="site:")
14+
15+
16+
class SiteUrlsPlugin(mkdocs.plugins.BasePlugin[Config]):
17+
def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
18+
attributes = (re.escape(attr) for attr in self.config["attributes"])
19+
self.prefix = re.escape(self.config["prefix"])
20+
regex_parts = [
21+
r"(", # capturing group 1
22+
"|".join(attributes), # attributes
23+
r")", # end of capturing group 1
24+
r"\s*=\s*", # equals sign with optional whitespace
25+
r"([\"'])", # quote with capturing group 2
26+
self.prefix, # url prefix
27+
r"([^\"']*)", # remainder of the url with capturing group 3
28+
r"\2", # matching quote
29+
]
30+
regex = "".join(regex_parts)
31+
self._regex = re.compile(regex, re.IGNORECASE)
32+
return config
2133

2234
@mkdocs.plugins.event_priority(50)
2335
def on_page_content(self, html, page, config, files):
2436
site_url = config["site_url"]
37+
if not site_url.endswith("/"):
38+
site_url += "/"
2539
path = urllib.parse.urlparse(site_url).path
2640

27-
if not path:
28-
path = "/"
29-
if not path.endswith("/"):
30-
path += "/"
31-
3241
def _replace(match):
33-
param = match.group(1)
34-
url = match.group(2)
35-
if url.startswith("/"):
36-
url = url[1:]
42+
attribute = match.group(1)
43+
url = match.group(3)
3744

38-
logger.info(f"Replacing site:{match.group(2)} with {path}{url}")
39-
return f'{param}="{path}{url}"'
45+
logger.info(f"Replacing '{self.prefix}{url}' with '{path}{url}'")
46+
return f'{attribute}="{path}{url}"'
4047

4148
return self._regex.sub(_replace, html)

requirements-dev.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mkdocs>=1.5.0
2+
pre-commit
3+
pytest
4+
-e .

setup.cfg

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
[metadata]
22
license_file = LICENSE
33

4-
[bdist_wheel]
5-
universal = 1
6-
74
[flake8]
85
max-line-length = 90
96
extend-ignore = E203, E231, E265, E266, E402, E501, E731, B023, B903, B904, B907, B950, W503

tests/__init__.py

Whitespace-only changes.

tests/test_mkdocs_site_urls.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import re
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
from mkdocs_site_urls import SiteUrlsPlugin
7+
8+
9+
@pytest.fixture
10+
def mock_plugin_config():
11+
return {"attributes": ["src", "href"], "prefix": "site:"}
12+
13+
14+
@pytest.fixture
15+
def create_plugin(mock_plugin_config):
16+
"""Factory function to create the plugin with the prescribed configuration options."""
17+
18+
def _plugin(config=mock_plugin_config, **kwargs):
19+
plugin = SiteUrlsPlugin()
20+
plugin.load_config(config)
21+
for key, value in kwargs.items():
22+
setattr(plugin, key, value)
23+
return plugin
24+
25+
return _plugin
26+
27+
28+
@pytest.mark.parametrize(
29+
"attributes, prefix, string_to_match, match_group1, match_group3, should_match",
30+
[
31+
(
32+
["href", "src"],
33+
"/docs/",
34+
'src = "/docs/image.png"',
35+
"src",
36+
"image.png",
37+
True,
38+
), # valid1
39+
(
40+
["href", "src"],
41+
"/",
42+
"href ='/docs/image.png'",
43+
"href",
44+
"docs/image.png",
45+
True,
46+
), # valid2
47+
(
48+
["data"],
49+
"site:",
50+
"data= 'site:image.png'",
51+
"data",
52+
"image.png",
53+
True,
54+
), # valid3
55+
(
56+
["href", "src"],
57+
"/docs/",
58+
'src = "/image.png"',
59+
None,
60+
None,
61+
False,
62+
), # invalid_different_prefix
63+
(
64+
["href", "src"],
65+
"/",
66+
"href ='site:docs/image.png'",
67+
None,
68+
None,
69+
False,
70+
), # invalid_different_prefix2
71+
(
72+
["data"],
73+
"/",
74+
"href='/image.png'",
75+
None,
76+
None,
77+
False,
78+
), # invalid_different_attribute
79+
],
80+
ids=[
81+
"valid1",
82+
"valid2",
83+
"valid3",
84+
"invalid_different_prefix",
85+
"invalid_different_prefix2",
86+
"invalid_different_attribute",
87+
],
88+
)
89+
def test_on_config_sets_regex(
90+
create_plugin,
91+
attributes,
92+
prefix,
93+
string_to_match,
94+
match_group1,
95+
match_group3,
96+
should_match,
97+
):
98+
"""Test the on_config method of the SiteUrlsPlugin."""
99+
plugin_config = {
100+
"attributes": attributes,
101+
"prefix": prefix,
102+
}
103+
plugin = create_plugin(plugin_config)
104+
plugin.on_config(MagicMock())
105+
106+
# Check that the regex is compiled
107+
assert isinstance(plugin._regex, re.Pattern)
108+
match = plugin._regex.search(string_to_match)
109+
110+
# Check regex is correct
111+
if should_match:
112+
assert match is not None
113+
assert match.group(1) == match_group1
114+
assert match.group(3) == match_group3
115+
else:
116+
assert match is None
117+
118+
119+
@pytest.mark.parametrize(
120+
"site_url",
121+
[
122+
"https://example.com/docs/subpage/",
123+
"https://example.com/docs/subpage",
124+
],
125+
ids=["trailing_slash", "no_trailing_slash"],
126+
)
127+
def test_on_page_content(create_plugin, site_url):
128+
"""Test the on_page_content method of the SiteUrlsPlugin."""
129+
plugin = create_plugin(
130+
{
131+
"attributes": ["src", "data"],
132+
"prefix": "prefix",
133+
}
134+
)
135+
page = MagicMock()
136+
config = MagicMock()
137+
config.__getitem__.side_effect = lambda key: site_url if key == "site_url" else None
138+
files = MagicMock()
139+
html = """
140+
<img src ="prefixexample.png" alt="Image">
141+
<img data = \'prefix/image.png\'>
142+
<img data="site:docs/image.svg" class="example">
143+
<img attr ="prefix/docs/image.png" >
144+
"""
145+
146+
plugin.on_config(config)
147+
result = plugin.on_page_content(html, page, config, files)
148+
expected_result = """
149+
<img src="/docs/subpage/example.png" alt="Image">
150+
<img data="/docs/subpage//image.png">
151+
<img data="site:docs/image.svg" class="example">
152+
<img attr ="prefix/docs/image.png" >
153+
"""
154+
assert result == expected_result

0 commit comments

Comments
 (0)