mkdocs-zip-bundle-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_zip_bundle/__init__.py +1 -0
- mkdocs_zip_bundle/plugin.py +147 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/METADATA +119 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/RECORD +8 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/WHEEL +5 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/entry_points.txt +2 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/licenses/LICENSE +33 -0
- mkdocs_zip_bundle_plugin-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# mkdocs-zip-bundle-plugin
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MkDocs plugin to bundle specific code blocks into downloadable ZIP or raw files.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import re
|
|
8
|
+
from mkdocs.plugins import BasePlugin
|
|
9
|
+
from mkdocs.config import config_options
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger('mkdocs.plugins.zip_bundle')
|
|
13
|
+
|
|
14
|
+
class ZipBundlePlugin(BasePlugin):
|
|
15
|
+
"""
|
|
16
|
+
Plugin class for bundling code blocks.
|
|
17
|
+
"""
|
|
18
|
+
config_scheme = (
|
|
19
|
+
('include_jszip', config_options.Type(bool, default=True)),
|
|
20
|
+
('zip_label_suffix', config_options.Type(str, default="(.zip)")),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def on_config(self, config):
|
|
24
|
+
"""
|
|
25
|
+
Automatically add our JS and CSS to the project configuration.
|
|
26
|
+
"""
|
|
27
|
+
if self.config['include_jszip']:
|
|
28
|
+
js_jszip = 'javascripts/jszip.min.js'
|
|
29
|
+
if js_jszip not in config['extra_javascript']:
|
|
30
|
+
config['extra_javascript'].append(js_jszip)
|
|
31
|
+
|
|
32
|
+
js_bundle = 'javascripts/zip-bundle.js'
|
|
33
|
+
if js_bundle not in config['extra_javascript']:
|
|
34
|
+
config['extra_javascript'].append(js_bundle)
|
|
35
|
+
|
|
36
|
+
css_bundle = 'css/zip-bundle.css'
|
|
37
|
+
if css_bundle not in config['extra_css']:
|
|
38
|
+
config['extra_css'].append(css_bundle)
|
|
39
|
+
|
|
40
|
+
return config
|
|
41
|
+
|
|
42
|
+
def _sanitize_filename(self, filename):
|
|
43
|
+
"""
|
|
44
|
+
Sanitize filename to prevent path traversal while allowing subdirectories.
|
|
45
|
+
"""
|
|
46
|
+
# Replace backslashes with forward slashes for ZIP compatibility
|
|
47
|
+
filename = filename.replace('\\', '/')
|
|
48
|
+
# Remove any ".." segments to prevent traversal
|
|
49
|
+
filename = re.sub(r'\.\.(?=/|$)', '', filename)
|
|
50
|
+
# Remove any leading slashes
|
|
51
|
+
filename = re.sub(r'^/+', '', filename)
|
|
52
|
+
# Collapse multiple slashes
|
|
53
|
+
filename = re.sub(r'/+', '/', filename)
|
|
54
|
+
return filename.strip()
|
|
55
|
+
|
|
56
|
+
def _create_button(self, soup, bundle_id, elements):
|
|
57
|
+
"""
|
|
58
|
+
Create the download button for a bundle.
|
|
59
|
+
"""
|
|
60
|
+
btn = soup.new_tag("button")
|
|
61
|
+
btn['class'] = "md-button zip-bundle-btn"
|
|
62
|
+
btn['data-bundle-id'] = bundle_id
|
|
63
|
+
btn['type'] = "button"
|
|
64
|
+
btn['aria-label'] = f"Download {bundle_id.replace('-', ' ')} bundle"
|
|
65
|
+
|
|
66
|
+
# Check for custom label on ANY element in the bundle (first one wins)
|
|
67
|
+
custom_label = next((el.get('data-zip-label') for el in elements if el.get('data-zip-label')), None)
|
|
68
|
+
force_zip = any(el.get('data-zip-force') == 'true' for el in elements)
|
|
69
|
+
|
|
70
|
+
if custom_label:
|
|
71
|
+
btn.string = custom_label
|
|
72
|
+
elif len(elements) == 1 and not force_zip:
|
|
73
|
+
filename = elements[0].get('data-zip-filename', 'file')
|
|
74
|
+
# Extract only the base filename for the button label if it's a path
|
|
75
|
+
display_name = os.path.basename(filename)
|
|
76
|
+
btn.string = f"Download {display_name}"
|
|
77
|
+
else:
|
|
78
|
+
label = bundle_id.replace('-', ' ').title()
|
|
79
|
+
suffix = self.config['zip_label_suffix']
|
|
80
|
+
btn.string = f"Download {label} {suffix}".strip()
|
|
81
|
+
|
|
82
|
+
container = soup.new_tag("div")
|
|
83
|
+
container['class'] = "zip-bundle-container"
|
|
84
|
+
container.append(btn)
|
|
85
|
+
return container
|
|
86
|
+
|
|
87
|
+
def on_page_content(self, html, page, config, files):
|
|
88
|
+
"""
|
|
89
|
+
Inject download buttons into the page content.
|
|
90
|
+
"""
|
|
91
|
+
if "data-zip-bundle" not in html:
|
|
92
|
+
return html
|
|
93
|
+
|
|
94
|
+
soup = BeautifulSoup(html, 'html.parser')
|
|
95
|
+
|
|
96
|
+
# 1. Group all elements by their bundle ID
|
|
97
|
+
bundles = {}
|
|
98
|
+
for el in soup.find_all(attrs={"data-zip-bundle": True}):
|
|
99
|
+
bundle_id = el['data-zip-bundle']
|
|
100
|
+
if bundle_id not in bundles:
|
|
101
|
+
bundles[bundle_id] = []
|
|
102
|
+
|
|
103
|
+
# Sanitize filename on the element before grouping
|
|
104
|
+
if el.has_attr('data-zip-filename'):
|
|
105
|
+
el['data-zip-filename'] = self._sanitize_filename(el['data-zip-filename'])
|
|
106
|
+
|
|
107
|
+
bundles[bundle_id].append(el)
|
|
108
|
+
|
|
109
|
+
if not bundles:
|
|
110
|
+
return html
|
|
111
|
+
|
|
112
|
+
# 2. For each bundle, inject a download button after the last element
|
|
113
|
+
for bundle_id, elements in bundles.items():
|
|
114
|
+
container = self._create_button(soup, bundle_id, elements)
|
|
115
|
+
elements[-1].insert_after(container)
|
|
116
|
+
|
|
117
|
+
return str(soup)
|
|
118
|
+
|
|
119
|
+
def on_post_build(self, config):
|
|
120
|
+
"""
|
|
121
|
+
Copy assets to the site directory after build.
|
|
122
|
+
"""
|
|
123
|
+
assets_dir = os.path.join(os.path.dirname(__file__), 'assets')
|
|
124
|
+
|
|
125
|
+
to_copy = ['zip-bundle.js', 'zip-bundle.css']
|
|
126
|
+
if self.config['include_jszip']:
|
|
127
|
+
to_copy.append('jszip.min.js')
|
|
128
|
+
|
|
129
|
+
for filename in to_copy:
|
|
130
|
+
subfolder = 'javascripts' if filename.endswith('.js') else 'css'
|
|
131
|
+
dest_path = os.path.join(config['site_dir'], subfolder, filename)
|
|
132
|
+
src_path = os.path.join(assets_dir, filename)
|
|
133
|
+
|
|
134
|
+
if not os.path.exists(src_path):
|
|
135
|
+
raise FileNotFoundError(
|
|
136
|
+
f"ZipBundle: Asset '{filename}' not found in package at {src_path}. "
|
|
137
|
+
"Your installation may be corrupt — try reinstalling mkdocs-zip-bundle."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
142
|
+
shutil.copyfile(src_path, dest_path)
|
|
143
|
+
except OSError as e:
|
|
144
|
+
raise OSError(
|
|
145
|
+
f"ZipBundle: Failed to copy '{filename}' to {dest_path}: {e}. "
|
|
146
|
+
"Downloads will not work in the built site."
|
|
147
|
+
) from e
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mkdocs-zip-bundle-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A MkDocs plugin to bundle specific code blocks into a downloadable ZIP or raw file.
|
|
5
|
+
Author: Daemonless
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/daemonless/mkdocs-zip-bundle-plugin
|
|
8
|
+
Project-URL: Repository, https://github.com/daemonless/mkdocs-zip-bundle-plugin
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/daemonless/mkdocs-zip-bundle-plugin/issues
|
|
10
|
+
Keywords: mkdocs,zip,bundle,placeholders,interactive
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: mkdocs>=1.4.0
|
|
15
|
+
Requires-Dist: beautifulsoup4>=4.11.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# mkdocs-zip-bundle-plugin
|
|
19
|
+
|
|
20
|
+
A MkDocs plugin that turns code blocks into downloadable files. Tag any code block with a bundle ID and filename — the plugin injects a download button automatically. Works with single files (direct download) or multiple files (ZIP archive).
|
|
21
|
+
|
|
22
|
+
Built to pair with [`mkdocs-placeholder-plugin`](https://github.com/six-two/mkdocs-placeholder-plugin): if your docs use interactive placeholders like `@PORT@`, the downloaded file will contain the user's actual values, not the defaults.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Single file downloads** — one code block gets a direct raw file download, no ZIP needed
|
|
27
|
+
- **Multi-file ZIP bundles** — group multiple code blocks into one ZIP with a single button
|
|
28
|
+
- **Placeholder-aware** — captures the live browser state, so user-edited values are included in the download
|
|
29
|
+
- **Nested paths** — use `configs/app.yaml` as a filename to create subdirectories inside the ZIP
|
|
30
|
+
- **Custom button labels** — override auto-generated text per bundle
|
|
31
|
+
- **Self-contained** — ships with JSZip and default styling, no extra dependencies
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install mkdocs-zip-bundle-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Python 3.8+ and MkDocs 1.4+. Download buttons require a modern browser (Chrome, Firefox, Safari, Edge).
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
plugins:
|
|
45
|
+
- search
|
|
46
|
+
- zip-bundle:
|
|
47
|
+
include_jszip: true # set to false if you already load JSZip elsewhere
|
|
48
|
+
zip_label_suffix: "(.zip)" # appended to multi-file bundle button labels
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Also enable the `attr_list` extension so MkDocs can read attributes on code blocks:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
markdown_extensions:
|
|
55
|
+
- attr_list
|
|
56
|
+
- pymdownx.superfences # recommended for reliable attribute support
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
Add `data-zip-bundle` and `data-zip-filename` attributes to any fenced code block:
|
|
62
|
+
|
|
63
|
+
### Single file
|
|
64
|
+
|
|
65
|
+
```yaml { data-zip-bundle="my-app" data-zip-filename="compose.yaml" }
|
|
66
|
+
services:
|
|
67
|
+
app:
|
|
68
|
+
image: my-image:latest
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The plugin injects a **Download compose.yaml** button directly after the code block.
|
|
72
|
+
|
|
73
|
+
### Multiple files (ZIP)
|
|
74
|
+
|
|
75
|
+
```yaml { data-zip-bundle="my-app" data-zip-filename="compose.yaml" }
|
|
76
|
+
services:
|
|
77
|
+
app:
|
|
78
|
+
image: my-image:latest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash { data-zip-bundle="my-app" data-zip-filename="setup.sh" }
|
|
82
|
+
mkdir -p /data/app
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both blocks share the same bundle ID. The plugin injects a single **Download My App (.zip)** button after the last block.
|
|
86
|
+
|
|
87
|
+
### Custom button label
|
|
88
|
+
|
|
89
|
+
```yaml { data-zip-bundle="my-app" data-zip-filename="compose.yaml" data-zip-label="Download config" }
|
|
90
|
+
...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Force ZIP for a single file
|
|
94
|
+
|
|
95
|
+
```yaml { data-zip-bundle="my-app" data-zip-filename="compose.yaml" data-zip-force="true" }
|
|
96
|
+
...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Nested directories
|
|
100
|
+
|
|
101
|
+
```yaml { data-zip-bundle="my-app" data-zip-filename="config/app.yaml" }
|
|
102
|
+
...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```bash { data-zip-bundle="my-app" data-zip-filename="scripts/setup.sh" }
|
|
106
|
+
...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The ZIP will contain `config/app.yaml` and `scripts/setup.sh` preserving the directory structure.
|
|
110
|
+
|
|
111
|
+
## How it works with placeholders
|
|
112
|
+
|
|
113
|
+
If you use [`mkdocs-placeholder-plugin`](https://github.com/six-two/mkdocs-placeholder-plugin), your docs can have editable values like `@PORT@` or `@DATA_PATH@` that users customize in the browser.
|
|
114
|
+
|
|
115
|
+
When the user clicks a download button from this plugin, the downloaded file contains whatever is currently in the code block — including any values the user has already changed. This makes it possible to offer personalized, copy-paste-ready config files directly from your documentation.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT — see [LICENSE](LICENSE) for details. Bundles [JSZip](https://github.com/Stuk/jszip) (MIT).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mkdocs_zip_bundle/__init__.py,sha256=3jNJ6SfgMi4B6OrY8UlvKUyHSQJcSX7Gs8_MnC7cV7w,27
|
|
2
|
+
mkdocs_zip_bundle/plugin.py,sha256=EGJd_bnC0PIqjTFbkamDfKdr5dcp7X_V0OiSzapA4ZE,5528
|
|
3
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/licenses/LICENSE,sha256=0haqjdG6Ov4T93hkmNLM8l_MGwhdib1qTvc5Wh6YvSE,1427
|
|
4
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/METADATA,sha256=CmsgDFMHiRMjA6mEqwmvmZzYGuZB29qJefOS2wS9N_E,4330
|
|
5
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/entry_points.txt,sha256=LcWQlmvHSbe44XiW7xEcoxAaVuBZXHiWvLv4FG7m90k,71
|
|
7
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/top_level.txt,sha256=MkX8MjVWHidUsmqRAWdNi5P4wi4Wd2L66tV2s9rNTg0,18
|
|
8
|
+
mkdocs_zip_bundle_plugin-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daemonless
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This package bundles JSZip v3.10.1, which includes pako.
|
|
26
|
+
|
|
27
|
+
JSZip is copyright (c) 2009-2016 Stuart Knightley <stuart@stuartk.com>
|
|
28
|
+
Dual licensed under the MIT License or GPLv3.
|
|
29
|
+
Source: https://github.com/Stuk/jszip
|
|
30
|
+
|
|
31
|
+
pako is copyright (c) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
|
|
32
|
+
Licensed under the MIT License.
|
|
33
|
+
Source: https://github.com/nodeca/pako
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mkdocs_zip_bundle
|