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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ zip-bundle = mkdocs_zip_bundle.plugin:ZipBundlePlugin
@@ -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