mkdocs-zip-bundle-plugin 0.1.1__tar.gz → 0.2.0__tar.gz

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.
Files changed (19) hide show
  1. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/PKG-INFO +7 -5
  2. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/README.md +4 -2
  3. mkdocs_zip_bundle_plugin-0.2.0/mkdocs_zip_bundle/assets/zip-bundle.js +157 -0
  4. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/PKG-INFO +7 -5
  5. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/pyproject.toml +6 -3
  6. mkdocs_zip_bundle_plugin-0.1.1/mkdocs_zip_bundle/assets/zip-bundle.js +0 -67
  7. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/LICENSE +0 -0
  8. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle/__init__.py +0 -0
  9. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle/assets/jszip.min.js +0 -0
  10. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle/assets/zip-bundle.css +0 -0
  11. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle/plugin.py +0 -0
  12. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/SOURCES.txt +0 -0
  13. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/dependency_links.txt +0 -0
  14. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/entry_points.txt +0 -0
  15. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/requires.txt +0 -0
  16. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/mkdocs_zip_bundle_plugin.egg-info/top_level.txt +0 -0
  17. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/setup.cfg +0 -0
  18. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/tests/test_integration.py +0 -0
  19. {mkdocs_zip_bundle_plugin-0.1.1 → mkdocs_zip_bundle_plugin-0.2.0}/tests/test_plugin.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-zip-bundle-plugin
3
- Version: 0.1.1
4
- Summary: A MkDocs plugin to bundle specific code blocks into a downloadable ZIP or raw file.
3
+ Version: 0.2.0
4
+ Summary: Bundle code blocks into downloadable ZIP or raw files — for MkDocs/Material and Zensical.
5
5
  Author: Daemonless
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/daemonless/mkdocs-zip-bundle-plugin
8
8
  Project-URL: Repository, https://github.com/daemonless/mkdocs-zip-bundle-plugin
9
9
  Project-URL: Bug Tracker, https://github.com/daemonless/mkdocs-zip-bundle-plugin/issues
10
- Keywords: mkdocs,zip,bundle,placeholders,interactive
10
+ Keywords: mkdocs,material,zensical,zip,bundle,placeholders,interactive
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
@@ -17,9 +17,11 @@ Dynamic: license-file
17
17
 
18
18
  # mkdocs-zip-bundle-plugin
