自动展开全文的 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_listValues // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_openInTab // @grant GM_setClipboard // @run-at document-end // @version 1.4.0 // @supportURL https://docs.qq.com/form/page/DYVFEd3ZaQm5pZ1ZR // @homepageURL https://script.izyx.xyz/expand-the-article/ // @icon https://i.v2ex.co/b39y298il.png // @require https://greasyfork.org/scripts/408776-dms-userscripts-toolkit/code/DMS-UserScripts-Toolkit.js?version=840920 // @inject-into content // @noframes // @author 稻米鼠 // @created 2020-07-24 07:04:35 // @updated 2020-09-05 09:30:51 // @description 自动展开全文的 beta 版,大概永远不会正式。使用前请务必认真阅读发布页说明,安装即表示知悉、理解并同意说明中全部内容。默认不开启任何功能,请在脚本菜单中切换功能(设置只针对当前网站)。 // ==/UserScript== const expand_article_main_function = function(){ // 闭包 Start /* ====== 初始设定 ====== */ /* ------ 弹出提示 ------ */ if(!GM_getValue('notice_mark') || GM_getValue('notice_mark')!== '0.1.0'){ if(confirm(`【自动展开全文 beta+】三个小提示: 1、请不要将脚本地址告诉他人,谢谢~ 2、请先阅读发布页的说明,因为此脚本需要一丢丢操作~ 3、有问题请反馈给作者,他有很努力的~`)){ GM_setValue('notice_mark', '0.1.0') } } /* ------ 引入工具库 ------ */ const DMSTookit = new DMS_UserScripts.Toolkit({ GM_info, // 脚本信息,用来控制台输出相关信息,以及通过当前版本好判断是否弹出更新提示 GM_addStyle, // 向页面注入样式,脚本功能重要依赖 GM_getValue, // 获取存储数据,用于读取脚本设置 GM_setValue, // 写入存储数据,用于存储脚本设置 GM_deleteValue, // 删除存储数据,用于清理脚本设置 GM_registerMenuCommand, // 注册脚本菜单 GM_unregisterMenuCommand, // 反注册脚本菜单 GM_openInTab, // 打开标签页 }) /* ------ Debug 相关 ------ */ const is_debug = DMSTookit.is_debug /* ------ 读取规则 ------ */ /** * Tag: 【Data】获取网站对应选项 */ const ruleName = 'rule_'+window.location.hostname const options = DMSTookit.proxyDataAuto(ruleName, { expand_article: false, super_expand : false, remove_pop : false, include_home : false, }) /* ------ 菜单注册 ------ */ // Tag: 菜单注册 // 【Menu】基础展开 DMSTookit.menuToggle(options, 'expand_article', '1、🍏自动展开启用中(仅本站)', '1、🍎自动展开禁用中(仅本站)', false, state=>{ if(state && window.location.pathname === '/' ){ options.include_home = true } window.location.reload(1) }) // 【Menu】超级展开菜单注册 DMSTookit.menuToggle(options, 'super_expand', '2、🍏超级展开启用中(仅本站)', '2、🍎超级展开禁用中(仅本站)') // 【Menu】去除遮盖 DMSTookit.menuToggle(options, 'remove_pop', '3、🍏去除遮挡开启中(仅本站)', '3、🍎去除遮挡禁用中(仅本站)') // 【Menu】自定义规则 GM_registerMenuCommand( options.custom ? '4、🍏自定义规则启用中(进阶)' : '4、🍎无自定义规则(进阶)', () => { const customRule = prompt('请输入自定义规则,帮助文档请见脚本发布页面', options.custom ? options.custom : '') options.custom = /^\s*$/.test(customRule) ? undefined : customRule window.location.reload(1) } ); // 【Menu】导入导出规则 GM_registerMenuCommand( '5、📓导入/导出(进阶)', () => { const keys = GM_listValues() const rules = {} for(const key of keys.filter(key=>/^rule_/.test(key))){ rules[key] = GM_getValue(key, {}) } DMSTookit.dblog('导入导出', JSON.stringify(rules)) GM_setClipboard(JSON.stringify(rules), 'text/plain') const inputRules = prompt('导出规则已复制到剪切板\n如需导入,请在输入框内粘贴导入规则\n输入 CLEAR 可以清空脚本设置\n无需导入则点击取消', '') if(inputRules){ // 清除设置 if(inputRules === 'CLEAR'){ if(confirm('确认清除数据?')){ for(const key of keys){ GM_deleteValue(key) } window.location.reload(1) } }else{ // 导入设置 try { Object.assign(rules, JSON.parse(inputRules)) for(const key in rules){ GM_setValue(key, rules[key]) } alert('规则导入成功~') window.location.reload(1) } catch (error) { alert('规则有误,无法正确导入。') DMSTookit.dblog('Import rules', error) } } } } ); // 更多脚本 DMSTookit.menuLink('6、🐹更多脚本', 'https://script.izyx.xyz/') /* ------ 执行判断 ------ */ // 如果有自定义规则,则执行相应规则 if(options.custom){ try { const setRule = (ruleName, rule)=>{ const style = (rule.expand ? rule.expand.replace(/,\s+/g, ',\n')+` { max-height: none !important; height: auto !important; }\n` : '') + (rule.remove ? rule.remove.replace(/,\s+/g, ',\n')+` { display: none !important; }\n` : '') + (rule.show ? rule.show.replace(/,\s+/g, ',\n')+` { display: block !important; }\n` : '') DMSTookit.info('自动展开全文', '当前启用自定义规则:'+ruleName) DMSTookit.dblog('Custom rule', rule) DMSTookit.addStyle(style) } const rules = JSON.parse(options.custom) for(const key in rules){ if(key==='default') continue const rule = rules[key] const reg = new RegExp(rule.reg, 'ig') DMSTookit.dblog('Custom rule\'s reg', reg) if(reg.test()){ setRule(key, rule) return } } if(rules.default){ setRule('Default', rules.default) return } } catch (error) { DMSTookit.info('自动展开全文', '自定义规则执行出错。'+error) } DMSTookit.info('自动展开全文', '自定义规则未适配当前网址。') } // 如果所有选项都为否,则不执行任何内容 if (!options.expand_article && !options.super_expand && !options.remove_pop && !options.custom) { // 如果存储中有此站规则,删除此规则 if (GM_getValue(ruleName)){ GM_deleteValue(ruleName) }else if(window.localStorage.getItem(ruleName)){ window.localStorage.removeItem(ruleName) } return } // 排除网站首页,一般都不需要展开,而且布局区别很大 if(window.location.pathname === '/' && !options.include_home) return /* ------ 阙值设定 ------ */ /** * Tag: 【Data】正文判定,特定子元素数量 * 设定控制阈值,当元素的子元素是特定元素的数量超过此值,当作正文处理 */ const passagesMinCount = 3 /** * Tag: 【Data】正文判定,字符阈值 * 设定控制阈值,当元素内文字字数超过此值,当作正文处理 */ const contentMinCount = 80 /** * Tag: 【Data】展开文章按钮子元素阙值 * 展开按钮元素包含的子元素应该不超过这个数值 */ const expandButtonMaxChildren = 10 /* ------ 样式注入 ------ */ /** * Tag: 添加样式 * 加入基础样式信息,后面通过为元素添加相应类来实现展开 */ DMSTookit.addStyle(` .expand-the-article-no-limit { max-height: none !important; height: auto !important; } .expand-the-article-display-none { display: none !important; } .expand-the-article-display-block { display: block !important; } .expand-the-article-no-linear-gradient { -webkit-mask: none !important; } `) /* ====== 功能函数 ====== */ /** * Tag: 【Debug】元素标记 * * @param {*} by 来源 * @param {*} el 元素对象 * @param {*} ret 结果 */ const addDebugMark = (el, log)=>{ if(is_debug && el && el.dataset){ const marks = el.dataset.mark ? el.dataset.mark.split('|') : [] if(marks.indexOf(log)!==-1) return marks.push(log) el.dataset.mark = marks.join('|') } } /** * 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){ addDebugMark(el,'elFilter-false-tag') return false } // 统计元素中特定子元素的个数,判断是否属于正文 if(is_rollback){ // 如果是回退情况,直接计算所有后代元素 if ( el.querySelectorAll('p, br, h1, h2, h3, h4, h5, h6').length >= passagesMinCount ){ addDebugMark(el,'elFilter-rollback-true') 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){ addDebugMark(el,'elFilter-true-passages') return true } } // 如果有文字内容,并且字数大于阈值 if(el.innerText && el.innerText.length >= contentMinCount){ addDebugMark(el,'elFilter-true-words') return true } } catch (error) {} addDebugMark(el,'elFilter-0') return 0 } /** * Tag: 移除渐变遮罩 * @param {*} el */ const removeMask = el=>{ if(elFilter(el) !== false){ const elStyle = window.getComputedStyle(el) addDebugMark(el,'removeMask-'+/linear-gradient/i.test(elStyle.webkitMaskImage)) if(/linear-gradient/i.test(elStyle.webkitMaskImage)){ el.classList.add('expand-the-article-no-linear-gradient') } } } /** * Tag: 【Func】移除[阅读更多]按钮 * 移除可能的 阅读更多 按钮 * @param {*} el 待处理元素 * @param {*} index 当前处理层级,超出一定深度则跳出 */ const removeReadMoreButton = (el)=>{ for(const e of el.children){ const eStyle = window.getComputedStyle(e) if ( // 绝对定位元素或者上方外部为负,则隐藏 (/^(absolute)$/i.test(eStyle.position) || /^-\d/i.test(eStyle.marginTop)) && e.innerText.length < 100 && // 文字数量小于 100 e.querySelectorAll('*').length < expandButtonMaxChildren && // 后代元素数量小于设定值 e.querySelectorAll( // 后代中不包含如下元素 'html, head, meta, link, body, article, aside, footer, header, main, nav, section, audio, video, track, embed, iframe, style, script, input, textarea' ).length === 0 ) { e.classList.add('expand-the-article-display-none'); } else { // 如果元素不需要隐藏,则去除渐变遮罩 removeMask(e); } } } /** * Tag: 【Func】移除高度限定 * 移除元素高度限制,会尝试处理正文元素的所有祖先元素 * @param {*} el 待处理元素 */ const removeHeightLimit = 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') ) { addDebugMark(el,'removeHeightLimit-height') el.classList.add('expand-the-article-no-limit'); if(elStyle.display === '-webkit-box') el.classList.add('expand-the-article-display-block'); } // 被隐藏元素判断 const childrenEls = el.children const childrenP = [] const childrenDIV = [] for(const cEl of childrenEls){ if(/^p$/i.test(cEl.tagName)){ childrenP.push(cEl) continue } if(/^div$/i.test(cEl.tagName)){ childrenDIV.push(cEl) continue } } // Todo: 隐藏元素显示条件的进一步推敲 // 此判断避免和 Clearly 扩展冲突 if(!/^chrome-clearly-/.test(el.id)){ // 如果元素被隐藏,则显示出来 addDebugMark(el,'removeHeightLimit-hiddenEl-'+/^none/i.test(elStyle.display)+'-'+childrenP.length+'-'+childrenDIV.length) if ( /^none/i.test(elStyle.display) && (childrenP.length >= 6 || childrenDIV.length >= 6) ) { el.classList.add('expand-the-article-display-block'); } // 如果子元素中有多个 div 或者段落被隐藏,则显示出来 // Todo: 这部分还有待仔细打磨 const childrenPHidden = childrenP.filter(e=>{ return /^none/i.test((window.getComputedStyle(e)).display) }) const childrenDIVHidden = childrenDIV.filter(e=>{ return /^none/i.test((window.getComputedStyle(e)).display) }) addDebugMark(el,'removeHeightLimit-hiddenEls-'+childrenPHidden.length+'-'+childrenDIVHidden.length) if(childrenPHidden>=6){ childrenPHidden.forEach(e=>{ e.classList.add('expand-the-article-display-block') }) } if(childrenDIVHidden>=6){ childrenDIVHidden.forEach(e=>{ e.classList.add('expand-the-article-display-block') }) } } // 寻找并移除 阅读更多 按钮 removeReadMoreButton(el) // 移除渐变遮罩 removeMask(el) } /** * Tag: 【Func】去除宽幅浮动元素 * 如果元素定位为 fixed ,并且宽度大于等于窗口宽度的 96%,则去除 * @param {*} el */ const hiddenPop = ()=>{ document.querySelectorAll('*').forEach(el=>{ // Todo: 能否更细致的判断 if(elFilter(el) !== false && !el.querySelectorAll('nav').length) { const elStyle = window.getComputedStyle(el) addDebugMark(el,'hiddenPop-'+/^fixed$/i.test(elStyle.position)+'-'+el.offsetWidth) 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=>{ // 如果元素标签是 html 或 body 则返回 if(!el || !el.tagName || /^(html|body)$/i.test(el.tagName)) return 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) } } /** * 对页面中所有元素进行展开判断 * 对页面的一次完整处理 */ const expandAllEl = ()=>{ document.querySelectorAll('*').forEach((el)=>{ if(elFilter(el)) removeHeightLimit(el) }) } /** * Tag: 【Func】元素变化处理 * @param {*} records 元素变化记录 */ const whenChange = async records => { for await (const rec of records){ // if(rec.type === 'attributes' || rec.type === 'characterData'){ // if(elFilter(rec.el)){ removeHeightLimit(rec.el) } // continue // } // if(rec.type === 'childListAdd'){ // if(elFilter(rec.el)){ // removeHeightLimit(rec.el) // rec.el.querySelectorAll('*').forEach((e)=>{ // if(elFilter(e)) removeHeightLimit(e) // }) // } // continue // } if(rec.type === 'childListRemove'){ rollbackEl(rec.el) continue } } expandAllEl() // 如果需要去除浮动 if(options.remove_pop){ hiddenPop() } } const observer = DMSTookit.pageObserverInit(document.body, (records)=>{ whenChange( DMSTookit.recordsPreProcessing(records) ) }) /* ====== 全局处理 ====== */ // Tag: 开始全局处理 if(options.expand_article){ expandAllEl() window.addEventListener('load', function(){ expandAllEl() }) if(options.super_expand){ observer.start() } }else if(options.super_expand){ options.super_expand = false } if(options.remove_pop){ hiddenPop() window.addEventListener('load', function(){ hiddenPop() }) } // 闭包 End } expand_article_main_function()