Greasy Fork is available in English.

colorDate

"Set date color according to date 根据网页上日期的新旧程度, 给日期进行着色, 比如说已经是5年前的一个日期会成为红色, 以便提醒阅览者,注意信息可能过于陈旧。"

// ==UserScript==
// @name     colorDate
// @namespace https://github.com/mika-cn/user-scripts
// @description "Set date color according to date 根据网页上日期的新旧程度, 给日期进行着色, 比如说已经是5年前的一个日期会成为红色, 以便提醒阅览者,注意信息可能过于陈旧。"
// @version  1.3.0
// @grant    none
// @include *
// @author   mika
// ==/UserScript==
/**
 *
 * # CHANGE LOG
 * 2018-06-18 [修复] 对于隐藏的文本,不再进行着色
 * 2018-04-17 支持<01.01> <2007.09> 格式
 *            修正标签正则
 * 2018-04-16 支持<01 Feb 2017> 格式
 *            添加 noscript 等特殊标签到黑名单
 *            兼容类似<textarea>$HTML</textarea>这种s13用法
 * 2018-04-14 支持<Feb 01 '17> 和 <2018年> 格式
 * 2018-04-13 修改替换方式,可应对多个正则存在包含关系的情况
 *
 */

(function(){
  'use strict';

  /*
   * 时长阀值与色值
   */
  var day  = 24 * 60 * 60 * 1000;
  var year = 365 * day;
  var colors = [
    {color: "#ff0000", threshold: 5 * year},
    {color: "#ff5f00", threshold: 3 * year},
    {color: "#d700ff", threshold: 1 * year},
    {color: "#8700ff", threshold: 183 * day},
    {color: "#00af00", threshold: 7 * day},
    {color: "#00d700", threshold: 1 * day},
  ];

  var monthPart = (`
    Jan|January|
    Feb|February|
    Mar|March|
    Apr|April|
    May|
    Jun|June|
    Jul|July|
    Aug|Augest|
    Sep|Sept|September|
    Oct|October|
    Nov|November|
    Dec|December
  `).replace(/\n\s*/gm, '');

  /*
   * 判断规则(信息越详细,放在前面)
   */
  var rules = [
    // yyyy-mm-dd
    {key: "01", regExp: /\d{4}-[01]?\d-[0-3]?\d/mg},
    // yyyy/mm/dd
    {key: "01", regExp: /\d{4}\/[01]?\d\/[0-3]?\d/mg},
    // yyyy.mm.dd
    {key: "01", regExp: /\d{4}\.[01]?\d\.[0-3]?\d/mg},
    // month dd, yyyy
    {key: "01", regExp: new RegExp("(?:"+ monthPart +") [0-3]?\\d[,\\s]{1}\\s?\\d{4}", 'igm')},
    // dd month yyyy
    {key: "01", regExp: new RegExp("[0-3]?\\d (?:"+ monthPart +")\\s?\\d{4}", 'igm')},
    // month dd 'yy
    {key: "09", regExp: new RegExp("(?:"+ monthPart +") [0-3]?\\d\\s'\\d{2}", 'igm')},
    // yyyy年mm月dd日
    {key: "01", regExp: /\d{4}\s?年\s?[01]?\d\s?月\s?[0-3]?\d\s?日/mg},
    // yyyy-mm | yyyy/mm | yyyy.mm
    {key: "02", regExp: /\d{4}[-\/\.]{1}[01]{1}\d/mg},
    // yyyy年mm月
    {key: "02", regExp: /\d{4}\s?年\s?[01]?\d\s?月/mg},
    // mm-dd | mm/dd | mm.dd
    {key: "03", regExp: /[01]{1}\d[-\/\.][0-3]{1}\d/mg},
    // mm月dd日
    {key: "03", regExp: /[01]?\d\s?月\s?[0-3]?\d\s?日/mg},
    // N 天前
    {key: "04", regExp: /\d+\s?天前/mg},
    // N days ago
    {key: "04", regExp: /\d+\s?days?\sago/mg},
    // N 月前
    {key: "05", regExp: /\d+\s?月前/mg},
    // N months ago
    {key: "05", regExp: /\d+\s?months?\sago/mg},
    // N 年前
    {key: "07", regExp: /\d+\s?年前/mg},
    // yyyy年
    {key: "10", regExp: /\d{4}\s?年/mg},
    // N years ago
    {key: "07", regExp: /\d+\s?years?\sago/mg},
    // month yyyy
    {key: "08", regExp: new RegExp("(?:"+ monthPart +")['\\s]{1}\\d{4}", 'igm')},
    // month dd
    {key: "06", regExp: new RegExp("(?:"+ monthPart +")['\\s]{1}[0-3]?\\d", 'igm')},
  ];



  // 主函数
  function colorDate(){
    var nodes = getMatchNode();
    nodes.forEach(function(node){
      var newNode = document.createElement("datetext");
      newNode.innerHTML = applyRules(node.nodeValue, rules)
      // replace child node
      node.parentNode.replaceChild(newNode, node);
    });
  }


  /*
   * 根据规则(判断是否是日期),获取满足的节点
   */
  function getMatchNode(){
    var nodes = [];
    var currentNode;
    var nodeIterator = getTextNodeIterator();
    while (currentNode = nodeIterator.nextNode()) {
      var v = currentNode.nodeValue;
      var rule = rules.some(function(rule){
        return !!v.match(rule.regExp);
      });
      if(rule){
        nodes.push(currentNode);
      }
    }
    return nodes;
  }


  /*
   * 获取文本节点迭代器
   */
  function getTextNodeIterator(){
    return document.createNodeIterator(
      document.body,
      NodeFilter.SHOW_TEXT, function(node) {
        var blackList = ['style', 'script', 'noscript', 'noframes', 'canvas', 'template', 'datetext', 'datespan'];
        var parentNode = node.parentNode;
        if(blackList.indexOf(parentNode.nodeName.toLowerCase()) > -1){
          return NodeFilter.FILTER_REJECT;
        }

        /*
         * 隐藏了的文本,不处理
         * - 有的程序员会使用隐藏文本来存储数据(比如存储 html 或者 链接)
         */
        var style = window.getComputedStyle(parentNode);
        if(style.display === "none"){
          return NodeFilter.FILTER_REJECT;
        }
        if(style.visibility === "hidden"){
          return NodeFilter.FILTER_REJECT;
        }
        return NodeFilter.FILTER_ACCEPT;
      }
   );
  }




  /*
   * 根据不同的正则,返回处理函数
   */
  function getHandler(key){
    switch(key){
      case "01" : return handler_01();
      case "02" : return handler_02();
      case "03" : return handler_03();
      case "04" : return handler_04();
      case "05" : return handler_05();
      case "06" : return handler_06();
      case "07" : return handler_07();
      case "08" : return handler_08();
      case "09" : return handler_09();
      case "10" : return handler_10();
      default: return function(match){ return match;};
    }
  }

  function handler_01(){
    return function(match){
      var dateStr = match.replace(/年|月|\./g, '-').replace("日", '');
      return replace(match, dateStr);
    };
  }

  function handler_09(){
    return function(match){
      var parts = match.split("'");
      var year  = parts.pop();
      var curr = new Date().getFullYear();
      var yStr = '' + Math.floor(curr%100 > parseInt(year) ? curr/100 : curr/100 - 1).toString() + year;
      parts.push(yStr);
      return replace(match, parts.join(" "));
    }
  }

  function handler_02(){
    return function(match){
      var dateStr = match.replace(/年|\/|\./g, '-').replace("月", '') + "-01";
      return replace(match, dateStr);
    };
  }

  function handler_03(){
    return function(match){
      var dateStr = (new Date()).getFullYear().toString() + "-" + match.trim().replace(/月|\/|\./g, '-').replace("日", '');
      return replace(match, dateStr);
    };
  }

  function handler_04(){
    return function(match){
      var n = parseInt(match.match(/\d+/)[0]);
      return replace(match, (new Date() - n * day));
    };
  }

  function handler_05(){
    return function(match){
      var n = parseInt(match.match(/\d+/)[0]);
      return replace(match, (new Date() - n * 30 * day));
    };
  }

  function handler_06(){
    return function(match){
      return replace(match, match.replace(/'/, ' ') + " " + (new Date()).getFullYear().toString());
    };
  }


  function handler_07(){
    return function(match){
      var n = parseInt(match.match(/\d+/)[0]);
      return replace(match, (new Date() - n * 365 * day));
    };
  }

  function handler_08(){
    return function(match){
      var v = match.replace(/'/, ' ').split(' ').join(" 01 ");
      return replace(match, v);
    };
  }

  function handler_10(){
    return function(match){
      var year = parseInt(match.match(/\d+/)[0]).toString();
      return replace(match, year + "-01-01");
    }
  }

  /*
   * 处理函数, 根据时长阀值着色
   */
  function replace(match, dateStr){
    try{
      var result = match;
      var diff = Date.now() - new Date(dateStr);
      colors.some(function(item){
        if(diff >= item.threshold){
          result = "<datespan style='color:"+ item.color +";'>" + match +"</datespan>";
          return true;
        }
        return false;
      });
      return result;
    }catch(e){
      return match;
    }
  }

  function applyRules(text, rules){
    var r = matchRules(text, rules);
    return replaceMark(r.markedText, r.values);
  }

  /*
   * 根据rules,进行匹配,标记匹配, 并计算出替换值
   * 用来避免两个正则是包含关系的情况
   */
  function matchRules(text, rules){
    var idxUtil = makeIdxUtil(-1);
    var values = [];
    rules.forEach(function(rule){
      var handler = getHandler(rule.key);
      text = text.replace(rule.regExp, function(match){
        values.push(handler(match));
        var mark = "@[["+ idxUtil.next() +"]]";
        return mark
      });
    });
    return {
      markedText: text,
      values: values
    }
  }

  // 索引工具
  function makeIdxUtil(init){
    return {
      curr: init,
      next: function(){ this.curr++; return this.curr; },
    }
  }


  // 把标记替换为着色后的人日期
  function replaceMark(text, values){
    return text.replace(/@\[\[\d+\]\]/mg, function(mark){
      var idx = parseInt(mark.match(/\d+/)[0]);
      return values[idx]
    });
  }


  /*
   * 创建延迟调用对象
   * 用来防止短时间内被调用多次
   */
  var createDelayCall = function(fn, delay){
    var dc = {};
    dc.action = fn;
    dc.clearTimeout = function(){
      if(dc.timeoutId){
        clearTimeout(dc.timeoutId);
      }
    };
    dc.run = function(){
      dc.clearTimeout();
      dc.timeoutId = setTimeout(function(){
        dc.action();
        dc.clearTimeout();
      }, delay);
    };
    return dc;
  };

  /*
   * 判断变更是否来自脚本
   */
  function isColorDateMotation(mutationRecords){
    return mutationRecords.every(function(record){
      return record.type === "childList" && record.addedNodes.length > 0 && record.addedNodes[0].nodeName.toLowerCase() === "datetext";
    });
  }


  /*
   * 初始化 mutationObserver
   */
  function initMutationObserver(){
    var delayColorDate = createDelayCall(colorDate, 400);
    var observer = new MutationObserver(function(mutationRecords){
      if(isColorDateMotation(mutationRecords)){
        // 本脚本产生的变更,不触发
        // console.log("Ignore motation")
      }else{
        delayColorDate.run();
      }
    });
    observer.observe(document, {
      attributes: false,
      childList: true,
      subtree: true
    });
    //console.log("init mutationObserver");
  }

  // 监听变更,触发着色 (适用于动态网页,如: ajax加载内容后产生变更)
  if(MutationObserver){ initMutationObserver(); }
  // 静态网页加载过程中不会产生变更, 直接调用
  setTimeout(colorDate, 400);
  setTimeout(colorDate, 1000);
  setTimeout(colorDate, 4000);

})();