Files
wehub-resource-sync f4ed7901c3
Module Market Check / Validate modules/ (push) Failing after 1s
Android CI Build / check (push) Failing after 1s
Module Market Publish / Generate submissions.json (push) Failing after 1s
Android CI Build / package-apks (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:29:44 +08:00

168 lines
6.1 KiB
JavaScript
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.
// Reading Mode — official WebToApp sample.
//
// Demonstrates the full module surface in one place:
// - Reads multiple typed configItems via getConfig()
// - Heuristic article extraction (no external libs)
// - Heavy DOM restructuring with safe rollback
// - Registers a panel button via __WTA_MODULE_UI__ so users can summon it
// - Persists state across SPA navigations using sessionStorage
//
// The implementation is intentionally simple — under 200 lines — so it
// reads like a tutorial. For production use, wire in a real extraction
// library such as Mozilla's @mozilla/readability.
(function () {
var STATE_KEY = 'wta-reading-mode-active';
var ROOT_CLASS = 'wta-reading-mode-root';
var ARTICLE_ID = 'wta-reading-mode-article';
function getOpts() {
return {
theme: getConfig('theme', 'light'),
fontSize: getConfig('fontSize', 'medium'),
fontFamily: getConfig('fontFamily', 'serif'),
maxWidth: parseInt(getConfig('maxWidth', '720'), 10) || 720,
autoEnter: String(getConfig('autoEnter', 'false')) === 'true',
};
}
// ── content extraction ────────────────────────────────────────────
// Score every block element by text density × tag weight, pick the best.
// Not as accurate as Readability.js but good enough for the common case
// and trivial to read.
function findArticle() {
var candidates = document.querySelectorAll('article, main, [role=main], section, div');
var best = null;
var bestScore = 0;
var AD_PATTERNS = /ad|advert|promo|sidebar|footer|header|nav|menu|cookie|consent|popup|modal|social|share|comment|related|recommend/i;
for (var i = 0; i < candidates.length; i++) {
var el = candidates[i];
if (el === document.body || el === document.documentElement) continue;
var text = (el.textContent || '').trim();
var len = text.length;
if (len < 200) continue;
var paragraphs = el.querySelectorAll('p').length;
if (paragraphs < 2) continue;
var tagBoost = 1;
var name = el.tagName.toLowerCase();
if (name === 'article') tagBoost = 4;
else if (name === 'main') tagBoost = 3;
else if (el.getAttribute('role') === 'main') tagBoost = 3;
var links = el.querySelectorAll('a').length;
var linkDensity = links / Math.max(1, paragraphs);
var penalty = linkDensity > 3 ? 0.3 : 1;
var cls = (el.className || '') + ' ' + (el.id || '');
if (AD_PATTERNS.test(cls)) penalty *= 0.2;
var commLinks = el.querySelectorAll('a[href*="comment"], a[href*="share"], a[href*="social"]').length;
if (commLinks > 3) penalty *= 0.5;
var score = len * tagBoost * penalty;
if (score > bestScore) {
bestScore = score;
best = el;
}
}
return best;
}
function wordCount(el) {
return (el.textContent || '').split(/\s+/).filter(Boolean).length;
}
// ── enter / exit ──────────────────────────────────────────────────
function enter() {
if (document.documentElement.classList.contains(ROOT_CLASS)) return;
var article = findArticle();
if (!article) {
console.warn('[Reading Mode] no readable article detected');
return;
}
var opts = getOpts();
var html = article.innerHTML;
var title = (document.title || '').trim();
var words = (article.textContent || '').trim().split(/\s+/).filter(Boolean).length;
var readMin = Math.max(1, Math.round(words / 200));
var readLabel = readMin + ' min read';
var shell = document.createElement('div');
shell.id = ARTICLE_ID;
shell.dataset.theme = opts.theme;
shell.dataset.fontSize = String(opts.fontSize);
shell.dataset.fontFamily = opts.fontFamily;
shell.style.maxWidth = opts.maxWidth + 'px';
shell.innerHTML =
'<header class="wta-rm-header"><h1>' +
escapeHtml(title) +
'</h1><span class="wta-rm-meta">' + escapeHtml(readLabel) + ' \u00b7 ' + words + ' words</span></header>' +
'<div class="wta-rm-body">' +
html +
'</div>';
document.documentElement.classList.add(ROOT_CLASS);
// Stash the original <body> instead of replacing it — leaving the
// node tree intact lets the page's own SPA router keep working.
document.body.dataset.wtaRmCovered = '1';
document.body.appendChild(shell);
sessionStorage.setItem(STATE_KEY, '1');
}
function exit() {
var shell = document.getElementById(ARTICLE_ID);
if (shell && shell.parentNode) shell.parentNode.removeChild(shell);
document.documentElement.classList.remove(ROOT_CLASS);
delete document.body.dataset.wtaRmCovered;
sessionStorage.removeItem(STATE_KEY);
}
function toggle() {
if (sessionStorage.getItem(STATE_KEY) === '1') exit();
else enter();
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// ── panel button registration ─────────────────────────────────────
// Lets users summon Reading Mode from WebToApp's floating panel.
// The runtime handles the visual button — we just hand it a callback.
if (typeof __WTA_MODULE_UI__ !== 'undefined' && __WTA_MODULE_UI__.register) {
__WTA_MODULE_UI__.register({
id: __MODULE_INFO__.id,
name: __MODULE_INFO__.name,
icon: __MODULE_INFO__.icon,
uiConfig: __MODULE_UI_CONFIG__,
runMode: __MODULE_RUN_MODE__,
onClick: toggle,
});
}
// ── auto-enter ────────────────────────────────────────────────────
// Restore reader if user navigated within the same SPA session.
if (sessionStorage.getItem(STATE_KEY) === '1') {
enter();
return;
}
// Auto-enter on long-form pages when the option is on.
if (getOpts().autoEnter) {
var article = findArticle();
if (article && wordCount(article) >= 800) enter();
}
})();