Files
KSP_project/scripts/normalize_mission_galleries.py

325 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Apply the shared gallery presentation and lightbox behavior to mission galleries."""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
WIKI_DIR = ROOT / "data" / "wiki"
GALLERY_FILES = sorted(WIKI_DIR.glob("guanghan_*_gallery_zh.html"))
STYLE_END_RE = re.compile(r"</style>", re.IGNORECASE)
SCRIPT_RE = re.compile(r"\s*<script>.*?</script>\s*", re.IGNORECASE | re.DOTALL)
ASSET_IMAGE_RE = re.compile(r"<img\b(?=[^>]*\bdata-wiki-asset=)[^>]*>", re.IGNORECASE)
CSS_BLOCK_RE = re.compile(
r"\n?/\* mission-gallery-enhancements:start \*/.*?"
r"/\* mission-gallery-enhancements:end \*/\n?",
re.DOTALL,
)
LEGACY_LIGHTBOX_CSS_RE = re.compile(
r"\n?\.wiki-article img\[data-wiki-asset\][^\n]*\n"
r".*?@media \(max-width: 820px\) \{ \.wiki-image-lightbox \{.*?\}\s*\}\n?",
re.DOTALL,
)
GALLERY_CSS = r"""
/* mission-gallery-enhancements:start */
.wiki-article .gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
align-items: stretch;
}
.wiki-article .gallery-item {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
content-visibility: auto;
contain-intrinsic-size: 340px 300px;
}
.wiki-article .gallery-item img[data-wiki-asset] {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: contain;
background: #202122;
cursor: zoom-in;
}
.wiki-article .gallery-item .thumbcaption {
padding: 8px 7px 7px;
}
.wiki-article .gallery-notice {
margin: 1em 0 1.25em;
padding: 10px 12px;
border-left: 4px solid #36c;
background: #f1f4fd;
color: #202122;
}
.wiki-article .image-placeholder {
position: relative;
border: 1px dashed #72777d;
background: linear-gradient(135deg, #f8f9fa, #eaecf0);
color: #54595d;
}
.wiki-article.gallery-planning .gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(min(100%, 360px), 1fr));
gap: 12px;
}
.wiki-article.gallery-planning .gallery-item {
display: block;
content-visibility: visible;
contain-intrinsic-size: none;
border-color: #a2a9b1;
background: #fff;
box-shadow: 0 1px 2px rgba(32, 33, 34, 0.08);
}
.wiki-article.gallery-planning .image-placeholder {
min-height: 0 !important;
display: block;
padding: 14px 14px 12px;
border: 0;
border-left: 4px solid #72777d;
background: #f8f9fa;
color: #202122;
text-align: left;
font-size: 13px;
font-weight: 600;
line-height: 1.65;
}
.wiki-article.gallery-planning .gallery-item .thumbcaption {
padding: 10px 14px 12px;
border-top: 1px solid #eaecf0;
color: #54595d;
}
.wiki-article.gallery-planning > .image-placeholder {
min-height: 0;
padding: 22px 18px;
border-left: 4px solid #72777d;
}
.wiki-image-lightbox {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(32, 33, 34, 0.88);
z-index: 10000;
box-sizing: border-box;
}
.wiki-image-lightbox[hidden] { display: none; }
.wiki-image-lightbox-panel {
position: relative;
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 44px;
grid-template-areas: "close close close" "prev image next" "counter caption caption";
gap: 8px;
width: min(1320px, 96vw);
max-height: 94vh;
overflow: auto;
margin: 0;
padding: 10px;
border: 1px solid #a2a9b1;
background: #fff;
box-shadow: 0 12px 38px rgba(0, 0, 0, 0.35);
box-sizing: border-box;
}
.wiki-image-lightbox-panel > img {
grid-area: image;
width: 100%;
height: min(78vh, 900px);
object-fit: contain;
background: #000;
}
.wiki-image-lightbox-close {
grid-area: close;
justify-self: end;
position: static;
float: none;
width: 34px;
height: 34px;
margin: 0;
border: 1px solid #a2a9b1;
border-radius: 3px;
background: #eaecf0;
color: #202122;
font-size: 22px;
line-height: 30px;
cursor: pointer;
}
.wiki-image-lightbox-close:hover,
.wiki-image-lightbox-close:focus { background: #fff; }
.wiki-image-lightbox-prev { grid-area: prev; }
.wiki-image-lightbox-next { grid-area: next; }
.wiki-image-lightbox-nav {
align-self: center;
width: 40px;
height: 56px;
border: 1px solid #a2a9b1;
border-radius: 3px;
background: #eaecf0;
color: #202122;
font-size: 26px;
cursor: pointer;
}
.wiki-image-lightbox-nav:hover,
.wiki-image-lightbox-nav:focus { background: #fff; }
.wiki-image-lightbox-counter { grid-area: counter; color: #54595d; white-space: nowrap; }
.wiki-image-lightbox-panel .thumbcaption { grid-area: caption; padding: 0 4px 3px; }
body.wiki-image-lightbox-open { overflow: hidden; }
@media (max-width: 820px) {
.wiki-article .gallery-grid { grid-template-columns: 1fr; }
.wiki-image-lightbox-panel {
grid-template-columns: 36px minmax(0, 1fr) 36px;
width: 98vw;
}
.wiki-image-lightbox-panel > img { height: 70vh; }
.wiki-image-lightbox-nav { width: 34px; height: 48px; }
}
/* mission-gallery-enhancements:end */
""".strip()
GALLERY_JS = r"""<script>
(function () {
function ready(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
} else {
fn();
}
}
function captionFor(image) {
var container = image.closest('.gallery-item, figure, td, tr, .infobox-image');
var caption = container && container.querySelector('.thumbcaption');
return caption ? caption.textContent.trim() : (image.alt || '');
}
function install(article) {
if (!article || article.dataset.galleryLightboxReady === '1') { return; }
var images = Array.prototype.slice.call(article.querySelectorAll('img[data-wiki-asset]'));
if (!images.length) { return; }
article.dataset.galleryLightboxReady = '1';
var overlay = document.createElement('div');
overlay.className = 'wiki-image-lightbox';
overlay.hidden = true;
overlay.innerHTML = '<figure class="wiki-image-lightbox-panel" role="dialog" aria-modal="true" aria-label="任务图集图片预览">' +
'<button class="wiki-image-lightbox-close" type="button" aria-label="关闭图片预览">×</button>' +
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-prev" type="button" aria-label="上一张"></button>' +
'<img alt="">' +
'<button class="wiki-image-lightbox-nav wiki-image-lightbox-next" type="button" aria-label="下一张"></button>' +
'<span class="wiki-image-lightbox-counter" aria-live="polite"></span>' +
'<figcaption class="thumbcaption"></figcaption></figure>';
document.body.appendChild(overlay);
var previewImage = overlay.querySelector('img');
var previewCaption = overlay.querySelector('figcaption');
var counter = overlay.querySelector('.wiki-image-lightbox-counter');
var closeButton = overlay.querySelector('.wiki-image-lightbox-close');
var previousButton = overlay.querySelector('.wiki-image-lightbox-prev');
var nextButton = overlay.querySelector('.wiki-image-lightbox-next');
var activeIndex = 0;
var lastTrigger = null;
function render(index) {
activeIndex = (index + images.length) % images.length;
var image = images[activeIndex];
previewImage.src = image.dataset.wikiAsset || image.currentSrc || image.src;
previewImage.alt = image.alt || '';
previewCaption.textContent = captionFor(image);
counter.textContent = (activeIndex + 1) + ' / ' + images.length;
}
function closePreview() {
overlay.hidden = true;
previewImage.removeAttribute('src');
document.body.classList.remove('wiki-image-lightbox-open');
if (lastTrigger) { lastTrigger.focus(); }
}
function openPreview(image, event) {
if (event) { event.preventDefault(); event.stopPropagation(); }
lastTrigger = image;
render(images.indexOf(image));
overlay.hidden = false;
document.body.classList.add('wiki-image-lightbox-open');
closeButton.focus();
}
images.forEach(function (image) {
image.tabIndex = 0;
image.setAttribute('role', 'button');
image.setAttribute('aria-label', '打开图片预览:' + (image.alt || '任务影像'));
image.addEventListener('click', function (event) { openPreview(image, event); });
image.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') { openPreview(image, event); }
});
});
previousButton.hidden = images.length < 2;
nextButton.hidden = images.length < 2;
previousButton.addEventListener('click', function () { render(activeIndex - 1); });
nextButton.addEventListener('click', function () { render(activeIndex + 1); });
closeButton.addEventListener('click', closePreview);
overlay.addEventListener('click', function (event) {
if (event.target === overlay) { closePreview(); }
});
document.addEventListener('keydown', function (event) {
if (overlay.hidden) { return; }
if (event.key === 'Escape') { closePreview(); }
if (event.key === 'ArrowLeft') { render(activeIndex - 1); }
if (event.key === 'ArrowRight') { render(activeIndex + 1); }
});
}
ready(function () {
Array.prototype.forEach.call(document.querySelectorAll('.wiki-article'), install);
});
}());
</script>"""
def add_image_loading_attributes(match: re.Match[str]) -> str:
tag = match.group(0)
additions = []
if not re.search(r"\bloading=", tag, re.IGNORECASE):
additions.append('loading="lazy"')
if not re.search(r"\bdecoding=", tag, re.IGNORECASE):
additions.append('decoding="async"')
if not re.search(r"\bfetchpriority=", tag, re.IGNORECASE):
additions.append('fetchpriority="low"')
if not additions:
return tag
return tag[:-1].rstrip() + " " + " ".join(additions) + ">"
def normalize(path: Path) -> bool:
original = path.read_text(encoding="utf-8")
updated = LEGACY_LIGHTBOX_CSS_RE.sub("\n", original)
updated = CSS_BLOCK_RE.sub("\n", updated)
updated = STYLE_END_RE.sub(GALLERY_CSS + "\n</style>", updated, count=1)
updated = ASSET_IMAGE_RE.sub(add_image_loading_attributes, updated)
updated = SCRIPT_RE.sub("\n", updated).rstrip()
updated += "\n"
if updated == original:
return False
path.write_text(updated, encoding="utf-8", newline="\n")
return True
def main() -> int:
changed = 0
for path in GALLERY_FILES:
was_changed = normalize(path)
changed += int(was_changed)
print(f"gallery_normalized file={path.name} changed={str(was_changed).lower()}")
print(f"gallery_normalized_summary files={len(GALLERY_FILES)} changed={changed}")
return 0
if __name__ == "__main__":
raise SystemExit(main())