hexo-plugin-waterfall 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wuhunyu
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.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # hexo-plugin-waterfall
2
+
3
+ Hexo 瀑布流图片画廊插件,支持 Lightbox 放大预览和图片懒加载。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install hexo-plugin-waterfall --save
9
+ ```
10
+
11
+ 或者将插件目录复制到 Hexo 项目的 `node_modules` 目录下。
12
+
13
+ ## 配置
14
+
15
+ 在 Hexo 的 `_config.yml` 中添加以下配置:
16
+
17
+ ```yaml
18
+ waterfall:
19
+ enable: true # 是否启用插件
20
+ columns: 3 # 默认列数
21
+ gap: 10px # 图片间距
22
+ borderRadius: 5px # 图片圆角
23
+ lazyload: true # 启用懒加载
24
+ lightbox: true # 启用点击放大
25
+ ```
26
+
27
+ ## 使用
28
+
29
+ 在 Markdown 文件中使用 `waterfall` 标签:
30
+
31
+ ```markdown
32
+ {% waterfall 3 %}
33
+ <img alt="图片描述1" src="/images/photo1.jpg"/>
34
+ <img alt="图片描述2" src="/images/photo2.jpg"/>
35
+ <img alt="图片描述3" src="/images/photo3.jpg"/>
36
+ <img alt="图片描述4" src="/images/photo4.jpg"/>
37
+ {% endwaterfall %}
38
+ ```
39
+
40
+ ### 参数说明
41
+
42
+ - `3`:可选参数,指定列数。如果不指定,则使用配置文件中的 `columns` 值。
43
+
44
+ ### 图片属性
45
+
46
+ - `src`:图片路径(必填)
47
+ - `alt`:图片描述(可选),在 Lightbox 中会显示为图片标题
48
+
49
+ ## 功能特性
50
+
51
+ ### 瀑布流布局
52
+
53
+ 使用 CSS `column-count` 实现瀑布流布局,自动响应式适配:
54
+ - 桌面端:使用指定列数
55
+ - 平板端(<=768px):2 列
56
+ - 手机端(<=480px):1 列
57
+
58
+ ### Lightbox 放大预览
59
+
60
+ - 点击图片弹出全屏预览
61
+ - 左右箭头切换图片
62
+ - 键盘支持:← → 切换,ESC 关闭
63
+ - 显示图片描述(alt 属性)
64
+ - 显示图片计数器
65
+
66
+ ### 图片懒加载
67
+
68
+ - 使用 IntersectionObserver API
69
+ - 图片进入可视区域时才加载
70
+ - 加载前显示占位动画
71
+ - 加载完成淡入效果
72
+
73
+ ## 示例效果
74
+
75
+ ```markdown
76
+ {% waterfall 4 %}
77
+ <img alt="春日樱花" src="/gallery/sakura.jpg"/>
78
+ <img alt="夏日海滩" src="/gallery/beach.jpg"/>
79
+ <img alt="秋日红叶" src="/gallery/autumn.jpg"/>
80
+ <img alt="冬日雪景" src="/gallery/winter.jpg"/>
81
+ {% endwaterfall %}
82
+ ```
83
+
84
+ ## License
85
+
86
+ MIT
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const loadConfig = require('./lib/config');
4
+ const registerTag = require('./lib/tag');
5
+ const registerFilter = require('./lib/filter');
6
+
7
+ // Load configuration
8
+ const config = loadConfig(hexo);
9
+
10
+ // Skip if disabled
11
+ if (!config.enable) {
12
+ return;
13
+ }
14
+
15
+ // Register tag: {% waterfall n %} ... {% endwaterfall %}
16
+ hexo.extend.tag.register('waterfall', registerTag(hexo, config), { ends: true });
17
+
18
+ // Register filter: inject CSS and JS after HTML render
19
+ hexo.extend.filter.register('after_render:html', registerFilter(hexo, config));
package/lib/config.js ADDED
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (hexo) {
4
+ const defaultConfig = {
5
+ enable: true,
6
+ columns: 3,
7
+ gap: '10px',
8
+ borderRadius: '5px',
9
+ lazyload: true,
10
+ lightbox: true
11
+ };
12
+
13
+ const config = hexo.config.waterfall || {};
14
+ return Object.assign({}, defaultConfig, config);
15
+ };
package/lib/consts.js ADDED
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ WATERFALL_CONTAINER: 'waterfall-container',
5
+ WATERFALL_ITEM: 'waterfall-item'
6
+ };
package/lib/filter.js ADDED
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ const { WATERFALL_CONTAINER } = require('./consts');
4
+ const templates = require('./templates');
5
+
6
+ /**
7
+ * Register filter to inject CSS and JS
8
+ * @param {object} hexo - Hexo instance
9
+ * @param {object} config - Plugin configuration
10
+ * @returns {function} Filter handler function
11
+ */
12
+ module.exports = function (hexo, config) {
13
+ return function (str) {
14
+ // Only inject if page contains waterfall container
15
+ if (str.indexOf(WATERFALL_CONTAINER) === -1) {
16
+ return str;
17
+ }
18
+
19
+ const assets = templates.renderInjectedAssets(config);
20
+
21
+ // Inject before </body>
22
+ return str.replace('</body>', assets + '</body>');
23
+ };
24
+ };
package/lib/tag.js ADDED
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const templates = require('./templates');
4
+
5
+ /**
6
+ * Extract attribute value from img tag
7
+ * @param {string} imgTag - The full img tag string
8
+ * @param {string} attrName - Attribute name to extract
9
+ * @returns {string} Attribute value or empty string
10
+ */
11
+ function extractAttribute(imgTag, attrName) {
12
+ // Match both single and double quotes
13
+ const regex = new RegExp(attrName + '=["\']([^"\']*)["\']', 'i');
14
+ const match = imgTag.match(regex);
15
+ return match ? match[1] : '';
16
+ }
17
+
18
+ /**
19
+ * Parse img tags from content
20
+ * @param {string} content - HTML content containing img tags
21
+ * @returns {Array} Array of image objects {src, alt}
22
+ */
23
+ function parseImages(content) {
24
+ const images = [];
25
+ // Match all <img> tags
26
+ const imgTagRegex = /<img\s+[^>]*\/?>/gi;
27
+ let match;
28
+
29
+ while ((match = imgTagRegex.exec(content)) !== null) {
30
+ const imgTag = match[0];
31
+ const src = extractAttribute(imgTag, 'src');
32
+
33
+ // Only add if src exists
34
+ if (src) {
35
+ images.push({
36
+ src: src,
37
+ alt: extractAttribute(imgTag, 'alt')
38
+ });
39
+ }
40
+ }
41
+
42
+ return images;
43
+ }
44
+
45
+ /**
46
+ * Register waterfall tag handler
47
+ * @param {object} hexo - Hexo instance
48
+ * @param {object} config - Plugin configuration
49
+ * @returns {function} Tag handler function
50
+ */
51
+ module.exports = function (hexo, config) {
52
+ return function (args, content) {
53
+ // Parse columns from args, fallback to config default
54
+ // Validate range: 1-12
55
+ let columns = parseInt(args[0], 10) || config.columns;
56
+ columns = Math.min(Math.max(columns, 1), 12);
57
+
58
+ // Parse images from content
59
+ const images = parseImages(content);
60
+
61
+ if (images.length === 0) {
62
+ return '';
63
+ }
64
+
65
+ // Generate HTML
66
+ return templates.renderTagHtml(images, columns, config);
67
+ };
68
+ };
@@ -0,0 +1,462 @@
1
+ 'use strict';
2
+
3
+ const { WATERFALL_CONTAINER, WATERFALL_ITEM } = require('./consts');
4
+
5
+ /**
6
+ * Escape HTML special characters to prevent XSS
7
+ * @param {string} str - Input string
8
+ * @returns {string} Escaped string
9
+ */
10
+ function escapeHtml(str) {
11
+ if (!str) return '';
12
+ return String(str)
13
+ .replace(/&/g, '&amp;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;');
18
+ }
19
+
20
+ /**
21
+ * Sanitize CSS value to prevent CSS injection
22
+ * @param {string} value - CSS value
23
+ * @param {string} defaultValue - Default fallback value
24
+ * @returns {string} Sanitized value
25
+ */
26
+ function sanitizeCssValue(value, defaultValue) {
27
+ if (!value) return defaultValue;
28
+ // Only allow valid CSS length values
29
+ if (/^[\d.]+(px|em|rem|%|vw|vh)?$/.test(String(value))) {
30
+ return value;
31
+ }
32
+ return defaultValue;
33
+ }
34
+
35
+ /**
36
+ * Generate HTML for the waterfall tag
37
+ * @param {Array} images - Array of image objects {src, alt}
38
+ * @param {number} columns - Number of columns
39
+ * @param {object} config - Plugin configuration
40
+ * @returns {string} HTML string
41
+ */
42
+ exports.renderTagHtml = function (images, columns, config) {
43
+ const imagesHtml = images.map((img, index) => {
44
+ const safeSrc = escapeHtml(img.src);
45
+ const safeAlt = escapeHtml(img.alt || '');
46
+ const src = config.lazyload ? '' : safeSrc;
47
+ const dataSrc = config.lazyload ? `data-src="${safeSrc}"` : '';
48
+ const lazyClass = config.lazyload ? 'waterfall-lazy' : '';
49
+ const clickHandler = config.lightbox ? `onclick="event.stopPropagation(); event.preventDefault(); waterfallLightbox.open(this, ${index})"` : '';
50
+
51
+ return `
52
+ <div class="${WATERFALL_ITEM}">
53
+ <img src="${src}" ${dataSrc} alt="${safeAlt}" class="${lazyClass}" ${clickHandler}/>
54
+ </div>
55
+ `;
56
+ }).join('');
57
+
58
+ return `
59
+ <div class="${WATERFALL_CONTAINER}" style="--wf-columns: ${columns};" data-columns="${columns}">
60
+ ${imagesHtml}
61
+ </div>
62
+ `;
63
+ };
64
+
65
+ /**
66
+ * Generate CSS and JS to inject into the page
67
+ * @param {object} config - Plugin configuration
68
+ * @returns {string} CSS and JS string
69
+ */
70
+ exports.renderInjectedAssets = function (config) {
71
+ // Sanitize CSS values
72
+ const safeGap = sanitizeCssValue(config.gap, '10px');
73
+ const safeBorderRadius = sanitizeCssValue(config.borderRadius, '5px');
74
+ const safeColumns = Math.min(Math.max(parseInt(config.columns, 10) || 3, 1), 12);
75
+
76
+ const css = `
77
+ <style>
78
+ /* Waterfall Container */
79
+ .${WATERFALL_CONTAINER} {
80
+ column-count: var(--wf-columns, ${safeColumns});
81
+ column-gap: ${safeGap};
82
+ width: 100%;
83
+ }
84
+
85
+ /* Waterfall Item */
86
+ .${WATERFALL_ITEM} {
87
+ break-inside: avoid;
88
+ margin-bottom: ${safeGap};
89
+ }
90
+
91
+ .${WATERFALL_ITEM} img {
92
+ width: 100%;
93
+ display: block;
94
+ border-radius: ${safeBorderRadius};
95
+ transition: transform 0.3s ease, opacity 0.3s ease;
96
+ cursor: ${config.lightbox ? 'pointer' : 'default'};
97
+ }
98
+
99
+ .${WATERFALL_ITEM} img:hover {
100
+ transform: scale(1.02);
101
+ }
102
+
103
+ /* Lazy Loading Styles */
104
+ .waterfall-lazy {
105
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
106
+ background-size: 200% 100%;
107
+ animation: waterfall-shimmer 1.5s infinite;
108
+ min-height: 150px;
109
+ opacity: 0;
110
+ }
111
+
112
+ .waterfall-lazy.loaded {
113
+ background: none;
114
+ animation: none;
115
+ opacity: 1;
116
+ min-height: auto;
117
+ }
118
+
119
+ .waterfall-lazy.error {
120
+ background: #f5f5f5;
121
+ animation: none;
122
+ opacity: 1;
123
+ min-height: 100px;
124
+ }
125
+
126
+ @keyframes waterfall-shimmer {
127
+ 0% { background-position: 200% 0; }
128
+ 100% { background-position: -200% 0; }
129
+ }
130
+
131
+ /* Lightbox Styles */
132
+ .waterfall-lightbox-overlay {
133
+ position: fixed;
134
+ top: 0;
135
+ left: 0;
136
+ width: 100%;
137
+ height: 100%;
138
+ background: rgba(0, 0, 0, 0.9);
139
+ z-index: 9999;
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ justify-content: center;
144
+ opacity: 0;
145
+ pointer-events: none;
146
+ transition: opacity 0.3s ease;
147
+ }
148
+
149
+ .waterfall-lightbox-overlay.active {
150
+ opacity: 1;
151
+ pointer-events: auto;
152
+ }
153
+
154
+ .waterfall-lightbox-content {
155
+ position: relative;
156
+ max-width: 90vw;
157
+ max-height: 80vh;
158
+ display: flex;
159
+ flex-direction: column;
160
+ align-items: center;
161
+ }
162
+
163
+ .waterfall-lightbox-content img {
164
+ max-width: 100%;
165
+ max-height: 75vh;
166
+ object-fit: contain;
167
+ border-radius: 4px;
168
+ }
169
+
170
+ .waterfall-lightbox-caption {
171
+ color: #fff;
172
+ text-align: center;
173
+ padding: 15px 20px;
174
+ font-size: 14px;
175
+ max-width: 80vw;
176
+ }
177
+
178
+ .waterfall-lightbox-close {
179
+ position: absolute;
180
+ top: 20px;
181
+ right: 20px;
182
+ color: #fff;
183
+ font-size: 30px;
184
+ cursor: pointer;
185
+ z-index: 10001;
186
+ width: 40px;
187
+ height: 40px;
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ transition: transform 0.2s ease;
192
+ }
193
+
194
+ .waterfall-lightbox-close:hover {
195
+ transform: scale(1.1);
196
+ }
197
+
198
+ .waterfall-lightbox-nav {
199
+ position: absolute;
200
+ top: 50%;
201
+ transform: translateY(-50%);
202
+ color: #fff;
203
+ font-size: 40px;
204
+ cursor: pointer;
205
+ padding: 20px;
206
+ user-select: none;
207
+ transition: opacity 0.2s ease;
208
+ z-index: 10001;
209
+ }
210
+
211
+ .waterfall-lightbox-nav:hover {
212
+ opacity: 0.7;
213
+ }
214
+
215
+ .waterfall-lightbox-prev {
216
+ left: 20px;
217
+ }
218
+
219
+ .waterfall-lightbox-next {
220
+ right: 20px;
221
+ }
222
+
223
+ .waterfall-lightbox-counter {
224
+ position: absolute;
225
+ bottom: 20px;
226
+ left: 50%;
227
+ transform: translateX(-50%);
228
+ color: rgba(255, 255, 255, 0.7);
229
+ font-size: 14px;
230
+ }
231
+
232
+ /* Responsive Breakpoints */
233
+ @media (max-width: 768px) {
234
+ .${WATERFALL_CONTAINER} {
235
+ column-count: 2 !important;
236
+ }
237
+
238
+ .waterfall-lightbox-nav {
239
+ font-size: 30px;
240
+ padding: 10px;
241
+ }
242
+
243
+ .waterfall-lightbox-prev {
244
+ left: 10px;
245
+ }
246
+
247
+ .waterfall-lightbox-next {
248
+ right: 10px;
249
+ }
250
+ }
251
+
252
+ @media (max-width: 480px) {
253
+ .${WATERFALL_CONTAINER} {
254
+ column-count: 1 !important;
255
+ }
256
+ }
257
+ </style>`;
258
+
259
+ let script = '';
260
+
261
+ // Lazy loading script
262
+ if (config.lazyload) {
263
+ script += `
264
+ <script>
265
+ (function() {
266
+ var lazyImages = document.querySelectorAll('.waterfall-lazy');
267
+
268
+ if ('IntersectionObserver' in window) {
269
+ var observer = new IntersectionObserver(function(entries) {
270
+ entries.forEach(function(entry) {
271
+ if (entry.isIntersecting) {
272
+ var img = entry.target;
273
+ var src = img.getAttribute('data-src');
274
+ if (src) {
275
+ img.src = src;
276
+ img.onload = function() {
277
+ img.classList.add('loaded');
278
+ };
279
+ img.onerror = function() {
280
+ img.classList.add('loaded');
281
+ img.classList.add('error');
282
+ };
283
+ img.removeAttribute('data-src');
284
+ }
285
+ observer.unobserve(img);
286
+ }
287
+ });
288
+ }, { rootMargin: '50px' });
289
+
290
+ lazyImages.forEach(function(img) {
291
+ observer.observe(img);
292
+ });
293
+ } else {
294
+ // Fallback for browsers without IntersectionObserver
295
+ lazyImages.forEach(function(img) {
296
+ var src = img.getAttribute('data-src');
297
+ if (src) {
298
+ img.src = src;
299
+ img.onload = function() {
300
+ img.classList.add('loaded');
301
+ };
302
+ img.onerror = function() {
303
+ img.classList.add('loaded');
304
+ img.classList.add('error');
305
+ };
306
+ }
307
+ });
308
+ }
309
+ })();
310
+ </script>`;
311
+ }
312
+
313
+ // Lightbox script
314
+ if (config.lightbox) {
315
+ script += `
316
+ <script>
317
+ var waterfallLightbox = (function() {
318
+ var overlay = null;
319
+ var images = [];
320
+ var currentIndex = 0;
321
+
322
+ function createOverlay() {
323
+ if (overlay) return;
324
+
325
+ overlay = document.createElement('div');
326
+ overlay.className = 'waterfall-lightbox-overlay';
327
+ overlay.innerHTML =
328
+ '<span class="waterfall-lightbox-close">&times;</span>' +
329
+ '<span class="waterfall-lightbox-nav waterfall-lightbox-prev">&#10094;</span>' +
330
+ '<span class="waterfall-lightbox-nav waterfall-lightbox-next">&#10095;</span>' +
331
+ '<div class="waterfall-lightbox-content">' +
332
+ '<img src="" alt=""/>' +
333
+ '<div class="waterfall-lightbox-caption"></div>' +
334
+ '</div>' +
335
+ '<div class="waterfall-lightbox-counter"></div>';
336
+
337
+ document.body.appendChild(overlay);
338
+
339
+ // Close button
340
+ overlay.querySelector('.waterfall-lightbox-close').addEventListener('click', function(e) {
341
+ e.stopPropagation();
342
+ e.preventDefault();
343
+ close();
344
+ });
345
+
346
+ // Nav buttons
347
+ overlay.querySelector('.waterfall-lightbox-prev').addEventListener('click', function(e) {
348
+ e.stopPropagation();
349
+ prev();
350
+ });
351
+ overlay.querySelector('.waterfall-lightbox-next').addEventListener('click', function(e) {
352
+ e.stopPropagation();
353
+ next();
354
+ });
355
+
356
+ // Close on overlay click
357
+ overlay.addEventListener('click', function(e) {
358
+ if (e.target === overlay) {
359
+ close();
360
+ }
361
+ });
362
+
363
+ // Keyboard navigation
364
+ document.addEventListener('keydown', handleKeydown);
365
+ }
366
+
367
+ function handleKeydown(e) {
368
+ if (!overlay || !overlay.classList.contains('active')) return;
369
+
370
+ switch(e.key) {
371
+ case 'Escape':
372
+ close();
373
+ break;
374
+ case 'ArrowLeft':
375
+ prev();
376
+ break;
377
+ case 'ArrowRight':
378
+ next();
379
+ break;
380
+ }
381
+ }
382
+
383
+ function collectImages(clickedImg) {
384
+ var container = clickedImg.closest('.${WATERFALL_CONTAINER}');
385
+ var imgs = container.querySelectorAll('.${WATERFALL_ITEM} img');
386
+ images = Array.prototype.map.call(imgs, function(img) {
387
+ return {
388
+ src: img.getAttribute('data-src') || img.src,
389
+ alt: img.alt || ''
390
+ };
391
+ });
392
+ }
393
+
394
+ function showImage(index) {
395
+ if (index < 0 || index >= images.length) return;
396
+
397
+ currentIndex = index;
398
+ var img = overlay.querySelector('.waterfall-lightbox-content img');
399
+ var caption = overlay.querySelector('.waterfall-lightbox-caption');
400
+ var counter = overlay.querySelector('.waterfall-lightbox-counter');
401
+
402
+ img.src = images[index].src;
403
+ img.alt = images[index].alt;
404
+ caption.textContent = images[index].alt;
405
+ caption.style.display = images[index].alt ? 'block' : 'none';
406
+ counter.textContent = (index + 1) + ' / ' + images.length;
407
+
408
+ // Show/hide nav buttons
409
+ overlay.querySelector('.waterfall-lightbox-prev').style.display = images.length > 1 ? 'block' : 'none';
410
+ overlay.querySelector('.waterfall-lightbox-next').style.display = images.length > 1 ? 'block' : 'none';
411
+ }
412
+
413
+ function open(clickedImg, index) {
414
+ createOverlay();
415
+ collectImages(clickedImg);
416
+ showImage(index);
417
+
418
+ // Prevent body scroll
419
+ document.body.style.overflow = 'hidden';
420
+
421
+ // Show overlay with animation
422
+ requestAnimationFrame(function() {
423
+ overlay.classList.add('active');
424
+ });
425
+ }
426
+
427
+ function close() {
428
+ if (!overlay) return;
429
+
430
+ overlay.classList.remove('active');
431
+ document.body.style.overflow = '';
432
+
433
+ setTimeout(function() {
434
+ images = [];
435
+ currentIndex = 0;
436
+ }, 300);
437
+ }
438
+
439
+ function prev() {
440
+ var newIndex = currentIndex - 1;
441
+ if (newIndex < 0) newIndex = images.length - 1;
442
+ showImage(newIndex);
443
+ }
444
+
445
+ function next() {
446
+ var newIndex = currentIndex + 1;
447
+ if (newIndex >= images.length) newIndex = 0;
448
+ showImage(newIndex);
449
+ }
450
+
451
+ return {
452
+ open: open,
453
+ close: close,
454
+ prev: prev,
455
+ next: next
456
+ };
457
+ })();
458
+ </script>`;
459
+ }
460
+
461
+ return css + script;
462
+ };
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "hexo-plugin-waterfall",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "keywords": [
6
+ "hexo",
7
+ "plugin",
8
+ "waterfall",
9
+ "gallery",
10
+ "lightbox"
11
+ ],
12
+ "author": "wuhunyu",
13
+ "license": "MIT",
14
+ "description": "Hexo waterfall image gallery plugin with lightbox support."
15
+ }