hexo-theme-linen 1.0.21 → 1.0.23

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 (46) hide show
  1. package/languages/en.yml +3 -0
  2. package/languages/zh-CN.yml +4 -1
  3. package/layout/footer.ejs +62 -0
  4. package/layout/layout.ejs +22 -9
  5. package/layout/partials/header.ejs +1 -1
  6. package/layout/partials/post-item.ejs +5 -5
  7. package/layout/partials/related-posts.ejs +1 -1
  8. package/layout/post.ejs +362 -24
  9. package/layout/series.ejs +277 -4
  10. package/package.json +1 -1
  11. package/scripts/font-version.js +5 -3
  12. package/scripts/helpers/tools.js +187 -191
  13. package/scripts/lazyload/client.js +11 -2
  14. package/scripts/lazyload/index.js +7 -5
  15. package/scripts/lazyload/inject.js +8 -0
  16. package/scripts/lazyload/process.js +202 -163
  17. package/scripts/markdown-it-image-grid.js +74 -10
  18. package/source/linen-theme/css/{mediaquery.scss → _mediaquery.scss} +183 -12
  19. package/source/linen-theme/css/atom.xsl +37 -0
  20. package/source/linen-theme/css/font-version.scss +1 -1
  21. package/source/linen-theme/css/images/layers-2x.png +0 -0
  22. package/source/linen-theme/css/images/layers.png +0 -0
  23. package/source/linen-theme/css/images/marker-icon-2x.png +0 -0
  24. package/source/linen-theme/css/images/marker-icon.png +0 -0
  25. package/source/linen-theme/css/images/marker-shadow.png +0 -0
  26. package/source/linen-theme/css/leaflet.css +661 -0
  27. package/source/linen-theme/css/linen.scss +1198 -114
  28. package/source/linen-theme/css/linkcard.scss +53 -11
  29. package/source/linen-theme/css/noscript.css +70 -0
  30. package/source/linen-theme/css/photoswipe-dynamic-caption-plugin.css +1 -1
  31. package/source/linen-theme/css/photoswipe.css +166 -1
  32. package/source/linen-theme/css/rss-en.xsl +1 -1
  33. package/source/linen-theme/css/rss-zh.xsl +1 -1
  34. package/source/linen-theme/css/vars.scss +1 -1
  35. package/source/linen-theme/img/pattern-randomized.svg +1 -0
  36. package/source/linen-theme/img/travel-map-fallback.svg +30 -0
  37. package/source/linen-theme/js/hdr.js +42 -12
  38. package/source/linen-theme/js/lazyload.js +11 -2
  39. package/source/linen-theme/js/leaflet.js +6 -0
  40. package/source/linen-theme/js/linen.js +114 -83
  41. package/source/linen-theme/js/photoswipe/photoswipe.esm.min.js +13 -0
  42. package/source/linen-theme/js/replace-emoji.js +2 -4
  43. package/source/linen-theme/js/toc.js +71 -121
  44. package/layout/partials/footer.ejs +0 -12
  45. package/source/linen-theme/img/icons/backref.svg +0 -1
  46. package/source/linen-theme/img/icons/more.svg +0 -4
@@ -5,13 +5,11 @@ const path = require("path");
5
5
  const { defaultConfig } = require("./config");
6
6
  function getElemAttributes(elemText) {
7
7
  // Regex to pick out start tag from start of element's HTML.
8
- var re_start_tag =
9
- /^<\w+\b(?:\s+[\w\-.:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[\w\-.:]+))?)*\s*\/?>/;
8
+ var re_start_tag = /^<\w+\b(?:\s+[\w\-.:]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[\w\-.:]+))?)*\s*\/?>/;
10
9
  var start_tag = elemText.match(re_start_tag);
11
10
  start_tag = start_tag ? start_tag[0] : "";
12
11
  // Regex to pick out attribute name and (optional) value from start tag.
