// ==UserScript==
// @name 自动展开全文(永久 beta 版)
// @namespace Expand the article for vip.
// @match none
// @grant GM_addStyle
// @grant GM_getValue
// @run-at document-end
// @version 0.0.10
// @supportURL https://docs.qq.com/form/page/DYVFEd3ZaQm5pZ1ZR
// @homepageURL https://meta.appinn.net/t/17991
// @inject-into content
// @author 稻米鼠
// @created 2020-07-24 07:04:35
// @updated 2020-08-08 17:17:38
// @description 自动展开全文的 beta 版,大概永远不会正式。使用前请务必认真阅读发布页说明,安装即表示知悉、理解并同意说明中全部内容。此版本不包含任何 match 信息,请自行添加。
// ==/UserScript==
(function(){
// 闭包 Start
/**
* Tag: 【Data】正文判定,特定子元素数量
* 设定控制阈值,当元素的子元素是特定元素的数量超过此值,当作正文处理
*/
const passagesMinCount = 3
/**
* Tag: 【Data】正文判定,字符阈值
* 设定控制阈值,当元素内文字字数超过此值,当作正文处理
*/
const contentMinCount = 200
// 添加 debug 输出函数
/**
* Tag: 【Data】Debug 状态声明
*/
let is_debugger = false
try {
is_debugger = GM_getValue('debugger', false)
} catch (error) {}
/**
* Tag: 【Func】Debug 输出
* @param {*} content 输出内容。若为数组,则以组的形式输出
* @param {*} by 输出来源
*/
const dglog = (content, by='Default')=>{
if(is_debugger){
if(content instanceof Array){
console.groupCollapsed(by+': ')
for(const c of content){
console.info(c)
}
console.groupEnd()
}else{
console.info(by+': ', content)
}
}
}
/**
* Tag: 添加样式
* 加入基础样式信息,后面通过为元素添加相应类来实现展开
*/
const cssString = `
.expand-the-article-no-limit {
max-height: none !important;
height: auto !important;
}
.expand-the-article-display-none { display: 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 = [
'a',
'abbr',
'applet',
'area',
'audio',
'b',
'base',
'bdi',
'bdo',
'body',
'br',
'button',
'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){
dglog([el, '子元素', passages], 'elFitler 子元素 '+el.tagName+' - '+el.classList.toString())
return true
}
}
// 如果有文字内容,并且字数大于阈值
if(el.innerText && el.innerText.length >= contentMinCount){
dglog([el, '文字数', el.innerText.length, el.innerText], 'elFitler 文字数 '+el.tagName+' - '+el.classList.toString())
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: 【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
// 如果存在高度限制,或者隐藏内容,则去除
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】元素回退函数
* 如果元素内不太可能包含正文,并且具有移除高度限定的类,则去除此类
* @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');
dglog(el.tagName+' - '+el.classList.toString, 'Rollback')
}
// 非类正文元素,则取消它后代元素中所有的隐藏
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 => {
// 改变的类型为 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(elGroupAdd)){
if(elFilter(el)){ removeHeightLimit(el) }
}
for await (const el of elGroupRemove){
rollbackEl(el)
}
}
// Tag: 开始全局处理
dglog('全局过滤开始', 'State')
document.querySelectorAll('*').forEach((el)=>{
if(elFilter(el)) removeHeightLimit(el)
})
// 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();
dglog('元素发生变化', 'State')
await whenChange(records)
// 页面处理完成之后重新监控页面变化
observer.observe(document.body, obOptions);
});
// Tag: 页面监控启动
// observer.observe(document.body, obOptions);
// 闭包 End
})()