Skip to content

Commit a1a43f3

Browse files
committed
给灯箱换了个更好的库
1 parent a307725 commit a1a43f3

5 files changed

Lines changed: 222 additions & 9 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
(function () {
2+
3+
const IC_CLOSE = '<svg class="pswp__icn" viewBox="0 0 24 24" aria-hidden="true"><path fill="var(--pswp-icon-color)" d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>';
4+
const IC_ZOOM = '<svg class="pswp__icn" viewBox="-1.333 -1.533 26.667 26.667" aria-hidden="true"><path fill="var(--pswp-icon-color)" d="m15.5 14 5 5-1.5 1.5-5-5v-.79l-.27-.28A6.47 6.47 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3 6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.57 4.23l.28.27zm-6 0C12 14 14 12 14 9.5S12 5 9.5 5 5 7 5 9.5 7 14 9.5 14m2.5-4h-2v2H9v-2H7V9h2V7h1v2h2z"/></svg>';
5+
const IC_ARROW = '<svg class="pswp__icn" viewBox="-159.058 -159.158 795.292 795.292" aria-hidden="true"><path fill="var(--pswp-icon-color)" d="M145.188 238.575 360.688 23.075c5.3-5.3 5.3-13.8 0-19.1s-13.8-5.3-19.1 0l-225.1 225.1c-5.3 5.3-5.3 13.8 0 19.1l225.1 225c2.6 2.6 6.1 4 9.5 4s6.9-1.3 9.5-4c5.3-5.3 5.3-13.8 0-19.1z"/></svg>';
6+
7+
const isVisible = (image, style = getComputedStyle(image)) =>
8+
style.display !== "none" && style.visibility !== "hidden" && image.getClientRects().length > 0;
9+
const imageSize = (
10+
image,
11+
width = Number(image.dataset.pswpWidth) || image.naturalWidth || Math.round(image.getBoundingClientRect().width),
12+
height = Number(image.dataset.pswpHeight) || image.naturalHeight || Math.round(image.getBoundingClientRect().height)
13+
) => ({
14+
width: width > 0 ? width : 1200,
15+
height: height > 0 ? height : 900
16+
});
17+
const createItem = (
18+
image,
19+
size = imageSize(image)
20+
) => ({
21+
src: image.dataset.pswpSrc,
22+
msrc: image.currentSrc || image.src,
23+
width: size.width,
24+
height: size.height,
25+
alt: image.alt || "",
26+
element: image
27+
});
28+
const waitForImage = image => image.complete
29+
? Promise.resolve()
30+
: new Promise(resolve => {
31+
image.addEventListener("load", resolve, { once: true });
32+
image.addEventListener("error", resolve, { once: true });
33+
})
34+
35+
function mountPhotoSwipe() {
36+
if (!window.PhotoSwipe) return;
37+
38+
const images = Array.from(document.querySelectorAll(".md-content article img[data-pswp-src]"));
39+
images.forEach(image => {
40+
if (image.dataset.pswpBound) return;
41+
42+
image.dataset.pswpBound = "true";
43+
image.addEventListener("click", async event => {
44+
event.preventDefault();
45+
await waitForImage(image);
46+
47+
const visibleImages = images.filter(it => isVisible(it));
48+
const items = visibleImages.map(it => createItem(it));
49+
const index = visibleImages.indexOf(image);
50+
new window.PhotoSwipe({
51+
dataSource: items,
52+
index: index >= 0 ? index : 0,
53+
showHideAnimationType: "zoom",
54+
closeSVG: IC_CLOSE,
55+
zoomSVG: IC_ZOOM,
56+
arrowPrevSVG: IC_ARROW,
57+
arrowNextSVG: IC_ARROW
58+
}).init();
59+
});
60+
});
61+
}
62+
63+
if (window.document$ && typeof window.document$.subscribe === "function") {
64+
window.document$.subscribe(mountPhotoSwipe);
65+
} else {
66+
document.addEventListener("DOMContentLoaded", mountPhotoSwipe);
67+
}
68+
69+
})();

