自动展开全文(永久 beta+ 版)

自动展开全文的 beta 版,大概永远不会正式。使用前请务必认真阅读发布页说明,安装即表示知悉、理解并同意说明中全部内容。默认不开启任何功能,请在脚本菜单中切换功能(设置只针对当前网站)。

Verze ze dne 24. 08. 2020. Zobrazit nejnovější verzi.

// ==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
})()
长期地址
遇到问题?请前往 GitHub 提 Issues。