19
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).
20
+ Turn code blocks into downloadable files — for **MkDocs/Material** and **[Zensical](https://zensical.org)**. Tag any code block with a bundle ID and filename and a download button is injected automatically. Works with single files (direct download) or multiple files (ZIP archive).
21
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.
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. _(MkDocs/Material — see [Using with Zensical](https://mkdocs-zip-bundle-plugin.daemonless.io/configuration/#using-with-zensical) for the Zensical caveat.)_
23
+
24
+ **[Live demo → mkdocs-zip-bundle-plugin.daemonless.io](https://mkdocs-zip-bundle-plugin.daemonless.io)**
23
25
 
24
26
  ## Features
25
27
 
@@ -1,8 +1,10 @@
1
1
  # mkdocs-zip-bundle-plugin
2
2
 
3
- 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).
3
+ Turn code blocks into downloadable files — for **MkDocs/Material** and **[Zensical](https://zensical.org)**. Tag any code block with a bundle ID and filename and a download button is injected automatically. Works with single files (direct download) or multiple files (ZIP archive).
4
4
 
5
- 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.
5
+ 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. _(MkDocs/Material — see [Using with Zensical](https://mkdocs-zip-bundle-plugin.daemonless.io/configuration/#using-with-zensical) for the Zensical caveat.)_
6
+
7
+ **[Live demo → mkdocs-zip-bundle-plugin.daemonless.io](https://mkdocs-zip-bundle-plugin.daemonless.io)**
6
8
 
7
9
  ## Features
8
10
 
@@ -0,0 +1,157 @@
1
+ // ── Button injection ──────────────────────────────────────────────────
2
+ // Mirrors the Python plugin's on_page_content / _create_button so the
3
+ // download buttons exist even where the plugin's build hook never runs
4
+ // (e.g. Zensical). Idempotent: skips any bundle that already has a button,
5
+ // so on MkDocs/Material (server-injected) this is a harmless no-op.
6
+
7
+ function sanitizeFilename(filename) {
8
+ // Match plugin._sanitize_filename: prevent path traversal, allow subdirs.
9
+ filename = filename.replace(/\\/g, '/'); // backslashes -> forward
10
+ filename = filename.replace(/\.\.(?=\/|$)/g, ''); // drop ".." segments
11
+ filename = filename.replace(/^\/+/, ''); // no leading slashes
12
+ filename = filename.replace(/\/+/g, '/'); // collapse slashes
13
+ return filename.trim();
14
+ }
15
+
16
+ function titleCase(str) {
17
+ // Python str.title(): capitalize each word, lowercase the rest.
18
+ return str.replace(/\w\S*/g, t => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());
19
+ }
20
+
21
+ function buildButtonContainer(bundleId, elements) {
22
+ const cfg = window.zipBundleConfig || {};
23
+ const labelSuffix = cfg.labelSuffix || '(.zip)';
24
+
25
+ const btn = document.createElement('button');
26
+ btn.className = 'md-button zip-bundle-btn';
27
+ btn.setAttribute('data-bundle-id', bundleId);
28
+ btn.type = 'button';
29
+ btn.setAttribute('aria-label', `Download ${bundleId.replace(/-/g, ' ')} bundle`);
30
+
31
+ const customLabel = elements
32
+ .map(el => el.getAttribute('data-zip-label'))
33
+ .find(Boolean);
34
+ const forceZip = elements.some(el => el.getAttribute('data-zip-force') === 'true');
35
+
36
+ if (customLabel) {
37
+ btn.textContent = customLabel;
38
+ } else if (elements.length === 1 && !forceZip) {
39
+ const filename = elements[0].getAttribute('data-zip-filename') || 'file';
40
+ const displayName = filename.split('/').pop(); // basename
41
+ btn.textContent = `Download ${displayName}`;
42
+ } else {
43
+ const label = titleCase(bundleId.replace(/-/g, ' '));
44
+ btn.textContent = `Download ${label} ${labelSuffix}`.trim();
45
+ }
46
+
47
+ const container = document.createElement('div');
48
+ container.className = 'zip-bundle-container';
49
+ container.appendChild(btn);
50
+ return container;
51
+ }
52
+
53
+ function injectZipButtons() {
54
+ const tagged = document.querySelectorAll('[data-zip-bundle]');
55
+ if (tagged.length === 0) return;
56
+
57
+ // Group elements by bundle id, preserving document order.
58
+ const bundles = new Map();
59
+ tagged.forEach(el => {
60
+ if (el.hasAttribute('data-zip-filename')) {
61
+ el.setAttribute('data-zip-filename',
62
+ sanitizeFilename(el.getAttribute('data-zip-filename')));
63
+ }
64
+ const id = el.getAttribute('data-zip-bundle');
65
+ if (!bundles.has(id)) bundles.set(id, []);
66
+ bundles.get(id).push(el);
67
+ });
68
+
69
+ bundles.forEach((elements, id) => {
70
+ // Idempotent: skip if a button already exists (server-injected or a
71
+ // prior run under instant navigation).
72
+ const sel = `.zip-bundle-btn[data-bundle-id="${CSS.escape(id)}"]`;
73
+ if (document.querySelector(sel)) return;
74
+ const container = buildButtonContainer(id, elements);
75
+ elements[elements.length - 1].insertAdjacentElement('afterend', container);
76
+ });
77
+ }
78
+
79
+ // Run on initial load and on every (instant) navigation. Material/Zensical
80
+ // expose the document$ observable; fall back to DOMContentLoaded elsewhere.
81
+ if (window.document$ && typeof window.document$.subscribe === 'function') {
82
+ window.document$.subscribe(injectZipButtons);
83
+ } else if (document.readyState !== 'loading') {
84
+ injectZipButtons();
85
+ } else {
86
+ document.addEventListener('DOMContentLoaded', injectZipButtons);
87
+ }
88
+
89
+ // ── Download handling ─────────────────────────────────────────────────
90
+
91
+ document.addEventListener('click', function(e) {
92
+ const btn = e.target.closest('.zip-bundle-btn[data-bundle-id]');
93
+ if (btn) {
94
+ downloadZipBundle(btn.getAttribute('data-bundle-id'));
95
+ }
96
+ });
97
+
98
+ async function downloadZipBundle(bundleId) {
99
+ const elements = document.querySelectorAll(`[data-zip-bundle="${bundleId}"]`);
100
+
101
+ if (elements.length === 0) {
102
+ console.warn(`No elements found for bundle ID: ${bundleId}`);
103
+ return;
104
+ }
105
+
106
+ const forceZip = Array.from(elements).some(el => el.getAttribute('data-zip-force') === 'true');
107
+
108
+ // If it's only one file AND we aren't forcing a ZIP, download it directly
109
+ if (elements.length === 1 && !forceZip) {
110
+ const el = elements[0];
111
+ const filename = el.getAttribute('data-zip-filename') || 'file';
112
+ const codeEl = el.querySelector('code') || el;
113
+ const content = codeEl.innerText;
114
+
115
+ // Ensure UTF-8 encoding
116
+ const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
117
+ const url = URL.createObjectURL(blob);
118
+ triggerDownload(url, filename);
119
+ return;
120
+ }
121
+
122
+ // Multiple files (or forced single ZIP)
123
+ if (typeof JSZip === 'undefined') {
124
+ console.error('JSZip is not loaded. Please include it in your mkdocs.yml extra_javascript or enable it in the plugin config.');
125
+ alert('Error: ZIP library not loaded.');
126
+ return;
127
+ }
128
+
129
+ const zip = new JSZip();
130
+ elements.forEach(el => {
131
+ const filename = el.getAttribute('data-zip-filename') || 'unnamed-file';
132
+ const codeEl = el.querySelector('code') || el;
133
+ const content = codeEl.innerText;
134
+
135
+ // Skip empty files if they have no content
136
+ if (content.trim().length === 0) {
137
+ console.warn(`Skipping empty file: ${filename}`);
138
+ return;
139
+ }
140
+
141
+ zip.file(filename, content);
142
+ });
143
+
144
+ const blob = await zip.generateAsync({type: "blob"});
145
+ const url = URL.createObjectURL(blob);
146
+ triggerDownload(url, `${bundleId}.zip`);
147
+ }
148
+
149
+ function triggerDownload(url, filename) {
150
+ const a = document.createElement('a');
151
+ a.href = url;
152
+ a.download = filename;
153
+ document.body.appendChild(a);
154
+ a.click();
155
+ document.body.removeChild(a);
156
+ URL.revokeObjectURL(url);
157
+ }
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs-zip-bundle-plugin
3
- Version: 0.1.1
4
- Summary: A MkDocs plugin to bundle specific code blocks into a downloadable ZIP or raw file.
3
+ Version: 0.2.0
4
+ Summary: Bundle code blocks into downloadable ZIP or raw files — for MkDocs/Material and Zensical.
5
5
  Author: Daemonless
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/daemonless/mkdocs-zip-bundle-plugin
8
8
  Project-URL: Repository, https://github.com/daemonless/mkdocs-zip-bundle-plugin
9
9
  Project-URL: Bug Tracker, https://github.com/daemonless/mkdocs-zip-bundle-plugin/issues
10
- Keywords: mkdocs,zip,bundle,placeholders,interactive
10
+ Keywords: mkdocs,material,zensical,zip,bundle,placeholders,interactive
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
@@ -17,9 +17,11 @@ Dynamic: license-file
17
17
 
18
18
  # mkdocs-zip-bundle-plugin
19
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).
20
+ Turn code blocks into downloadable files — for **MkDocs/Material** and **[Zensical](https://zensical.org)**. Tag any code block with a bundle ID and filename and a download button is injected automatically. Works with single files (direct download) or multiple files (ZIP archive).
21
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.
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. _(MkDocs/Material — see [Using with Zensical](https://mkdocs-zip-bundle-plugin.daemonless.io/configuration/#using-with-zensical) for the Zensical caveat.)_
23
+
24
+ **[Live demo → mkdocs-zip-bundle-plugin.daemonless.io](https://mkdocs-zip-bundle-plugin.daemonless.io)**
23
25
 
24
26
  ## Features
25
27
 
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mkdocs-zip-bundle-plugin"
7
- version = "0.1.1"
8
- description = "A MkDocs plugin to bundle specific code blocks into a downloadable ZIP or raw file."
7
+ version = "0.2.0"
8
+ description = "Bundle code blocks into downloadable ZIP or raw files — for MkDocs/Material and Zensical."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Daemonless" }]
11
11
  license = { text = "MIT" }
12
12
  requires-python = ">=3.8"
13
- keywords = ["mkdocs", "zip", "bundle", "placeholders", "interactive"]
13
+ keywords = ["mkdocs", "material", "zensical", "zip", "bundle", "placeholders", "interactive"]
14
14
  dependencies = [
15
15
  "mkdocs>=1.4.0",
16
16
  "beautifulsoup4>=4.11.0"
@@ -24,5 +24,8 @@ Repository = "https://github.com/daemonless/mkdocs-zip-bundle-plugin"
24
24
  [project.entry-points."mkdocs.plugins"]
25
25
  zip-bundle = "mkdocs_zip_bundle.plugin:ZipBundlePlugin"
26
26
 
27
+ [tool.setuptools.packages.find]
28
+ include = ["mkdocs_zip_bundle*"]
29
+
27
30
  [tool.setuptools.package-data]
28
31
  mkdocs_zip_bundle = ["assets/*"]
@@ -1,67 +0,0 @@
1
- document.addEventListener('click', function(e) {
2
- const btn = e.target.closest('.zip-bundle-btn[data-bundle-id]');
3
- if (btn) {
4
- downloadZipBundle(btn.getAttribute('data-bundle-id'));
5
- }
6
- });
7
-
8
- async function downloadZipBundle(bundleId) {
9
- const elements = document.querySelectorAll(`[data-zip-bundle="${bundleId}"]`);
10
-
11
- if (elements.length === 0) {
12
- console.warn(`No elements found for bundle ID: ${bundleId}`);
13
- return;
14
- }
15
-
16
- const forceZip = Array.from(elements).some(el => el.getAttribute('data-zip-force') === 'true');
17
-
18
- // If it's only one file AND we aren't forcing a ZIP, download it directly
19
- if (elements.length === 1 && !forceZip) {
20
- const el = elements[0];
21
- const filename = el.getAttribute('data-zip-filename') || 'file';
22
- const codeEl = el.querySelector('code') || el;
23
- const content = codeEl.innerText;
24
-
25
- // Ensure UTF-8 encoding
26
- const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
27
- const url = URL.createObjectURL(blob);
28
- triggerDownload(url, filename);
29
- return;
30
- }
31
-
32
- // Multiple files (or forced single ZIP)
33
- if (typeof JSZip === 'undefined') {
34
- console.error('JSZip is not loaded. Please include it in your mkdocs.yml extra_javascript or enable it in the plugin config.');
35
- alert('Error: ZIP library not loaded.');
36
- return;
37
- }
38
-
39
- const zip = new JSZip();
40
- elements.forEach(el => {
41
- const filename = el.getAttribute('data-zip-filename') || 'unnamed-file';
42
- const codeEl = el.querySelector('code') || el;
43
- const content = codeEl.innerText;
44
-
45
- // Skip empty files if they have no content
46
- if (content.trim().length === 0) {
47
- console.warn(`Skipping empty file: ${filename}`);
48
- return;
49
- }
50
-
51
- zip.file(filename, content);
52
- });
53
-
54
- const blob = await zip.generateAsync({type: "blob"});
55
- const url = URL.createObjectURL(blob);
56
- triggerDownload(url, `${bundleId}.zip`);
57
- }
58
-
59
- function triggerDownload(url, filename) {
60
- const a = document.createElement('a');
61
- a.href = url;
62
- a.download = filename;
63
- document.body.appendChild(a);
64
- a.click();
65
- document.body.removeChild(a);
66
- URL.revokeObjectURL(url);
67
- }