docs/assets/stylesheets/glightbox.css

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/posts/.meta.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
license: 'CC BY-NC-SA 3.0'
2-
glightbox: true

hooks/photoswipe.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import posixpath
2+
from html import escape
3+
from html.parser import HTMLParser
4+
from pathlib import Path
5+
from urllib.parse import urlparse
6+
7+
from PIL import Image
8+
9+
IMAGE_EXTENSIONS = (".apng", ".avif", ".gif", ".jpg", ".jpeg", ".png", ".webp")
10+
SKIP_IMAGE_CLASSES = {"twemoji", "md-author", "md-post__cover"}
11+
12+
def is_supported_image_url(url: str) -> bool:
13+
parsed = urlparse(url.strip())
14+
if is_remote_url(url):
15+
return True
16+
if parsed.scheme:
17+
return False
18+
return parsed.path.lower().endswith(IMAGE_EXTENSIONS)
19+
20+
def is_remote_url(url: str) -> bool:
21+
parsed = urlparse(url.strip())
22+
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
23+
24+
def resolve_rendered_path(src: str, page_url: str) -> str:
25+
parsed = urlparse(src.strip())
26+
if parsed.path.startswith("/"):
27+
return parsed.path.lstrip("/")
28+
29+
base = page_url if page_url.endswith("/") else posixpath.dirname(page_url)
30+
return posixpath.normpath(posixpath.join(base, parsed.path))
31+
32+
class PhotoSwipeTransformer(HTMLParser):
33+
def __init__(self, page_url: str, image_info: dict[str, tuple[int, int, str]]):
34+
super().__init__(convert_charrefs=False)
35+
self.page_url = page_url
36+
self.image_info = image_info
37+
self.parts = []
38+
self.anchor_depth = 0
39+
40+
def handle_starttag(self, tag, attrs):
41+
if tag == "a":
42+
self.anchor_depth += 1
43+
44+
if tag != "img" or self.anchor_depth:
45+
self.parts.append(self.get_starttag_text())
46+
return
47+
48+
attrs_dict = dict(attrs)
49+
src = attrs_dict.get("src", "")
50+
classes = set(attrs_dict.get("class", "").split())
51+
if not src or classes & SKIP_IMAGE_CLASSES or not is_supported_image_url(src):
52+
self.parts.append(self.get_starttag_text())
53+
return
54+
55+
attrs_dict["data-pswp-src"] = src
56+
attrs_dict["data-pswp-gallery"] = "content"
57+
58+
if not is_remote_url(src):
59+
rendered_path = resolve_rendered_path(src, self.page_url)
60+
if rendered_path not in self.image_info:
61+
self.parts.append(self.get_starttag_text())
62+
return
63+
64+
width, height, _ = self.image_info[rendered_path]
65+
attrs_dict["data-pswp-width"] = str(width)
66+
attrs_dict["data-pswp-height"] = str(height)
67+
68+
classes = attrs_dict.get("class", "").split()
69+
if "pswp-image" not in classes:
70+
classes.append("pswp-image")
71+
attrs_dict["class"] = " ".join(classes).strip()
72+
self.parts.append(render_starttag(tag, attrs_dict, self.get_starttag_text()))
73+
74+
def handle_startendtag(self, tag, attrs):
75+
self.handle_starttag(tag, attrs)
76+
77+
def handle_endtag(self, tag):
78+
if tag == "a" and self.anchor_depth:
79+
self.anchor_depth -= 1
80+
self.parts.append(f"</{tag}>")
81+
82+
def handle_data(self, data):
83+
self.parts.append(data)
84+
85+
def handle_entityref(self, name):
86+
self.parts.append(f"&{name};")
87+
88+
def handle_charref(self, name):
89+
self.parts.append(f"&#{name};")
90+
91+
def handle_comment(self, data):
92+
self.parts.append(f"<!--{data}-->")
93+
94+
def handle_decl(self, decl):
95+
self.parts.append(f"<!{decl}>")
96+
97+
def output(self) -> str:
98+
return "".join(self.parts)
99+
100+
def transform_html(html: str, page_url: str, image_info: dict[str, tuple[int, int, str]]) -> str:
101+
transformer = PhotoSwipeTransformer(page_url, image_info)
102+
transformer.feed(html)
103+
transformer.close()
104+
return transformer.output()
105+
106+
def render_starttag(tag: str, attrs: dict[str, str], original: str) -> str:
107+
suffix = " /" if original.rstrip().endswith("/>") else ""
108+
rendered = " ".join(
109+
f'{name}="{escape(value, quote=True)}"'
110+
for name, value in attrs.items()
111+
if value is not None
112+
)
113+
if rendered:
114+
return f"<{tag} {rendered}{suffix}>"
115+
return f"<{tag}{suffix}>"
116+
117+
def build_image_info(files, config) -> dict[str, tuple[int, int, str]]:
118+
docs_dir = Path(config["docs_dir"])
119+
info = {}
120+
for file in files.media_files():
121+
if not file.src_uri.lower().endswith(IMAGE_EXTENSIONS):
122+
continue
123+
124+
path = docs_dir / file.src_path
125+
if not path.is_file():
126+
continue
127+
128+
try:
129+
with Image.open(path) as image:
130+
size = (*image.size, file.url)
131+
info[file.src_uri] = size
132+
info[file.url.lstrip("/")] = size
133+
except Exception:
134+
continue
135+
136+
return info
137+
138+
def is_blog_post_page(page) -> bool:
139+
return bool(page and page.meta and page.meta.get("template") == "blog-post.html")
140+
141+
def on_page_content(html, page, config, files, **kwargs):
142+
if not is_blog_post_page(page):
143+
return html
144+
145+
return transform_html(html, page.url, build_image_info(files, config))