13
- var re_attribs =
14
- /\s+([\w\-.:]+)(\s*=\s*(?:"([^"]*)"|'([^']*)'|([\w\-.:]+)))?/g;
12
+ var re_attribs = /\s+([\w\-.:]+)(\s*=\s*(?:"([^"]*)"|'([^']*)'|([\w\-.:]+)))?/g;
15
13
  var attribs = {}; // Store attribute name=value pairs in object.
16
14
  var match = re_attribs.exec(start_tag);
17
15
  while (match != null) {
@@ -22,8 +20,8 @@ function getElemAttributes(elemText) {
22
20
  value = match[3]
23
21
  ? match[3] // Attribute value is in $3, $4 or $5.
24
22
  : match[4]
25
- ? match[4]
26
- : match[5];
23
+ ? match[4]
24
+ : match[5];
27
25
  }
28
26
  attribs[attrib] = value;
29
27
  match = re_attribs.exec(start_tag);
@@ -70,11 +68,46 @@ function getInlineWidthStyle(style) {
70
68
  return filtered ? `style="${filtered};"` : "";
71
69
  }
72
70
 
71
+ function escapeAttribute(value = "") {
72
+ return String(value)
73
+ .replace(/&/g, "&amp;")
74
+ .replace(/"/g, "&quot;")
75
+ .replace(/</g, "&lt;")
76
+ .replace(/>/g, "&gt;");
77
+ }
78
+
79
+ function getLivePhotoInfo(src = "", style = "") {
80
+ src = decodeHexEntity(src).replace(/&amp;/g, "&");
81
+
82
+ if (!/\/static\/live-photo\//.test(src)) {
83
+ return null;
84
+ }
85
+
86
+ try {
87
+ const livePhotoUrl = new URL(src, "https://lynan.cn");
88
+ const picUrl = livePhotoUrl.searchParams.get("picUrl") || livePhotoUrl.searchParams.get("photoSrc");
89
+ const aspectRatio = /aspect-ratio\s*:\s*([^;]+)/i.exec(style)?.[1]?.trim() || "";
90
+ const [width, height] = aspectRatio.includes("/")
91
+ ? aspectRatio.split("/").map((value) => Number(value.trim()))
92
+ : [Number(aspectRatio) || 16, 1];
93
+
94
+ if (!picUrl || !width || !height) {
95
+ return null;
96
+ }
97
+
98
+ return {
99
+ picUrl,
100
+ width,
101
+ height
102
+ };
103
+ } catch (error) {
104
+ return null;
105
+ }
106
+ }
107
+
73
108
  function decodeHexEntity(str) {
74
109
  return str
75
- ? str.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) =>
76
- String.fromCharCode(parseInt(hex, 16))
77
- )
110
+ ? str.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
78
111
  : "";
79
112
  }
80
113
 
@@ -83,16 +116,26 @@ function isUrlEqual(src1 = "", src2 = "") {
83
116
  const u1 = new URL(src1);
84
117
  const u2 = new URL(src2);
85
118
 
86
- return (
87
- u1.pathname === u2.pathname &&
88
- u1.search === u2.search &&
89
- u1.hash === u2.hash
90
- );
119
+ return u1.pathname === u2.pathname && u1.search === u2.search && u1.hash === u2.hash;
91
120
  } catch (e) {
92
121
  return false;
93
122
  }
94
123
  }
95
124
 
