// ==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_unregisterMenuCommand
// @grant GM_openInTab
// @run-at document-end
// @version 0.1.1
// @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://greasyforks.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-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_addStyle, // 向页面注入样式,脚本功能重要依赖
GM_getValue, // 获取存储数据,用于读取脚本设置
GM_setValue, // 写入存储数据,用于存储脚本设置
GM_deleteValue, // 删除存储数据,用于清理脚本设置
GM_registerMenuCommand, // 注册(不可用)脚本菜单
GM_unregisterMenuCommand, // 反注册(不可用)脚本菜单
GM_openInTab, // 打开标签页
})
/* ------ 读取规则 ------ */
/**
* Tag: 【Data】获取网站对应选项
*/
const ruleName = 'rule_'+window.location.hostname
const options = DMSTookit.proxyDataAuto(ruleName, {
expand_article: false,
super_expand : false,
remove_pop : false,
})
/* ------ 菜单注册(不可用) ------ */
// Tag: 【Menu】基础展开
DMSTookit.menuToggle(options, 'expand_article', '1、🍏自动展开启用中(仅本站)', '1、🍎自动展开禁用中(仅本站)')
// Tag: 【Menu】超级展开菜单注册(不可用)
DMSTookit.menuToggle(options, 'super_expand', '2、🍏超级展开启用中(仅本站)', '2、🍎超级展开禁用中(仅本站)')
// Tag: 【Menu】去除遮盖
DMSTookit.menuToggle(options, 'remove_pop', '3、🍏去除遮挡开启中(仅本站)', '3、🍎去除遮挡禁用中(仅本站)')
// 更多脚本
DMSTookit.menuLink('4、🐹更多脚本', 'https://script.izyx.xyz/')
/* ------ 执行判断 ------ */
// 如果所有选项都为否,则不执行任何内容
if (!options.expand_article && !options.super_expand && !options.remove_pop) {
// 如果存储中有此站规则,删除此规则
if (GM_getValue(ruleName)){
GM_deleteValue(ruleName)
}else if(window.localStorage.getItem(ruleName)){
window.localStorage.removeItem(ruleName)
}
return
}
// 排除网站首页,一般都不需要展开,而且布局区别很大
if(window.location.pathname === '/') return
/* ------ 阙值设定 ------ */
/**
* Tag: 【Data】正文判定,特定子元素数量
* 设定控制阈值,当元素的子元素是特定元素的数量超过此值,当作正文处理
*/
const passagesMinCount = 3
/**
* Tag: 【Data】正文判定,字符阈值
* 设定控制阈值,当元素内文字字数超过此值,当作正文处理
*/
const contentMinCount = 200
/**
* 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-no-linear-gradient { -webkit-mask: none !important; }
`)
/* ====== 功能函数 ====== */
/**
* 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: 移除渐变遮罩
* @param {*} el
*/
const removeMask = el=>{
if(elFilter(el) !== false){
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 待处理元素
* @param {*} index 当前处理层级,超出一定深度则跳出
*/
const removeReadMoreButton = (el, index=0)=>{
if(index>=2) return // 只处理两层深度
for(const e of el.children){
const eStyle = window.getComputedStyle(e)
// Todo: 内部元素类型、数量判断
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'
).leangth === 0
) {
e.classList.add('expand-the-article-display-none');
} else {
// 如果元素不需要隐藏,则去除渐变遮罩
removeMask(e);
}
// 递归
removeReadMoreButton(e, ++index)
}
}
/**
* Tag: 【Func】移除高度限定
* 移除元素高度限制,会尝试处理正文元素的所有祖先元素
* @param {*} el 待处理元素
*/
const removeHeightLimit = el=>{
// 如果元素标签是 html 或 body 则返回
if(/^(html|body)$/i.test(el.tagName)) return
// 如果包含特定类名(表示已处理过),则返回
if(el.classList.contains('expand-the-article-no-limit')) return
// 移除渐变遮罩
removeMask(el)
// 如果存在高度限制,或者隐藏内容,则去除
const elStyle = window.getComputedStyle(el)
if (
elStyle.maxHeight !== 'none' ||
(elStyle.height !== 'auto' && elStyle.overflowY === 'hidden')
) {
el.classList.add('expand-the-article-no-limit');
}
// 寻找并移除 阅读更多 按钮
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=>{
// 如果元素标签是 html 或 body 则返回
if(/^(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)
}
}
/**
* 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
}
}
// 如果需要去除浮动
// Todo: 能否更细致的判断
if(options.remove_pop){
hiddenPop()
}
}
const observer = DMSTookit.pageObserverInit(document.body, (records)=>{
whenChange( DMSTookit.recordsPreProcessing(records) )
})
/**
* 对页面中所有元素进行展开判断
* 对页面的一次完整处理
*/
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.start()
}
}else if(options.super_expand){
options.super_expand = false
}
if(options.remove_pop){
hiddenPop()
window.addEventListener('load', function(){
hiddenPop()
})
}
// 闭包 End
})()