mkdocs.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,12 @@ plugins:
105105
cache_safe: true
106106
minify_css: true
107107
css_files:
108+
- assets/stylesheets/article.css
108109
- assets/stylesheets/md-banner.css
109-
- assets/stylesheets/md-search.css
110+
- assets/stylesheets/md-nav.css
110111
- assets/stylesheets/md-post.css
111-
- glightbox:
112-
effect: fade
113-
manual: true
112+
- assets/stylesheets/md-search.css
113+
- assets/stylesheets/md-typeset.css
114114

115115
# noinspection YAMLSchemaValidation, SpellCheckingInspection
116116
markdown_extensions:
@@ -165,6 +165,7 @@ hooks:
165165
- hooks/copyright_footer.py
166166
- hooks/filters.py
167167
- hooks/instant_preview.py
168+
- hooks/photoswipe.py
168169
- hooks/post_cover.py
169170
- hooks/random_posts.py
170171
- hooks/rss_dates.py
@@ -174,10 +175,12 @@ extra_javascript:
174175
- https://unpkg.zhimg.com/mathjax@3.2.2/es5/tex-mml-chtml.js
175176
- https://cdn.bootcdn.net/ajax/libs/tablesort/5.2.1/tablesort.min.js
176177
- assets/javascripts/tablesort.js
178+
- https://cdn.bootcdn.net/ajax/libs/photoswipe/5.4.4/umd/photoswipe.umd.min.js
179+
- assets/javascripts/photoswipe.js
177180

178181
extra_css:
182+
- https://cdn.bootcdn.net/ajax/libs/photoswipe/5.4.4/photoswipe.min.css
179183
- assets/stylesheets/article.css
180-
- assets/stylesheets/glightbox.css
181184
- assets/stylesheets/md-banner.css
182185
- assets/stylesheets/md-nav.css
183186
- assets/stylesheets/md-post.css

0 commit comments

Comments
 (0)