125
+ function appendAttributeToTag(tag, attrName, attrValue) {
126
+ if (!attrValue || new RegExp(`\\s${attrName}=`).test(tag)) {
127
+ return tag;
128
+ }
129
+ return tag.replace(/\s*\/?>$/, (end) => ` ${attrName}="${attrValue}"${end}`);
130
+ }
131
+
132
+ function getPlaceholderStyle(placeholder = "") {
133
+ if (!placeholder || placeholder.startsWith("blurhash:")) {
134
+ return "";
135
+ }
136
+ return /http|data:image/.test(placeholder) ? `background-image: url(${placeholder})` : placeholder;
137
+ }
138
+
96
139
  function formatAttributes(str, src, imgs = [], language) {
97
140
  let res = {};
98
141
  const imgItem = imgs.find((item) => isUrlEqual(item.url, src || ""));
@@ -117,9 +160,7 @@ function formatAttributes(str, src, imgs = [], language) {
117
160
  aspectRatio = `${imgItem.width}/${imgItem.height}`;
118
161
  }
119
162
  if (styleAttr) {
120
- const styleObj = styleStringToObject(
121
- styleAttr.replace(/style="(.*?)"/gi, "$1")
122
- );
163
+ const styleObj = styleStringToObject(styleAttr.replace(/style="(.*?)"/gi, "$1"));
123
164
  if (styleObj["aspect-ratio"]) {
124
165
  aspectRatio = styleObj["aspect-ratio"];
125
166
  }
@@ -136,11 +177,9 @@ function formatAttributes(str, src, imgs = [], language) {
136
177
  if (aspectRatio) {
137
178
  if (/\//.test(aspectRatio)) {
138
179
  percentage =
139
- (
140
- (Number(aspectRatio.split("/")[1]) /
141
- Number(aspectRatio.split("/")[0])) *
142
- 100
143
- ).toFixed(5) + "%";
180
+ ((Number(aspectRatio.split("/")[1]) / Number(aspectRatio.split("/")[0])) * 100).toFixed(
181
+ 5
182
+ ) + "%";
144
183
  } else {
145
184
  percentage = 100 / Number(aspectRatio) + "%";
146
185
  }
@@ -150,9 +189,7 @@ function formatAttributes(str, src, imgs = [], language) {
150
189
 
151
190
  const getPlaceholderImage = (returnStr) => {
152
191
  let placeholderImage = attributesObj["data-placeholderimg"] || "";
153
- var placeholderImageRes = /placeholder=(\S+)=placeholder/gi.exec(
154
- attributesObj.alt
155
- );
192
+ var placeholderImageRes = /placeholder=(\S+)=placeholder/gi.exec(attributesObj.alt);
156
193
  if (placeholderImageRes && placeholderImageRes[1]) {
157
194
  placeholderImage = placeholderImageRes[1];
158
195
  }
@@ -170,8 +207,8 @@ function formatAttributes(str, src, imgs = [], language) {
170
207
  return isBlurHash
171
208
  ? ""
172
209
  : /http|data:image/.test(placeholderImage)
173
- ? `url(${placeholderImage})`
174
- : placeholderImage;
210
+ ? `url(${placeholderImage})`
211
+ : placeholderImage;
175
212
  };
176
213
 
177
214
  const getAltText = () => {
@@ -230,20 +267,14 @@ const getformatedStr = (str, attrs, placeholerStyle = "") => {
230
267
 
231
268
  if (placeholderImageStr) {
232
269
  if (/data-placeholderimg/.test(formatedStr)) {
233
- formatedStr = formatedStr.replace(
234
- `data-placeholderimg="${placeholderImageStr}"`,
235
- ""
236
- );
270
+ formatedStr = formatedStr.replace(`data-placeholderimg="${placeholderImageStr}"`, "");
237
271
  noScriptElementStr = noScriptElementStr.replace(
238
272
  `data-placeholderimg="${placeholderImageStr}"`,
239
273
  ""
240
274
  );
241
275
  }
242
276
  if (/\$placeholder=/.test(formatedStr)) {
243
- formatedStr = formatedStr.replace(
244
- `$placeholder=${placeholderImageStr}=placeholder`,
245
- ""
246
- );
277
+ formatedStr = formatedStr.replace(`$placeholder=${placeholderImageStr}=placeholder`, "");
247
278
  noScriptElementStr = noScriptElementStr.replace(
248
279
  `$placeholder=${placeholderImageStr}=placeholder`,
249
280
  ""
@@ -252,10 +283,7 @@ const getformatedStr = (str, attrs, placeholerStyle = "") => {
252
283
  }
253
284
  if (aspectRatioStr) {
254
285
  if (/\$aspect-ratio=/.test(formatedStr)) {
255
- formatedStr = formatedStr.replace(
256
- `$aspect-ratio=${aspectRatioStr}=aspect-ratio`,
257
- ""
258
- );
286
+ formatedStr = formatedStr.replace(`$aspect-ratio=${aspectRatioStr}=aspect-ratio`, "");
259
287
  }
260
288
  }
261
289
  if (style) {
@@ -273,110 +301,94 @@ function lazyProcess(htmlContent) {
273
301
  const language = this.config.language;
274
302
  const { showAltText, placeholderRatio } = {
275
303
  ...defaultConfig,
276
- ...(themeConfig?.lazyload || {}),
304
+ ...(themeConfig?.lazyload || {})
277
305
  };
278
306
 
279
- const enableGallery = themeConfig?.photoswipe ?? true;
280
- const imgs = readAllImgs(
281
- path.join(this.base_dir, "assets-db/imgs")
282
- );
307
+ const enableGallery = themeConfig?.photoswipe === true;
308
+ const imgs = readAllImgs(path.join(this.base_dir, "assets-db/imgs"));
309
+ let livePhotoThumbIndex = 0;
283
310
 
284
311
  return htmlContent
285
- .replace(
286
- /<img([\s\S]*?)src="(.*?)"([\s\S]*?)(>|<\/img>)/gi,
287
- function (str, p1, src, attrStr) {
288
- if (/no-lazy/gi.test(str)) {
289
- return str.replace(/\$?no-lazy/, "");
290
- }
312
+ .replace(/<img([\s\S]*?)src="(.*?)"([\s\S]*?)(>|<\/img>)/gi, function (str, p1, src, attrStr) {
313
+ if (/no-lazy/gi.test(str)) {
314
+ return str.replace(/\$?no-lazy/, "");
315
+ }
291
316
 
292
- const formatedAttributes = formatAttributes(str, src, imgs, language);
293
- const wrapStyle = formatedAttributes.style;
317
+ const formatedAttributes = formatAttributes(str, src, imgs, language);
318
+ const wrapStyle = formatedAttributes.style;
294
319
 
295
- let placeholerStyle = "";
296
- if (
297
- formatedAttributes.paddingBottom ||
320
+ let placeholerStyle = "";
321
+ if (formatedAttributes.paddingBottom || formatedAttributes.placeholderImage) {
322
+ placeholerStyle = `style="${
323
+ formatedAttributes.paddingBottom
324
+ ? `padding-bottom: ${formatedAttributes.paddingBottom};`
325
+ : ""
326
+ }${
298
327
  formatedAttributes.placeholderImage
299
- ) {
300
- placeholerStyle = `style="${
301
- formatedAttributes.paddingBottom
302
- ? `padding-bottom: ${formatedAttributes.paddingBottom};`
328
+ ? `background-image: ${formatedAttributes.placeholderImage}`
329
+ : ""
330
+ }"`;
331
+ }
332
+
333
+ const { formatedStr, noScriptElementStr } = getformatedStr(str, formatedAttributes);
334
+
335
+ const defaultpswpConfig = {
336
+ width: 1920,
337
+ height: 1920 / placeholderRatio
338
+ };
339
+ const hasSizeConfig =
340
+ formatedAttributes?.originalWidth || formatedAttributes.aspectRatioStr?.split("/")?.[0];
341
+ const pswpWidth =
342
+ formatedAttributes?.originalWidth ??
343
+ (/\//.test(formatedAttributes.aspectRatioStr)
344
+ ? formatedAttributes.aspectRatioStr?.split("/")?.[0]
345
+ : defaultpswpConfig.width);
346
+ const pswpHeight =
347
+ formatedAttributes?.originalHeight ??
348
+ (/\//.test(formatedAttributes.aspectRatioStr)
349
+ ? formatedAttributes.aspectRatioStr?.split("/")?.[1]
350
+ : defaultpswpConfig.height);
351
+ const pswpConfig = enableGallery
352
+ ? `data-pswp-hassize="${(!!hasSizeConfig).toString()}" data-pswp-width="${pswpWidth}" data-pswp-height="${pswpHeight}" ${
353
+ formatedAttributes["pswp-src"]
354
+ ? `data-pswp-src="${formatedAttributes["pswp-src"]}"`
303
355
  : ""
356
+ } data-cropped="true"${
357
+ formatedAttributes.exif ? ` data-pspw-exif="${formatedAttributes.exif}"` : ""
304
358
  }${
305
- formatedAttributes.placeholderImage
306
- ? `background-image: ${formatedAttributes.placeholderImage}`
359
+ formatedAttributes.location
360
+ ? ` data-pspw-location="${formatedAttributes.location}"`
307
361
  : ""
308
- }"`;
309
- }
362
+ }`
363
+ : "";
310
364
 
311
- const { formatedStr, noScriptElementStr } = getformatedStr(
312
- str,
313
- formatedAttributes
314
- );
315
-
316
- const defaultpswpConfig = {
317
- width: 1920,
318
- height: 1920 / placeholderRatio,
319
- };
320
- const hasSizeConfig =
321
- formatedAttributes?.originalWidth ||
322
- formatedAttributes.aspectRatioStr?.split("/")?.[0];
323
- const pswpWidth =
324
- formatedAttributes?.originalWidth ??
325
- (/\//.test(formatedAttributes.aspectRatioStr)
326
- ? formatedAttributes.aspectRatioStr?.split("/")?.[0]
327
- : defaultpswpConfig.width);
328
- const pswpHeight =
329
- formatedAttributes?.originalHeight ??
330
- (/\//.test(formatedAttributes.aspectRatioStr)
331
- ? formatedAttributes.aspectRatioStr?.split("/")?.[1]
332
- : defaultpswpConfig.height);
333
- const pswpConfig = enableGallery
334
- ? `data-pswp-hassize="${(!!hasSizeConfig).toString()}" data-pswp-width="${pswpWidth}" data-pswp-height="${pswpHeight}" ${
335
- formatedAttributes["pswp-src"]
336
- ? `data-pswp-src="${formatedAttributes["pswp-src"]}"`
337
- : ""
338
- } data-cropped="true"${
339
- formatedAttributes.exif
340
- ? ` data-pspw-exif="${formatedAttributes.exif}"`
341
- : ""
342
- }${
343
- formatedAttributes.location
344
- ? ` data-pspw-location="${formatedAttributes.location}"`
345
- : ""
346
- }`
347
- : "";
348
-
349
- const itemTag = formatedAttributes.isLink ? "a" : "span";
350
- const widthStyle = getInlineWidthStyle(wrapStyle || "");
351
-
352
- return `<span class="lazyload-outer-wrap" style="width: 100%;"><${itemTag} class="gallery-item" href="${src}" ${pswpConfig} target="_blank" rel="noopener" ${
353
- theme === "landscape" ? 'data-fancybox="gallery"' : ""
354
- } ${widthStyle}><noscript>${noScriptElementStr}</noscript><span class="lazyload-wrap" data-content="${encodeURIComponent(
355
- formatedStr
356
- )}" ${
357
- wrapStyle ? `style="${wrapStyle}"` : ""
358
- }><span class="placeholder" ${placeholerStyle}></span></span>${
359
- formatedAttributes.caption
360
- ? `<span class="pswp-caption-content">${formatedAttributes.caption}</span>`
361
- : ""
362
- }</${itemTag}>${
363
- showAltText && formatedAttributes.alt
364
- ? `<span class="caption" ${widthStyle}>${formatedAttributes.alt}</span>`
365
- : ``
366
- }</span>`;
367
- }
368
- )
369
- .replace(/<iframe(.*?)src="(.*?)"(.*?)<\/iframe>/gi, function (str) {
365
+ const itemTag = formatedAttributes.isLink ? "a" : "span";
366
+ const widthStyle = getInlineWidthStyle(wrapStyle || "");
367
+
368
+ return `<span class="lazyload-outer-wrap" style="width: 100%;"><${itemTag} class="gallery-item" href="${src}" ${pswpConfig} target="_blank" rel="noopener" ${
369
+ theme === "landscape" ? 'data-fancybox="gallery"' : ""
370
+ } ${widthStyle}><noscript>${noScriptElementStr}</noscript><span class="lazyload-wrap" data-content="${encodeURIComponent(
371
+ formatedStr
372
+ )}" ${
373
+ wrapStyle ? `style="${wrapStyle}"` : ""
374
+ }><span class="placeholder" ${placeholerStyle}></span></span>${
375
+ formatedAttributes.caption
376
+ ? `<span class="pswp-caption-content">${formatedAttributes.caption}</span>`
377
+ : ""
378
+ }</${itemTag}>${
379
+ showAltText && formatedAttributes.alt
380
+ ? `<span class="caption" ${widthStyle}>${formatedAttributes.alt}</span>`
381
+ : ``
382
+ }</span>`;
383
+ })
384
+ .replace(/<iframe(.*?)src="(.*?)"(.*?)<\/iframe>/gi, function (str, p1, src) {
370
385
  if (/no-lazy/gi.test(str)) {
371
386
  return str;
372
387
  }
373
388
  const formatedAttributes = formatAttributes(str);
374
389
  const wrapStyle = formatedAttributes.style;
375
390
  let placeholerStyle = "";
376
- if (
377
- formatedAttributes.paddingBottom ||
378
- formatedAttributes.placeholderImage
379
- ) {
391
+ if (formatedAttributes.paddingBottom || formatedAttributes.placeholderImage) {
380
392
  placeholerStyle = `style="${
381
393
  formatedAttributes.paddingBottom
382
394
  ? `padding-bottom: ${formatedAttributes.paddingBottom};`
@@ -395,48 +407,56 @@ function lazyProcess(htmlContent) {
395
407
  : ""
396
408
  );
397
409
 
398
- return `<span class="lazyload-outer-wrap" style="width: 100%;"><span class="lazyload-wrap" data-content="${encodeURIComponent(
410
+ const livePhotoInfo = enableGallery ? getLivePhotoInfo(src, wrapStyle || "") : null;
411
+ const livePhotoThumbId = livePhotoInfo ? `live-photo-thumb-${livePhotoThumbIndex++}` : "";
412
+ const livePhotoGalleryItem = livePhotoInfo
413
+ ? `<a class="gallery-item live-photo-gallery-item" href="${escapeAttribute(
414
+ livePhotoInfo.picUrl
415
+ )}" data-pswp-width="${livePhotoInfo.width}" data-pswp-height="${
416
+ livePhotoInfo.height
417
+ }" data-cropped="true" data-pswp-thumb-id="${livePhotoThumbId}" data-live-photo-src="${encodeURIComponent(src)}" data-live-photo-pic-url="${escapeAttribute(
418
+ livePhotoInfo.picUrl
419
+ )}" target="_blank" rel="noopener" style="display: none;" aria-hidden="true"></a>`
420
+ : "";
421
+
422
+ return `<span class="lazyload-outer-wrap" style="width: 100%;">${livePhotoGalleryItem}<span class="lazyload-wrap" data-content="${encodeURIComponent(
399
423
  formatedStr
400
- )}" ${wrapStyle ? `style="${wrapStyle}"` : ""}><span class="placeholder" ${placeholerStyle}></span></span><noscript>[This iframe content needs to be loaded by JavaScript.]</noscript></span>`;
424
+ )}" ${livePhotoThumbId ? `id="${livePhotoThumbId}"` : ""} ${
425
+ wrapStyle ? `style="${wrapStyle}"` : ""
426
+ }><span class="placeholder" ${placeholerStyle}></span></span><noscript>[This iframe content needs to be loaded by JavaScript.]</noscript></span>`;
401
427
  })
402
- .replace(
403
- /<video([\s\S]*?)src="(.*?)"([\s\S]*?)(>|<\/video>)/gi,
404
- function (str) {
405
- if (/no-lazy/gi.test(str)) {
406
- return str;
407
- }
408
- const formatedAttributes = formatAttributes(str);
409
- const wrapStyle = formatedAttributes.style;
410
- let placeholerStyle = "";
411
- if (
412
- formatedAttributes.paddingBottom ||
413
- formatedAttributes.placeholderImage
414
- ) {
415
- placeholerStyle = `style="${
416
- formatedAttributes.paddingBottom
417
- ? `padding-bottom: ${formatedAttributes.paddingBottom};`
418
- : ""
419
- }${
420
- formatedAttributes.placeholderImage
421
- ? `background-image: ${formatedAttributes.placeholderImage}`
422
- : ""
423
- }"`;
424
- }
425
- const { formatedStr, noScriptElementStr } = getformatedStr(
426
- str,
427
- formatedAttributes,
428
+ .replace(/<video([\s\S]*?)src="(.*?)"([\s\S]*?)(>|<\/video>)/gi, function (str) {
429
+ if (/no-lazy/gi.test(str)) {
430
+ return str;
431
+ }
432
+ const formatedAttributes = formatAttributes(str);
433
+ const wrapStyle = formatedAttributes.style;
434
+ let placeholerStyle = "";
435
+ if (formatedAttributes.paddingBottom || formatedAttributes.placeholderImage) {
436
+ placeholerStyle = `style="${
437
+ formatedAttributes.paddingBottom
438
+ ? `padding-bottom: ${formatedAttributes.paddingBottom};`
439
+ : ""
440
+ }${
428
441
  formatedAttributes.placeholderImage
429
- ? `style="background-image: ${formatedAttributes.placeholderImage}; background-size: 100% 100%;"`
442
+ ? `background-image: ${formatedAttributes.placeholderImage}`
430
443
  : ""
431
- );
432
-
433
- return `<span class="lazyload-outer-wrap" style="width: 100%;"><span class="lazyload-wrap" data-content="${encodeURIComponent(
434
- formatedStr
435
- )}" ${
436
- wrapStyle ? `style="${wrapStyle}"` : ""
437
- }><span class="placeholder" ${placeholerStyle}></span></span><noscript>${noScriptElementStr}</noscript></span>`;
444
+ }"`;
438
445
  }
439
- );
446
+ const { formatedStr, noScriptElementStr } = getformatedStr(
447
+ str,
448
+ formatedAttributes,
449
+ formatedAttributes.placeholderImage
450
+ ? `style="background-image: ${formatedAttributes.placeholderImage}; background-size: 100% 100%;"`
451
+ : ""
452
+ );
453
+
454
+ return `<span class="lazyload-outer-wrap" style="width: 100%;"><span class="lazyload-wrap" data-content="${encodeURIComponent(
455
+ formatedStr
456
+ )}" ${
457
+ wrapStyle ? `style="${wrapStyle}"` : ""
458
+ }><span class="placeholder" ${placeholerStyle}></span></span><noscript>${noScriptElementStr}</noscript></span>`;
459
+ });
440
460
  }
