コンテンツにスキップ

英文维基 | 中文维基 | 日文维基 | 草榴社区

利用者:Frozen-mikan/Archivebot.js

お知らせ: 保存した後、ブラウザのキャッシュをクリアしてページを再読み込みする必要があります。

多くの WindowsLinux のブラウザ

  • Ctrl を押しながら F5 を押す。

Mac における Safari

  • Shift を押しながら、更新ボタン をクリックする。

Mac における ChromeFirefox

  • Cmd Shift を押しながら R を押す。

詳細についてはWikipedia:キャッシュを消すをご覧ください。

/*
 * このスクリプトを実行したページの過去ログ化を支援します。
 * 投稿用ボタンは実装されていません。
 * @author [[w:ja:User:Frozen-mikan]]
 * 
 * 【注意】
 * このスクリプトの中には全く使われていない部分も含まれています。
 */
jQuery( document ).ready( function() {
  
  /* 初期設定。
   * 最上位無名関数の変数テーブルを汚さないために隔離。
   */
  function initialSettings() {
    window.fm = window.fm || {};
    var bot = fm.archivebot = fm.archivebot || {};
    
    /* UI追加場所の確保(無ければ終了) */
    var contentSub = document.getElementById('contentSub');
    if (!contentSub
      || !contentSub.parentNode
      || !contentSub.parentNode.insertBefore
    ) {
      console.log('Do not find the "id=contentSub". Exit!');
      return false;
    }
    
    /* 過去ログの設定 */
    var config = bot.config = bot.config || {};
    config.archiveheader = getTemplateTag('Archives');
    config.maxarchivesize = '100K';
    config.basepagename = wgPageName;
    config.subbasepagename = '過去ログ';
    config.counter = 1;
    config.algo = '30(d)';
    
    return true;
  }
  
  /* APIのURLを取得 */
  function getApiBase() {
    return mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?';
  }
  
  /* ページタイトルから編集用のURLを取得 */
  function getContentUrl(title) {
    var apiBase = getApiBase();
    return apiBase + getParamText({
      'action' : 'query',
      'format' : 'json',
      'prop' : 'info|revisions',
      'intoken' : 'edit',
      'rvprop' : 'timestamp|content',
      'titles' : title
    });
  }
  
  /* 名前空間名と指定文字列から始まるページを取得するためのURLを取得 */
  function getSubpagesUrl(namespace, prefix) {
    var apiBase = getApiBase();
    return apiBase + getParamText({
      'action' : 'query',
      'format' : 'json',
      'list' : 'allpages',
      'aplimit' : '500',
      'apnamespace' : namespace,
      'apprefix' : prefix
    });
  }
  function getEditUrl(title) {
    var apiBase = getApiBase();
    return apiBase + 'action=edit&format=json';
  }
  
  function getParamText(param) {
    var paramText = '';
    for (var key in param) {
      paramText += '&' + encodeURIComponent(key);
      var val = param[key];
      if (val && !(val == '')) {
        paramText += '=' + encodeURIComponent(val);
      }
    }
    return paramText;
  }
  /* 
   * callback: function callback(obj)
   */
  function get(url, callback) {
    var request = sajax_init_object();
    if (request == null) {
      return;
    }
    request.open('GET', url, true);
    request.onreadystatechange = function() {
      if (request.readyState == 4) {
        try {
          var responseText = request.responseText;
          var obj = JSON.parse(responseText);
        } catch (err) {
          console.log("Error (by parse): " + err);
          return;
        }
        callback(obj);
      }
    };
    request.setRequestHeader ('Pragma', 'cache=yes');
    request.setRequestHeader ('Cache-Control', 'no-transform');
    request.send(null);
  }
  
  function post(url, param, callback) {
    if (!param.summary) {
      param.summary = 'スクリプトによる編集: 書き込み試験中';
    }
    var content = getParamText(param);
    var request = sajax_init_object();
    if (request == null) {
      return;
    }
    request.open('POST', url, true);
    request.onreadystatechange = function() {
      if (request.readyState == 4) {
        try {
          var responseText = request.responseText;
          var obj = JSON.parse(responseText);
        } catch (err) {
          console.log("Error (by parse): " + err);
          return;
        }
        callback(obj);
      }
    };
    request.setRequestHeader ('Cache-Control', 'no-transform');
    request.setRequestHeader ('Content-Type',
      'application/x-www-form-urlencoded');
    request.send(content);
  }
  
  /* 最新版の内容と編集用のトークンなどの取得 */
  function getContent(title) {
    var bot = window.fm.archivebot; /* 要事前検査 */
    title = title.replace(/_/g, ' ');
    console.log('loading data: ' + title);
    get(getContentUrl(title), function(obj) {
      if ( !obj
        || !obj['query']
        || !obj['query']['pages']
      ) { return; }
      var pages = obj['query']['pages'];
      var param = {};
      for (var pageid in pages) {
        var page = pages[pageid];
        param.title = page.title;
        param.token = page.edittoken;
        param.starttimestamp = page.starttimestamp;
        if ( !page['revisions']
          || !page['revisions'][0]
          || !page['revisions'][0]['*']
        ) {
          param.text = '';
        } else {
          param.basetimestamp = page['revisions'][0]['timestamp'];
          param.text = page['revisions'][0]['*'];
          var pagetitle = page['title'];
          if (!bot.pages) bot.pages = {};
          var pagedata = bot.pages[pagetitle] = bot.pages[pagetitle] || {};
          /* overwrite: no cache */
          pagedata.threads = parseRawdata(page['revisions'][0]['*']);
        }
        pagedata.param = param;
      }
      printData(title, pagedata.threads);
      if (!/\//.test(title))
        getArchivePages(wgTitle);
    }); /* end of get */
  }
  
  function getArchivePages() {
    var fullPagename = wgPageName.replace(/_/g, ' ');
    var bot = window.fm.archivebot;
    getSubpages(function() {
    var subpages = bot.pages[fullPagename].subpages;
    bot.archivepages = bot.archivepages || [];
    var reBasepage = new RegExp(wgPageName + '/(.+?)$');
    var reArchiveSubpage = /^(過去ログ *)(\d+)$/;
    var latest;
    for(var i = 0; i < subpages.length; ++i) {
      var subpage = subpages[i];
      var subpagename = subpage.title.replace(
        reBasepage, function(p0, p1) {
          return p1; });
      var mArchiveSubpage = reArchiveSubpage.exec(subpagename);
      var m = mArchiveSubpage;
      if (!m) continue;
      if (!latest || latest.pageid < subpage.pageid) {
        bot.archivepages.push(subpage);
        bot.config.subbasename = m[1];
        bot.config.counter = m[2];
        latest = subpage;
        console.log(latest);
      }
    }
    /* 過去ログの最新版らしきタイトルが見つかれば内容を取得する */
    if (latest) getContent(latest.title);
    });
  }
  function getSubpages(fn) {
    var fullPagename = wgPageName.replace(/_/g, ' ');
    var basePagename = wgTitle;
    var bot = window.fm.archivebot;
    var config = bot.config;
    var namespace = wgNamespaceNumber;
    var prefix = basePagename + '/';
    get(getSubpagesUrl(namespace, prefix), function(obj) {
      if ( !obj
        || !obj['query']
        || !obj['query']['allpages']
      ) { return; }
      var subpages = obj['query']['allpages'];
      if (!bot.pages) bot.pages = {};
      bot.pages[fullPagename].subpages = subpages;
      fn();
    }); /* end of get */
  }
  
  /* 
   * rawdata: (String) サーバから取得したwikitext
   * 戻り値: (Object) 解析後の要素をまとめたオブジェクト
   *     threads: thread の配列
   *       thread: header, lines, times で構成される
   */
  function parseRawdata(rawdata) {
    var threads = [];
    
    /* 開始時、終了時、reHeaderにマッチした時に呼ぶ
     * thread: (Object) この関数の戻り値を使う。
     * header: (String) reHeader にマッチした時はその行。
     *         開始時は空文字列。終了時は不定。
     * 戻り値: 新しい thread オブジェクト。
     */
    function cleanup(thread, header) {
      if (thread) {
        thread.times.sort(compareNumbers);
        threads.push(thread);
      }
      thread = {};
      thread.header = header;
      thread.lines = [];
      thread.times = [];
      return thread;
    }
    var lines = rawdata.split('\n');
    var thread = cleanup(null, ''); /* 冒頭部の header は空文字列 */
    var reHeader = /^={2}[^=]+=+\s*$/;
    var reDatetime = /(\d{4})年(\d+)月(\d+)日 \([日月火水木金土]\) (\d+):(\d+) \(UTC\)$/g; // とりあえず行末の署名時刻に限定
    var mDatetime;
    for (var i = 0; i < lines.length; ++i) (function (line) {
      if (reHeader.test(line)) {
        thread = cleanup(thread, line);
      } else {
        thread.lines.push(line);
        while ((mDatetime = reDatetime.exec(line)) != null) (function (m) {
          var utctime = Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5]);
          thread.times.push(utctime);
        })(mDatetime);
      }
    })(lines[i]);
    cleanup(thread);
    return threads;
  }
  
  /* 一つのスレッドを wikitext にまとめる。 */
  function getRawdata(thread) {
    var rawdata = '';
    var header = thread.header;
    if ('' != header) { /* 導入部の header は識別用 */
      rawdata += header + '\n';
    }
    if (thread.lines && thread.lines.join) {
      rawdata += thread.lines.join('\n');
    }
    return rawdata;
  }
  
  /* 全てのスレッドをひとつの wikitext にまとめる。 */
  function getRawdataAll(threads) {
    var rawdata = '';
    var first = true;
    for (var i = 0; i < threads.length; ++i) {
      if (first) {
        first = false;
      } else {
        rawdata += '\n';
      }
      rawdata += getRawdata(threads[i]);
    }
    return rawdata;
  }
  
  /* 解析結果を表示する
   * title: (String) 表題に使用する
   * threads: (Object) rawdata を 解析したスレッド配列
   */
  function printData(title, threads) {
    var trs = [];
    for (var i = 0; i < threads.length; ++i) {
      var thread = threads[i];
//      if (thread.header == '') continue;
      var size = numberOfUTF8Bytes(getRawdata(thread));
      var first = thread.times[0] || '-';
      var last = thread.times[thread.times.length - 1] || '-';
      var tr = {
        'size' : size,
        'first' :
          first == '-' ?
            first :
            Math.floor(100 * diffTime(first).day())/100,
        'last' :
          last == '-' ?
            last :
            Math.floor(100 * diffTime(last).day())/100,
        'posts': thread.times.length,
        'name' : thread.header.replace(/^== *(.+?) *== *$/, function(p0, p1) {
          return p1; })
      };
      trs.push(tr);
    }
    var rawdataAll = getRawdataAll(threads);
    console.log('Number of threads: ' + (threads.length - 1) +
      '; Number of utf-8 bytes: ' + numberOfUTF8Bytes(rawdataAll)
    );
    displayContentInfo(title, ['size', 'first', 'last', 'posts', 'name'], trs)
  }
  
  /* テンプレートの簡易文字列表現 */
  function getTemplateTag(templatename) {
    return '{' + '{' + templatename + '}}';
  }
  
  /* 日付と時間に関係する変数と関数 */
  
  /* 実行時の基準時間。ミリ秒単位。 */
  var CURRENT_TIME = new Date().getTime();
  
  /* 
   * 時間差を計算する。time1 から time0 を引いた値を基にしたオブジェクトを返す。
   * 世界標準時1970年1月1日0時0分0秒からのミリ秒を表す整数値を用いる。
   * @param time1 時刻1
   * @param time0 時刻0 
   */
  function diffTime(time1, opt_time0) {
    var time0 = typeof opt_time0 !== 'undefined' ? opt_time0 : CURRENT_TIME;
    var time = time1 - time0;
    return {
      time: time,
      day: function() {
        return time / 1000 / 3600 / 24;
      }
    };
  }
  
  /* Array.sort() 用の数値比較関数 */
  function compareNumbers(a, b) {
    if (a < b) {
      return -1;
    } else if (b < a) {
      return 1;
    } else {
      return 0;
    }
  }
  
  /**
   *  文字列を UTF-8 にした場合のバイト数を計算する。
   * TODO: code の値はUTF-16です。サロゲートペアを考慮すること。
   * FIXME: サロゲートペアを考慮した新しい関数に置き換える。
   */
  function numberOfUTF8Bytes(string) {
    var len = 0;
    if (!string) return len;
    var code;
    for (var i = 0; ; ++i) {
      code = string.charCodeAt(i);
      if (isNaN(code)) break;
      if (code < 0x80) {
        len += 1;
      } else if (code < 0x800) {
        len += 2;
      } else if (code < 0x10000) {
        len += 3;
      } else {
        len += 4;
      }
    }
    return len;
  }

  
  /* UI */
  function insertContent(c) {
    /* 存在は確認済み */
    var contentSub = document.getElementById('contentSub');
    contentSub.parentNode.insertBefore(c, contentSub.nextSibling);
  }
  /* 要素の生成支援 */
  function createElement(name, attr, inner) {
    if (!name) return null;
    var element = document.createElement(name);

    /* attr */
    for (key in attr) {
      if (key == 'style') {
        for (styleKey in attr[key]) {
          element[key][styleKey] = attr[key][styleKey];
        }
      } else {
        element.setAttribute(key, attr[key]);
      }
    }
    
    /* inner */
    if (typeof inner == 'undefined') return element;
    for (var i=0; i<inner.length; ++i) {
      var type = typeof inner[i];
      if (type == 'string' || type == 'number') {
        element.appendChild(document.createTextNode(inner[i]));
      } else {
        element.appendChild(inner[i]);
      }
    }
    return element;
  }
  /* 初期配置 */
  function displayInitialPlacement() {
    var button = createElement('button', {'type': 'button'}, ['解析と表示']);
    var form = createElement('form', {'id': 'botform'}, [button]);
    $(button).on('click', function() {
      getContent(wgPageName);
    });
    insertContent(form);
  }
  /* 修正内容の確認 */
  function displayContentInfo(title, headers, data) {
    headers = headers || [];
    data = data || [];
    var th_ = function(headers) {
      var tr = [];
      for (var i = 0; i < headers.length; ++i) {
        tr.push(createElement('th', {}, [headers[i]]));
      }
      return tr;
    };
    var td_ = function(i, data) {
      var tr = [];
      var attr = {'id': 'check' + i, 'type': 'checkbox'};
      if (data.last != '') {
        if (data.last <= -7) { /* 7日前より前。誤差は計算精度次第。 */
          attr.checked = '';
        }
      }
      var checkbox = createElement('input', attr);
      $(checkbox).on('change', function(e){
        var src = e.target || e.srcElement;
        console.log(src.checked);
      });
      tr.push(createElement('td', {}, ['' + i]));
      tr.push(createElement('td', {}, [checkbox]));
      for (var i = 0; i < headers.length; ++i) {
        var value = data[headers[i]];
        tr.push(createElement('td', {}, [value]));
      }
      return tr;
    };
    var form = document.getElementById('botform');
    var table = createElement('table', {'class': 'wikitable'}, [
      createElement('caption', {}, [title]),
      createElement('tr', {}, th_(['#', '過去ログ'].concat(headers)))
    ]);
    for (var i = 0; i < data.length; ++i) {
      table.appendChild(createElement('tr', {}, td_(i, data[i])));
    }
    form.appendChild(table);
  }
  
  function displayArchivePagesSize(pagesSize) {
    var headers = ['Discussion', 'Archiving'];
    var table = createElement('table', {'class': 'wikitable'}, [
      createElement('caption', {}, ['Pagesize']),
      createElement('tr', {}, [
        createElement('th', {}, [headers[0]]),
        createElement('th', {}, [headers[1]])
      ])
    ]);
  }
  
  /* Discussion, Archive
   */
  function updateArchivePagesSize(pagesSizeContainer) {
    /* update('pages_size', 'ps_', pagesSize); */
    update(pagesSizeContainer);
  }
  
  /* コンテナ内部のprefix付きidを取得し、内容 (innerHTML) を上書きする。 */
  function update(container) {
    var cid = container.cid;
    var prefix = container.prefix;
    var data = container.data;
    var c = document.getElementById(cid);
    if (c == null) {
      console.log('Element Not Found (by id): Id is: ' + cid);
      return;
    }
    for (var key in data) {
      var item = c.getElementById(prefix + key);
      if (item == null) { continue; }
      var oldHTML = item.innerHTML;
      if (oldHTML != data.key) {
        item.innerHTML = data.key;
      }
    }
  }
  
  /* main */
  if (!initialSettings()) return;
  displayInitialPlacement();
});