feat: 加入 web-to-pixso 插件源码
Chrome 扩展,用于从网页提取 DOM 树为 JSON 格式。 核心文件:capture.js(76KB,提取逻辑)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
function freezeAnimations() {
|
||||
const style = document.createElement("style");
|
||||
style.id = "__web-to-pixso-freeze";
|
||||
style.textContent = `
|
||||
*, *::before, *::after {
|
||||
animation-play-state: paused !important;
|
||||
transition-duration: 0s !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.slick-track,
|
||||
.swiper-wrapper {
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
`;
|
||||
document.documentElement.appendChild(style);
|
||||
|
||||
for (const media of document.querySelectorAll("video, audio")) {
|
||||
try {
|
||||
media.pause();
|
||||
} catch {
|
||||
// Ignore media elements that cannot be controlled by content scripts.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stabilizeCarousels() {
|
||||
try {
|
||||
if (window.jQuery) {
|
||||
window.jQuery(".slick-slider").each((_, element) => {
|
||||
try {
|
||||
window.jQuery(element).slick("slickPause");
|
||||
window.jQuery(element).slick("slickGoTo", 0, true);
|
||||
} catch {
|
||||
// Some pages expose slick classes without the jQuery plugin instance.
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Best effort only.
|
||||
}
|
||||
|
||||
for (const element of document.querySelectorAll(".swiper, .swiper-container")) {
|
||||
try {
|
||||
if (element.swiper) {
|
||||
element.swiper.autoplay?.stop?.();
|
||||
element.swiper.slideToLoop?.(0, 0, false);
|
||||
element.swiper.slideTo?.(0, 0, false);
|
||||
element.swiper.update?.();
|
||||
}
|
||||
} catch {
|
||||
// Keep DOM capture running even if a carousel API rejects.
|
||||
}
|
||||
}
|
||||
|
||||
for (const wrapper of document.querySelectorAll(".swiper-wrapper, .slick-track")) {
|
||||
wrapper.style.transitionDuration = "0s";
|
||||
wrapper.style.animationPlayState = "paused";
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForStableTopLayer(timeout = 2500) {
|
||||
const start = Date.now();
|
||||
const selector = [
|
||||
"header",
|
||||
"nav",
|
||||
"[role='navigation']",
|
||||
"[class*='header' i]",
|
||||
"[class*='nav' i]",
|
||||
"[class*='top' i]"
|
||||
].join(",");
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
const candidates = Array.from(document.querySelectorAll(selector));
|
||||
const hasVisibleCandidate = candidates.some(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
return rect.width > 20 &&
|
||||
rect.height > 10 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.top < Math.max(160, window.innerHeight * 0.2) &&
|
||||
style.display !== "none" &&
|
||||
style.visibility !== "hidden" &&
|
||||
Number(style.opacity || 1) > 0;
|
||||
});
|
||||
if (hasVisibleCandidate || document.readyState === "complete") {
|
||||
await delay(300);
|
||||
return;
|
||||
}
|
||||
await delay(120);
|
||||
}
|
||||
}
|
||||
|
||||
async function scrollToLoadLazyContent() {
|
||||
const maxScroll = Math.max(
|
||||
document.documentElement.scrollHeight,
|
||||
document.body.scrollHeight
|
||||
) - window.innerHeight;
|
||||
|
||||
if (maxScroll <= 0) return;
|
||||
|
||||
const step = Math.max(480, Math.floor(window.innerHeight * 0.8));
|
||||
for (let y = 0; y <= maxScroll; y += step) {
|
||||
window.scrollTo(0, Math.min(y, maxScroll));
|
||||
await delay(160);
|
||||
}
|
||||
|
||||
window.scrollTo(0, maxScroll);
|
||||
await delay(220);
|
||||
window.scrollTo(0, 0);
|
||||
await delay(220);
|
||||
}
|
||||
|
||||
async function waitForImages(timeout = 3500) {
|
||||
const images = Array.from(document.images || []);
|
||||
await Promise.race([
|
||||
Promise.allSettled(images.map(image => {
|
||||
if (image.complete) return Promise.resolve();
|
||||
return new Promise(resolve => {
|
||||
image.addEventListener("load", resolve, { once: true });
|
||||
image.addEventListener("error", resolve, { once: true });
|
||||
});
|
||||
})),
|
||||
delay(timeout)
|
||||
]);
|
||||
}
|
||||
|
||||
async function waitForFonts(timeout = 3000) {
|
||||
if (!document.fonts?.ready) return;
|
||||
await Promise.race([document.fonts.ready, delay(timeout)]);
|
||||
}
|
||||
|
||||
window.__webToPixsoRunCapture = async function runCapture(options = {}) {
|
||||
if (!window.__webToPixsoCapture) {
|
||||
throw new Error("采集引擎未加载");
|
||||
}
|
||||
|
||||
freezeAnimations();
|
||||
stabilizeCarousels();
|
||||
await waitForStableTopLayer();
|
||||
stabilizeCarousels();
|
||||
await scrollToLoadLazyContent();
|
||||
stabilizeCarousels();
|
||||
await waitForImages();
|
||||
await waitForFonts();
|
||||
stabilizeCarousels();
|
||||
await delay(200);
|
||||
|
||||
return window.__webToPixsoCapture({
|
||||
useProxy: Boolean(options.useProxy),
|
||||
concurrency: options.concurrency || "8",
|
||||
captureMode: options.captureMode === "editable" ? "editable" : "mixed",
|
||||
captureWidth: options.captureWidth,
|
||||
selectionId: options.selectionId,
|
||||
selectionWidth: options.selectionWidth
|
||||
});
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user