441
461
 
442
462
  module.exports.processPost = function (data) {
@@ -456,6 +476,25 @@ module.exports.processCovers = function (str, data) {
456
476
  })">`
457
477
  );
458
478
  }
479
+ str = str.replace(
480
+ /<div class="cover-img">\s*(<img\b[^>]*\bsrc="([^"]+)"[^>]*>)\s*<\/div>/gi,
481
+ (coverStr, imgStr, src) => {
482
+ if (/no-lazy/gi.test(imgStr)) {
483
+ return coverStr.replace(/\s+\$?no-lazy\b/i, "");
484
+ }
485
+
486
+ const imgData = imgs.find((item) => isUrlEqual(item.url, src || ""));
487
+ const placeholder = imgData?.placeholder || "";
488
+ const contentImgStr = appendAttributeToTag(imgStr, "data-placeholderimg", placeholder);
489
+ const placeholderStyle = getPlaceholderStyle(placeholder);
490
+
491
+ return `<div class="cover-img"><noscript>${imgStr}</noscript><span class="lazyload-wrap post-list-cover-lazy" data-content="${encodeURIComponent(
492
+ contentImgStr
493
+ )}"><span class="placeholder"${
494
+ placeholderStyle ? ` style="${placeholderStyle}"` : ""
495
+ }></span></span></div>`;
496
+ }
497
+ );
459
498
  } catch (error) {
460
499
  console.log(error);
461
500
  return str;
@@ -2,23 +2,87 @@ hexo.extend.filter.register(
2
2
  "before_post_render",
3
3
  function (data) {
4
4
  try {
5
+ const escapeAttribute = (value = "") =>
6
+ String(value)
7
+ .replace(/&/g, "&amp;")
8
+ .replace(/"/g, "&quot;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;");
11
+
12
+ const getImageAlt = (renderedLine) => {
13
+ const altMatch = renderedLine.match(/<img[\s\S]*?\salt=["']([^"']*)["']/i);
14
+ return altMatch?.[1]?.trim() || "";
15
+ };
16
+
17
+ const getFallbackLabel = (renderedLine, index) => {
18
+ return getImageAlt(renderedLine) || `${index + 1}`;
19
+ };
20
+
21
+ const renderMarkdownLine = (line) =>
22
+ hexo.render
23
+ .renderSync({
24
+ text: line,
25
+ engine: "markdown"
26
+ })
27
+ .replace(/<p>/g, "")
28
+ .replace(/<\/p>/g, "")
29
+ .replace(/<br\s?\>/g, "");
30
+
5
31
  const formatedContent = data.content.replace(
6
32
  /:::image-grid(?:\s+([\w-]+))?([\s\S]*?):::/g,
7
33
  (match, grid, content) => {
8
34
  const gridClass = grid ? ` ${grid}` : "";
9
- return `<span class="image-grid${gridClass}">${content
35
+ const lines = content
10
36
  .split("\n")
11
37
  .filter((v) => v)
38
+ .map((line) => line.trim());
39
+
40
+ if (grid === "switcher") {
41
+ const items = lines
42
+ .map((line, index) => {
43
+ const delimiterIndex = line.indexOf("|");
44
+ const hasLabel = delimiterIndex > -1;
45
+ const label = hasLabel ? line.slice(0, delimiterIndex).trim() : "";
46
+ const imageMarkdown = hasLabel ? line.slice(delimiterIndex + 1).trim() : line;
47
+ const renderedLine = renderMarkdownLine(imageMarkdown);
48
+ const alt = getImageAlt(renderedLine);
49
+
50
+ return /<img|<iframe/.test(renderedLine)
51
+ ? {
52
+ label: alt || label || getFallbackLabel(renderedLine, index),
53
+ content: renderedLine
54
+ }
55
+ : null;
56
+ })
57
+ .filter(Boolean);
58
+
59
+ if (!items.length) return match;
60
+
61
+ return `<span class="image-grid switcher">${items
62
+ .map(
63
+ (item, index) =>
64
+ `<span class="grid-item${index === 0 ? " active" : ""}" data-switch-label="${escapeAttribute(
65
+ item.label
66
+ )}" role="tabpanel" aria-hidden="${index === 0 ? "false" : "true"}">${item.content}</span>`
67
+ )
68
+ .join(
69
+ ""
70
+ )}<span class="image-grid-switcher-controls is-ready" role="tablist" style="--switcher-item-count: ${
71
+ items.length
72
+ }; --switcher-active-index: 0;">${items
73
+ .map(
74
+ (item, index) =>
75
+ `<button class="image-grid-switcher-button${index === 0 ? " active" : ""}" type="button" role="tab" aria-selected="${
76
+ index === 0 ? "true" : "false"
77
+ }">${escapeAttribute(item.label)}</button>`
78
+ )
79
+ .join("")}</span></span>`;
80
+ }
81
+
82
+ return `<span class="image-grid${gridClass}">${lines
12
83
  .map((line) => {
13
- const renderedLine = hexo.render
14
- .renderSync({
15
- text: line,
16
- engine: "markdown",
17
- })
18
- .replace(/<p>/g, "")
19
- .replace(/<\/p>/g, "")
20
- .replace(/<br\s?\>/g, "");
21
- return /<img/.test(renderedLine)
84
+ const renderedLine = renderMarkdownLine(line);
85
+ return /<img|<iframe/.test(renderedLine)
22
86
  ? `<span class="grid-item">${renderedLine}</span>`
23
87
  : renderedLine;
24
88
  })