您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
自动展开全文的 beta 版,大概永远不会正式。使用前请务必认真阅读发布页说明,安装即表示知悉、理解并同意说明中全部内容。默认不开启任何功能,请在脚本菜单中切换功能(设置只针对当前网站)。
当前为
// ==UserScript== // @name 自动展开全文(永久 beta+ 版) // @namespace Expand the article for vip. // @match *://*/* // @grant GM_info // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_openInTab // @run-at document-end // @version 0.1.0 // @supportURL https://docs.qq.com/form/page/DYVFEd3ZaQm5pZ1ZR // @homepageURL https://script.izyx.xyz/expand-the-article/ // @icon https://i.v2ex.co/b39y298il.png // @inject-into content // @noframes // @author 稻米鼠 // @created 2020-07-24 07:04:35 // @updated 2020-08-19 17:40:56 // @description 自动展开全文的 beta 版,大概永远不会正式。使用前请务必认真阅读发布页说明,安装即表示知悉、理解并同意说明中全部内容。默认不开启任何功能,请在脚本菜单中切换功能(设置只针对当前网站)。 // ==/UserScript== (function(){ // 闭包 Start /* ====== 弹出提示 ====== */ if(!GM_getValue('notice_mark') || GM_getValue('notice_mark')!== '0.1.0'){ if(confirm(`【自动展开全文 beta+】用法变更提示: 1、脚本设置里自定义的匹配规则可以删了。不知道怎么删除再重装本脚本好了; 2、默认适配所有网站,但不启用任何功能,就是默认啥都不会做滴~; 3、在需要展开的网站下,通过脚本菜单开启相应功能。(悄悄告诉你,再点一下可以关闭哦~; 4、由于一些原因,切换设置后页面会刷新。应该没人频繁切换吧,所以问题不大; 5、希望你认真读懂后再点击确认,确认后此提示不再弹出。`)){ GM_setValue('notice_mark', '0.1.0') } } /* ====== 引入工具库 ====== */ // const DMSTookit = new DMS_UserScripts.Toolkit({ // GM_info, // GM_registerMenuCommand // }) /* ====== 初始设定 ====== */ /** * Tag: 【Data】获取网站对应选项 */ const ruleName = 'rule_'+window.location.hostname const options = new Proxy({ expand_article: false, super_expand : false, remove_pop : false, }, { get: (target, property, receiver)=>{ return (Object.assign( target, GM_getValue(ruleName, {}) ))[property] }, set: (target, property, value, receiver)=>{ (Object.assign( target, GM_getValue(ruleName, {}) ))[property] = value GM_setValue(ruleName, target) } }) /** * 如果所有选项皆为默认,则返回清除存储并 false 否则返回 true */ const optionsCleaner = ()=>{ // 如果所有选项都为否,则不执行任何内容 if (!options.expand_article && !options.super_expand && !options.remove_pop) { // 如果存储中有此站规则,删除此规则 if (GM_getValue(ruleName)){ GM_deleteValue(ruleName) } return false } return true } /* ====== 菜单相关 ====== */ /** * Tag: 【Func】菜单注册(不可用)函数 * @param {*} markName 标记名称 * @param {*} onName 启用状态菜单文字 * @param {*} offName 禁用状态菜单文字 * @param {*} reload 是否需要刷新页面,默认为是 * @returns 当前启用/禁用状态 */ const registerMenu = (markName, onName, offName, reload=true)=>{ if(options[markName]){ // 【Menu】菜单启用状态 GM_registerMenuCommand(onName, ()=>{ options[markName] = false optionsCleaner() if(reload) window.location.reload(1) }) return } // 【Menu】菜单禁用状态 GM_registerMenuCommand(offName, ()=>{ options[markName] = true if(reload) window.location.reload(1) }) } // Tag: 【Menu】基础展开 registerMenu('expand_article', '1、🍏自动展开启用中(仅本站)', '1、🍎自动展开禁用中(仅本站)') // Tag: 【Menu】超级展开菜单注册(不可用) registerMenu('super_expand', '2、🍏超级展开启用中(仅本站)', '2、🍎超级展开禁用中(仅本站)') // Tag: 【Menu】去除遮盖 registerMenu('remove_pop', '3、🍏去除遮挡开启中(仅本站)', '3、🍎去除遮挡禁用中(仅本站)') // 更多脚本 GM_registerMenuCommand('4、🐹更多脚本', ()=>{ const url = 'https://script.izyx.xyz/' if(GM_openInTab){ GM_openInTab(url) return } window.open(url, '_blank') }) /* ====== 执行判断 ====== */ // 如果所有选项都为否,则不执行任何内容 if (!optionsCleaner()) { return; } /* ====== 阙值设定 ====== */ /** * Tag: 【Data】正文判定,特定子元素数量 * 设定控制阈值,当元素的子元素是特定元素的数量超过此值,当作正文处理 */ const passagesMinCount = 3 /** * Tag: 【Data】正文判定,字符阈值 * 设定控制阈值,当元素内文字字数超过此值,当作正文处理 */ const contentMinCount = 200 /** * Tag: 添加样式 * 加入基础样式信息,后面通过为元素添加相应类来实现展开 */ const cssString = ` .expand-the-article-no-limit { max-height: none !important; height: auto !important; } .expand-the-article-display-none { display: none !important; } .expand-the-article-no-linear-gradient { -webkit-mask: none !important; } ` try { GM_addStyle(cssString) } catch (error) { // 重写添加 style 功能,以兼容无接口的运行环境 const style = document.createElement('style') // 添加额外 ID 便于识别 style.id = 'expand-the-article-for-me' style.innerHTML = cssString document.head.appendChild(style) } /* ====== 功能函数 ====== */ /** * Tag: 【Func】元素过滤器 * 对每个元素进行分析,是否为(疑似)正文元素 * @param {*} el 待判断元素 * @param {*} is_rollback 是否为回退判断,默认为 false * @returns * 是类正文元素返回 true * 无需处理元素返回 false * 非类正文元素返回 0 */ const elFilter = (el, is_rollback=false)=>{ try { // 非元素,无需处理 if(!el) return false // 特定标签,无需处理 const excludeTags = [ 'abbr', 'applet', 'area', 'audio', 'b', 'base', 'bdi', 'bdo', 'body', 'br', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'del', 'details', 'dfn', 'dialog', 'em', 'embed', 'fieldset', 'form', 'g', 'head', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'link', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meta', 'meter', 'noscript', 'optgroup', 'option', 'output', 'param', 'picture', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'svg', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'var', 'video', 'wbr', ]; if(excludeTags.indexOf(el.tagName.toLowerCase()) !== -1){ return false } // 统计元素中特定子元素的个数,判断是否属于正文 if(is_rollback){ // 如果是回退情况,直接计算所有后代元素 if ( el.querySelectorAll('p, br, h1, h2, h3, h4, h5, h6').length >= passagesMinCount ) return true; }else{ // 如果不是回退判断 let passages = 0 const children = el.children for(let i=0; i<children.length; i++){ if(/^(p|br|h1|h2|h3|h4|h5|h6)$/i.test(children[i].tagName)){ passages++ } } if(passages >= passagesMinCount){ return true } } // 如果有文字内容,并且字数大于阈值 if(el.innerText && el.innerText.length >= contentMinCount){ return true } } catch (error) {} return 0 } /** * Tag: 【Func】移除[阅读更多]按钮 * 移除可能的 阅读更多 按钮 * @param {*} el 待处理元素 * @param {*} index 当前处理层级,超出一定深度则跳出 */ const removeReadMoreButton = (el, index=0)=>{ if(index>=2) return // 只处理两层深度 for(const e of el.children){ // 定位元素或者上方外部为负,则隐藏 const eStyle = window.getComputedStyle(e) if ( (/^(absolute)$/i.test(eStyle.position) && e.innerText.length < 100) || /^-\d/i.test(eStyle.marginTop) ) { e.classList.add('expand-the-article-display-none'); } // 递归 removeReadMoreButton(e, ++index) } } /** * Tag: 移除渐变遮罩 * @param {*} el */ const removeMask = el=>{ if(elFilter(el)){ const elStyle = window.getComputedStyle(el) if(/linear-gradient/i.test(elStyle.webkitMaskImage)){ el.classList.add('expand-the-article-no-linear-gradient') } } } /** * Tag: 【Func】移除高度限定 * 移除元素高度限制,会尝试处理正文元素的所有祖先元素 * @param {*} el 待处理元素 */ const removeHeightLimit = el=>{ // 如果元素标签是 html 或 body 则返回 if(/^(html|body)$/i.test(el.tagName)) return // 移除渐变遮罩 removeMask(el) // 如果包含特定类名(表示已处理过),则返回 if(el.classList.contains('expand-the-article-no-limit')) return // 如果存在高度限制,或者隐藏内容,则去除 const elStyle = window.getComputedStyle(el) if ( elStyle.maxHeight !== 'none' || (elStyle.height !== 'auto' && elStyle.overflowY === 'hidden') ) { el.classList.add('expand-the-article-no-limit'); if(/linear-gradient/i.test(elStyle.webkitMaskImage)){ el.classList.add('expand-the-article-no-linear-gradient') } } // 寻找并移除 阅读更多 按钮 removeReadMoreButton(el) // 递归处理祖先元素 if(el.parentElement) removeHeightLimit(el.parentElement) } /** * Tag: 【Func】去除宽幅浮动元素 * 如果元素定位为 fixed ,并且宽度大于等于窗口宽度的 96%,则去除 * @param {*} el */ const hiddenPop = ()=>{ document.querySelectorAll('*').forEach(el=>{ if(elFilter(el) !== false) { const elStyle = window.getComputedStyle(el) if ( /^fixed$/i.test(elStyle.position) && el.offsetWidth >= 0.96 * window.innerWidth ) { el.classList.add('expand-the-article-display-none'); } } }) } /** * Tag: 【Func】元素回退函数 * 如果元素内不太可能包含正文,并且具有移除高度限定的类,则去除此类 * @param {*} el 待处理元素 */ const rollbackEl = el=>{ if(elFilter(el) === 0){ if (el.classList && el.classList.contains('expand-the-article-no-limit')) { el.classList.remove('expand-the-article-no-limit'); } // 非类正文元素,则取消它后代元素中所有的隐藏 el.querySelectorAll('.expand-the-article-display-none').forEach(e=>{ e.classList.remove('expand-the-article-display-none') }) rollbackEl(el.parentElement) } } /** * Tag: 【Func】元素变化处理 * @param {*} records 元素变化记录 */ const whenChange = async records => { // 改变的类型为 attributes,对发生变化的元素进行处理 const elGroupAttr = records .filter( (el) => /^attributes$/i.test(el.type)) .map((el) => el.target); // 改变的类型为 characterData,对发生变化的元素进行处理 const elGroupChange = records .filter( (el) => /^characterData$/i.test(el.type)) .map((el) => el.target); // 改变的类型为 childList,对添加和删除元素分别处理 const elGroupAdd = [] const elGroupRemove = [] records .filter((el) => /^childList$/i.test(el.type)) .forEach((el) => { el.addedNodes.forEach(node => elGroupAdd.push(node)); el.removedNodes.forEach(node => elGroupRemove.push(node.parentElement)); }); // 进行同步遍历 for await (const el of elGroupChange.concat(elGroupAttr)){ if(elFilter(el)){ removeHeightLimit(el) } } for await (const el of elGroupAdd){ if(elFilter(el)){ removeHeightLimit(el) el.querySelectorAll('*').forEach((e)=>{ if(elFilter(e)) removeHeightLimit(e) }) } } for await (const el of elGroupRemove){ rollbackEl(el) } // 如果需要去除浮动 if(options.remove_pop){ hiddenPop() } } // Tag: 【Data】页面监控选项 const obOptions = { childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: false, characterDataOldValue: false, attributeFilter: [], }; // Tag: 页面变化处理 const observer = new MutationObserver(async (records, observer) => { observer.disconnect(); await whenChange(records) // 页面处理完成之后重新监控页面变化 observer.observe(document.body, obOptions); }); /** * 对页面中所有元素进行展开判断 * 对页面的一次完整处理 */ const expandAllEl = ()=>{ document.querySelectorAll('*').forEach((el)=>{ if(elFilter(el)) removeHeightLimit(el) }) } // Tag: 开始全局处理 if(options.expand_article){ expandAllEl() window.addEventListener('load', function(){ expandAllEl() }) if(options.super_expand){ observer.observe(document.body, obOptions); } }else if(options.super_expand){ options.super_expand = false } if(options.remove_pop){ hiddenPop() window.addEventListener('load', function(){ hiddenPop() }) } // 闭包 End })()