コンテンツにスキップ

利用者:Hatukanezumi/Anansi.js

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

多くの WindowsLinux のブラウザ

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

Mac における Safari

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

Mac における ChromeFirefox

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

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

/*
----
[[Image:Disambig.svg|25px]]
If you wish to know about Anansi in the real world, a spider, human or
both of them, see [[w:en:Anansi]].
----

Anansi - proofreading support script for Mediawiki.

Version: 0.00-alpha; it may not work as smart as you expect. :-/

== Requirements ==

* Mediawiki 1.13alpha (wikibits js 160) or later
* JavaScript 1.5 or later (apparently compliant with ECMA-262 3rd ed.)
* CSS level 2 (optionally level 2 rev. 1 features are used)
* DOM level 2
* Optional features:
** Transparency effect: Internet Explorer >=6?, CSS2 (Firefox, Safari, Opera)
** Pointing lines: SVG Tiny (Firefox >= 2?, Opera >= 9?, Safari >=3) or VML (IE >=6?)

== How to use Anansi ==

1. Add following line to your user script, [[User:YourName/monobook.js]] etc.:

  importScript("User:Hatukanezumi/Anansi.js");

2. Enjoy!

== Configuring Anansi ==

Edit your user page [[User:YourName/AnansiConfig.js]]
(you might create this page at first time).

''Description of configuration options will be written on
[[User:Hatukanezumi/Help:Anansi]]...''

== Code ==
 */

/*
 * Anansi
 *
 * Main Processes.
 *
 * Properties:
 *   config:        AnansiConfig object.
 *   util:          AnansiUtil object.
 *
 * Methods:
 *   onloadHook():  body.onload hook function.
 *   registerRule(definition)
 *   disableRule(id)
 *   enableRule(id)
 *                  register/disable/enable rules.
 *
 * Files:
 *   Style Sheet:   Configured by anansi.config.stylesheet.
 *                  Default styles are defined by "User:Hatukanezumi/Anansi.css".
 *   Config file:   "User:''YourUserName''/AnansiConfig.js".
 *                  Note that camelcase is used for this page name.
 *   Default Rules: "User:Hatukanezumi/AnansiRules.js".
 *
 */
function Anansi()
{
  /****
   **** Configurations and object-global variables.
   ****/

  /***
   *** Constants
   ***/
  var DEFAULT_STYLESHEET = "User:Hatukanezumi/Anansi.css";
  var VMLNS = "urn:schemas-microsoft-com:vml";
  var VMLRULE = "behavior: url(#default#VML); display: inline-block;";
  var SVGNS = "http://www.w3.org/2000/svg";
  var SVGVER = 1.1;
  var ICON_QUESTION = "http://upload.wikimedia.org/wikipedia/commons/thumb" +
                        "/2/24/Gtk-dialog-question.svg/16px-Gtk-dialog-question.svg.png";
  var ICON_CLOSE = "http://upload.wikimedia.org/wikipedia/commons/thumb" +
                     "/3/35/Button_normal.svg/14px-Button_normal.svg.png";
  var FONTWEIGHT_EMPH = "bold";
  var STROKEWIDTH_NORMAL = "1";
  var STROKEWIDTH_EMPH = "3";
  
  var LOGO_ENABLED = "http://upload.wikimedia.org/wikipedia/ja" +
                      "/2/2d/Anansi_logo_spider_enabled_small.png";
  var LOGO_ENABLED_ALT = "アナンシ有効";
  var LOGO_ENABLED_TITLE = "アナンシ有効。クリックすると無効にします。"; 
  var LOGO_DISABLED = "http://upload.wikimedia.org/wikipedia/ja" +
                       "/9/9a/Anansi_logo_spider_disabled_small.png";
  var LOGO_DISABLED_ALT = "アナンシ無効";
  var LOGO_DISABLED_TITLE = "アナンシ無効。クリックすると有効にします。"; 

  /***
   *** AnansiConfig
   ***
   *
   * Configuration variables.
   */
  function AnansiConfig()
  {
    this.debuglevel = 1;       // debug level.
    this.disabledpages = null; // disable Anansi on specific page(s).
    this.enabled = true;       // enable Anansi.
    this.lineshape = "polyline"; // line style.
    this.namespaces = [0, 1,   // Namespaces where Anansi is available.
                       2, 3,
                       4, 5,
                       6, 7,
                       8, 9,
                       10, 11,
                       12, 13,
                       14, 15
                       ];
    this.stylesheet = DEFAULT_STYLESHEET; // location of stylesheet.
  }

  /**
   ** config
   **/
  this.config = new AnansiConfig();
  var config = this.config;

  /***
   *** AnansiUtil
   ***
   *
   * Placeholder for user-side utilities.
   */
  function AnansiUtil()
  {
  }
  
  /**
   ** util
   **/
  this.util = new AnansiUtil();

  /***
   *** blocks
   ***/
  var blocks = new Array();

  /***
   *** index & seq
   ***/
  var index = 0;
  var seq = 0;

  /***
   *** rules
   ***/
  var rules = new Array();

  /***
   *** logo
   ***/
  var anansiLogo = null;

  /***
   *** AnansiPlatform
   ***
   *
   * Detect platform of client.
   */
  function AnansiPlatform()
  {
    this.domLevel = 0;
    this.graphicFormat = null;
    this.minVersion = 0;
    this.name = null;
    this.supported = false;
    this.ua = null;
    
    // get name of user-agent.
    if (navigator && navigator.userAgent)
      this.ua = navigator.userAgent;
    
    // check features.
    if (document.createElementNS)
      this.domLevel = 2;
    else if (document.getElementById)
      this.domLevel = 1;
    
    // detect user-agent and its version.
    if (window.opera) { // Opera
      this.name = "Opera";
      // cf. [http://www.howtocreate.co.uk/operaStuff/operaObject.html The mysterious Opera object].
      if (typeof window.opera.version == "function") { // >= 7.6
        var g = /^[0-9]+(\.[0-9]+)?/.exec(window.opera.version());
        if (g)
          this.minVersion = eval(g[0]);
        else
          this.minVersion = 7.6;
      }
      else if (typeof window.opera.buildNumber == "function") {
        var bn = window.opera.buildNumber('inconspicuous');
        if (0 < bn.indexOf("as smart as"))
          this.minVersion = 6.0;
        else if (bn)
          this.minVersion = 7.0;
      }
    }
    else if (document.all) { // Trident (or Tasman)
      this.name = "MSIE";
      var vernum = null;
      /*@cc_on
      @if (@_jscript)
        if (5.7 <= @_jscript_version)
          vernum = 7.0;
        else if (5.6 <= @_jscript_version)
          vernum = 6.0;
        else if (5.5 <= @_jscript_version)
          vernum = 5.5;
        else if (5.1 <= @_jscript_version)
          vernum = 5.01;
        else if (5.0 <= @_jscript_version)
          vernum = 5.0;
        else
          vernum = 4.0;
      @end @*/
      if (vernum)
        this.minVersion = vernum;
    }
    else if (this.ua && 0 < this.ua.indexOf(" AppleWebKit/")) { // WebKit
      this.name = "WebKit";
      var g;
      if (g = / AppleWebKit\/([0-9]+)(\.([0-9]+))?/.exec(this.ua)) {
        this.minVersion = eval(g[1]);
        if (g[3])
          this.minVersion += eval(g[3]) / 100.0;
      }
    }
    else if (this.ua && 0 < this.ua.indexOf("Gecko/")) { // Gecko
      this.name = "Gecko";
      var g;
      if (g = /rv:([01]\.[0-9]+)/.exec(this.ua))
        this.minVersion = eval(g[1]);
    }
    
    // determin graphic format
    if (2 <= this.domLevel)
      this.graphicFormat = "SVG";
    else if (this.name == "MSIE")
      this.graphicFormat = "VML";
    
    // check if this platform is supported.
    if (!this.domLevel)
      this.supported = false;
    else if (this.name == "Opera" && this.minVersion < 7.5)
      this.supported = false;
    else if (this.name == "MSIE" && this.minVersion < 5.5) // guess.
      this.supported = false;
    else if (this.name == "WebKit" && this.minVersion < 522.11) // Safari < 3
      this.supported = false;
    else if (this.name == "Gecko" && this.minVersion < 1.0) // guess.
      this.supported = false;
    else
      this.supported = true;
  }

  /**
   ** platform
   **/
  this.platform = new AnansiPlatform();
  var platform = this.platform;


  /****
   **** Main Function
   ****/

  /***
   *** Anansi.onloadHook()
   ***
   *
   * Event handler for Load event.
   */
  this.onloadHook = function()
    {
      // When Anansi is disabled, do nothing.
      if (!config.enabled)
        return;
    
      // When Anansi is disabled by personal toolbar button (via cookie), do nothing.
      if (!initLogo())
        return;

      if (!enableMe())
        return;
      
      // Initialize, parse and construct.
      try {
        init();
        parsePreview();
        markBlocks();
        
        addHandler(window, "resize", drawAllLines);
        if (window.setTimeout)
          window.setTimeout(drawAllLines, 5000); // FIXME: Case of long rendering process.
      } catch (e) { // Crashed.
        debug(e.name + ": " + e.message +
              " (" + e.fileName + ":" + e.lineNumber + ")", 3);
      }
    };

  /***
   *** enableMe()
   ***/
  var metaPageRe = new RegExp("\\.(js|css)$", "i");
  function enableMe()
  {
    // Will work only on preview mode.
    if (mw.config.get('wgAction') != "submit")
      return false;
    
    // Won't work on scripts and stylesheets.
    if (mw.config.get('wgNamespaceNumber') == 2 || mw.config.get('wgNamespaceNumber') == 8)
      if (metaPageRe.test(mw.config.get('wgPageName')))
        return false;
    
    // Won't work on specified pages.
    if (config.disabledpages && config.disabledpages.length)
      for (var i = 0; i < config.disabledpages.length; i++)
        if (config.disabledpages[i][0] == mw.config.get('wgNamespaceNumber') &&
            config.disabledpages[i][1] == wgTitle)
          return false;
    
    // Otherwise, if configured, will work on limited namespaces.
    if (config.namespaces && config.namespaces.length) {
      for (var i = 0; i < config.namespaces.length; i++)
        if (config.namespaces[i] == mw.config.get('wgNamespaceNumber') ||
            (config.namespaces[i] + "").toLowerCase() ==
            wgCanonicalNamespace.toLowerCase())
          return true;
      return false;
    }
    
    // OK
    return true;
  }

  /***
   *** init()
   ***
   *
   * Initialize Anansi:
   */
  function init()
  {
    // Set stylesheet.
    importStylesheet(DEFAULT_STYLESHEET);
    if (config.stylesheet != DEFAULT_STYLESHEET)
      importStylesheet(config.stylesheet);
    
    // Add a namespace and styles for MSIE VML.
    if (platform.name == "MSIE" && !document.namespaces.v) {
      document.namespaces.add("v", VMLNS);
      document.createStyleSheet().addRule("v\\:*", VMLRULE);
    }
  }

  /***
   *** initLogo()
   ***
   *
   * Initialize Anansi Logo:
   */
  function initLogo()
  {
    // Initialize logo.
    anansiLogo = document.createElement("img");
    anansiLogo.setAttribute("id", "anansiLogoImage");
    anansiLogo.setAttribute("class", "anansiLogo");
    
    var logoList = document.getElementById("p-personal");
    if (logoList) { // monobook, simple, myskin, modern
      var logoItem = document.createElement("li");
      logoItem.setAttribute("id", "anansiLogoItem");
      logoItem.appendChild(anansiLogo);
      logoList.getElementsByTagName("ul")[0].appendChild(logoItem);
    }
    else { // standard, nostalgia, cologneblue
      logoList = document.getElementById("quickbar");
      if (!logoList)
        logoList = document.getElementById("topbar");
      if (logoList)
        logoList.appendChild(anansiLogo);
    }
    if (!logoList) // Unknown skin.
      return true; // fallback to be always enabled.
    else
      addHandler(anansiLogo, 'click', toggleAnansi);
    
    // Get cookie.
    var enabled = getCookie("anansi");
    if (enabled && enabled == "disabled") {
      anansiLogo.setAttribute("src", LOGO_DISABLED); 
      anansiLogo.setAttribute("alt", LOGO_DISABLED_ALT); 
      anansiLogo.setAttribute("title", LOGO_DISABLED_TITLE); 
      return false;
    }
    else {
      anansiLogo.setAttribute("src", LOGO_ENABLED); 
      anansiLogo.setAttribute("alt", LOGO_ENABLED_ALT); 
      anansiLogo.setAttribute("title", LOGO_ENABLED_TITLE); 
    }
    return true;
  }

  /***
   *** toggleAnansi()
   ***
   *
   * Event handler: toggle Anansi.
   */
  function toggleAnansi() {
    if (!anansi.config.enabled)
      return;

    var enabled = getCookie("anansi");
    if (enabled && enabled == "disabled") {
      document.cookie = 'anansi=enabled; path=/';
      anansiLogo.setAttribute("src", LOGO_ENABLED); 
      anansiLogo.setAttribute("alt", LOGO_ENABLED_ALT); 
      anansiLogo.setAttribute("title", LOGO_ENABLED_TITLE); 
      return true;
    }
    else {
      document.cookie = 'anansi=disabled; path=/';
      anansiLogo.setAttribute("src", LOGO_DISABLED); 
      anansiLogo.setAttribute("alt", LOGO_DISABLED_ALT); 
      anansiLogo.setAttribute("title", LOGO_DISABLED_TITLE); 
      return false;
    }
  }

  /****
   **** Parser Functions.
   ****/

  /***
   *** Block(wtag, texts)
   ***
   *
   * Parsed “block” object.
   *
   * Properties:
   *   index: ''0-based block index'',
   *   wtag:  ''wiki tag name'',
   *   texts: ''Texts object'',
   *
   * where ''wiki tag name'' will be either of:
   * - paragraph    - "\n"
   * - preformatted - IGNORED
   * - headdings    - "=", "==", ... or "======"
   * - list items   - ";", ":", "#", "*" or possible combination of them.
   * - hairline     - "-
   * - table [unimplemented yet]:
   *   - caption    - "|+"
   *   - data       - "|"
   *   - headding   - "!"
   * - division     - "D" [experimental]
   * - blockquote   - "Q"
   * - center       - "C" [experimental]
   *
   * Methods:
   *   None.
   *
   */
  function Block(wtag, texts)
  {
    this.index = index;
    this.wtag = wtag;
    if (texts)
      this.texts = texts;
    
    var blen = blocks.length;
    if (blen) {
      this.prev = blocks[blen - 1];
      blocks[blen - 1].next = this;
    }
    else
      this.prev = null;
    this.next = null;
    
    blocks.push(this);
  }

  /***
   *** Texts()
   ***
   *
   * Parsed text fragments in a paragraph (block).
   *
   * Properties:
   *   ''n'':           a text node object of ''n''-th fragment.
   *   length:          current number of text fragments.
   *   indexes[''n'']: 0-based index of ''n''-th fragment in paragraph.
   *   lengths[''n'']: length of ''n''-th fragment.
   *   plain:           concatenated all fragments in paragraph.
   *
   * Methods:
   *   append(n):      add a text node to Texts object.
   *   concat(t):      append other Texts object to Texts object.
   *
   */
  function Texts()
  {
    this.length = 0;
    this.indexes = new Array();
    this.lengths = new Array();
    this.plain = "";
    
    this.append = function(n)
      {
        var text;
/*
        if (n.nodeType == 1)
          switch (n.nodeName.toLowerCase()) {
          case "img":
          case "object":
          case "iframe":
            text = "\uFFFC"; // OBJECT REPLACEMENT CHARACTER
            break;
          
          default:
            debug("Unexpected element (1) " + n.nodeName, 2);
            break;
          }
        else
*/
          text = n.nodeValue;
        if (typeof text != "string")
          text = "";
        
        this.plain += text;
        this.lengths.push(text.length);
        if (this.length)
          this.indexes.push(this.indexes[this.length - 1] + this.lengths[this.length - 1]);
        else
          this.indexes.push(0);
        this[this.length] = n;
        
        this.length++;
      };
    
    this.concat = function(t)
      {
        for (var i = 0; i < t.length; i++)
          this.append(t[i]);
      };
  }

  /***
   *** parsePreview()
   ***
   *
   * Parse Preview.
   *
   * Get “blocks” from the preview then accumlate into an array
   * blocks.
   */
  function parsePreview()
  {
    // get preview.
    var preview = document.getElementById("wikiPreview");
    if (!preview ||
        !preview.hasChildNodes || !preview.childNodes.length ||
        preview.childNodes.length == 1 && preview.firstChild.nodeType != 1)
      return false;
    
    // parse block-level nodes.
    var afterNote = false;
    var node = preview.firstChild;
    while (node) {
      // skip script or style elements.
      if (isMeta(node))
        ;
      // skip anchors.
      else if (isAutoAnchor(node))
        ;
      // skip non-block.
      else if (isIgnorable(node))
        ;
      else if (!isBlock(node))
        debug("a top-level non-empty non-block is found: " +
              index + " (" + node.nodeType + ") (" + node.nodeName + ") " +
              node.nodeValue, 2);
      // skip previewnote.
      else if (!afterNote &&
               (node.getAttribute("class") == "previewnote" ||
                node.getAttribute("className") == "previewnote"))
        afterNote = true;
      else if (afterNote) {
        // skip last empty paragraph.
        if (!node.nextSibling && isEmptyParagraph(node))
          break;
        
        // otherwise, insert candidate areas,
        var sideNote = document.createElement("div");
        sideNote.id = "AnansiSideNote-" + index;
        sideNote.setAttribute("class", "AnansiSideNote");
        sideNote.setAttribute("className", "AnansiSideNote");
        sideNote.style.display = "none";
        // XXX sideNote.appendChild(document.createElement("ul"));
        preview.insertBefore(sideNote, node);
        // ...then do parse.
        parseBlock(node);
        index++;
      }
      node = node.nextSibling;
    }
    
    if (!afterNote) {
      debug("no previewnote were found.", 2);
      return false;
    }
    else
      return true;
  }

  /***
   *** parse one block-level node.
   ***/
  function parseBlock(node, wtag)
  {
    if (!wtag)
      wtag = "";
    
    var name = node.nodeName.toLowerCase();
    switch (name) {
    case "blockquote":
      parseBCDBlock(node, wtag, "Q");
      break;
    
    case "center":
      parseBCDBlock(node, wtag, "C"); // experimental
      break;
    
    case "div":
      parseBCDBlock(node, wtag, "D"); // experimental
      break;
      
    case "h1":
    case "h2":
    case "h3":
    case "h4":
    case "h5":
    case "h6":
    case "p":
      var wt;
      if (name == "p")
        wt = "\n";
      else {
        wt = "";
        for (var i = 0; i < eval(name.charAt(1)); i++) 
          wt = "=";
      }
      var texts = new Texts();
      var n = node.firstChild;
      while (n) {
        texts.concat(parseInline(n));
        n = n.nextSibling;
      }
      new Block(wtag + wt, texts);
      break;

    case "pre":
      /* IGNORE preformatted block */
      break;
      
    case "hr":
      new Block(wtag + "-");
      break;
      
    case "dl":
    case "ol":
    case "ul":
      parseList(node, wtag);
      break;
      
    case "table":
      if (node.id == "toc")
        break;
      // TODO: parse table recursively.
      break;
      
    default:
      debug("unknown block element: index=" + index +
            "; name=" + node.nodeName, 2);
      break;
    }
  }

  /***
   *** parse blockquote, center or div node recursively.
   ***/
  function parseBCDBlock(node, wtag, mywtag)
  {
    if (!wtag)
      wtag = "";
    var texts = new Texts();

    function flushTexts(wtag)
    {
      if (texts.length) {
        new Block(wtag, texts);
        texts = new Texts();
      }
    }
    
    /* start here. */
    
    var n = node.firstChild;
    while (n) {
      // skip script or style elements.
      if (isMeta(n))
        ;
      // skip anchors.
      else if (isAutoAnchor(n))
        ;
      // skip non-block.
      else if (isIgnorable(n))
        ;
      // parse another sub-level block.
      else if (isBlock(n)) {
        flushTexts(wtag + mywtag);
        parseBlock(n, wtag + mywtag);
      }
      // otherwise, child will be (a part of) anonymous block.
      else
        texts.concat(parseInline(n));

      n = n.nextSibling;
    }

    flushTexts(wtag + mywtag);
  }

  /***
   *** parse list node recursively.
   ***/
  function parseList(node, wtag)
  {
    if (!wtag)
      wtag = "";
    var texts = new Texts();
    
    function flushTexts(wtag)
    {
      if (texts.length) {
        new Block(wtag, texts);
        texts = new Texts();
      }
    }
    
    function parseListItem(node, wtag)
    {
      var n = node.firstChild;
      while (n) {
        // skip script or style elements.
        if (isMeta(n))
          ;
        // skip anchors.
        else if (isAutoAnchor(n))
          ;
        // skip non-block
        if (isIgnorable(n))
          ;
        // parse sub-level list.
        else if (isList(n)) {
          flushTexts(wtag);
          parseList(n, wtag);
        }
        // parse another sub-level block (maybe division).
        else if (isBlock(n)) {
          flushTexts(wtag);
          parseBlock(n, wtag);
        }
        // otherwise, child will be (a part of) anonymous block.
        else
          texts.concat(parseInline(n));
        
        n = n.nextSibling;
      }
      
      flushTexts(wtag);
    }
    
    /* start here. */
    var n = node.firstChild;
    while (n) {
      if (isIgnorable(n))
        ;
      else if (n.nodeType == 1) { // elements
        switch (n.nodeName.toLowerCase()) {
        case "dd":
          flushTexts(wtag);
          parseListItem(n, wtag + ":");
          break;
          
        case "dt":
          flushTexts(wtag);
          parseListItem(n, wtag + ";");
          break;
          
        case "li":
          switch (node.nodeName.toLowerCase()) {
          case "ol":
            flushTexts(wtag);
            parseListItem(n, wtag + "#");
            break;
            
          case "ul":
            flushTexts(wtag);
            parseListItem(n, wtag + "*");
            break;
            
          default:
            debug("Unknown parent node of list item: index=" + index +
                  " name=" + node.nodeName, 2);
            break;
          }
          break;
          
        case "dl":
        case "ol":
        case "ul":
          flushTexts(wtag);
          parseList(n, wtag);
          break;
          
        default:
          debug("Unknown list item: index=" + index +
                " name=" + n.nodeName, 2);
          break;
        }
      }
      
      n = n.nextSibling;
    }
    
    flushTexts(wtag);
  }

  /***
   *** parseInline(node)
   ***
   *
   * parse one node containing inline node(s) only.
   */
  function parseInline(node)
  {
    var texts = new Texts();
    switch (node.nodeType) {
    case 1: // element node (assumed to be inline-level).
      if (isBlock(node))
        debug("unexpected block element: name=" + node.nodeName, 2);
      else if (isIgnorable(node))
        ;
      else if (isOmittable(node))
        ;
      else
        switch (node.nodeName.toLowerCase()) {
        /* replacements. */
        case "img":
        case "object":
        // case "iframe":
          texts.append(node);
          break;

        default:
          var n = node.firstChild;
          while (n) {
            texts.concat(arguments.callee(n));
            n = n.nextSibling;
          }
          break;
        }
      break;
      
    case 3: // text node.
      texts.append(node);
      break;
      
    case 8: // comment node.
      break;
      
    default:
      debug("unknown nodetype: " + node.nodeType, 2);
      break;
    }
    return texts;
  }


  /****
   **** Proofreader Functions.
   ****/

  /***
   *** Result(rule, block, index, length)
   ***
   *
   * Result object.
   *
   * Properties:
   *   id:          Rule ID.
   *   rule:        Rule object.
   *   block:       Current block.
   *   index:
   *   length:
   *   string:      Substring matched by rule.match.
   *   seq:          0-based (global) sequencial number of match.
   *   subindex:
   *   sublength:
   *   substring:   A fragment of string above.
   *   subseq:      0-based sequencial number of fragments.
   *   candidate:   candidate(s): given by user-defined Rule.replace() callback via apply().
   *   instruction: instruction: given by user-defined Rule.replace() callback via apply().
   *
   * Methods:
   *   Constructor:   Create object based on rule-string (see above).
   *   shift(length): Create object based on substring of ''text''.
   *   apply():        Apply Rule.replace() with ''this'' context as this object.
   */

  function Result(rule, block, index, length)
  {
    // update seq
    this.seq = seq++;
    //
    this.rule = rule;
    this.id = rule.id;
    this.text = block.texts.plain;
    this.subindex = this.index = index;
    this.sublength = this.length = length;
    this.substring = this.string = this.text.slice(index, index + length);
    
    // init subseq
    this.subseq = 0;
    
    // init results.
    this.candidate = new Array();
    this.instruction = new Array();

    /**
     ** shift(sublength)
     **
     *
     * bite off a substring.
     */

    this.shift = function(sublength) {
      var Res = function(){};
      Res.prototype = this;
      var res = new Res;
      
      res.subindex = this.subindex;
      res.sublength = sublength;
      res.substring = this.text.slice(this.subindex, this.subindex + sublength);
      res.subcandidate = null;
      res.subinstruction = null;
      
      this.subindex += sublength;
      this.sublength = (this.index + this.length) - this.subindex;
      this.substring = this.text.slice(this.subindex, this.subindex + this.sublength);
      
      // update subseq
      res.subseq = this.subseq++;
      
      return res;
    };

    /**
     ** apply()
     **
     *
     * apply rule.
     */

    this.apply = function() {
      var rule = this.rule;
      var replace = rule.replace;
      if (typeof replace == "undefined" || replace === null)
        ;
      else if (typeof replace == "function") {
        delete this.rule; // user should not access to internal object.
        try {
          replace.call(this);
          
          if (this.hasOwnProperty("instruction")) {
            debug(this.id + ": don't assign values directly to instructioin.  Use push().", 1);
            delete this.instruction;
          }
          if (this.hasOwnProperty("candidate")) {
            debug(this.id + ": don't assign values directly to candidate.  Use push().", 1);
            delete this.candidate;
          }
        } catch (e) {
          debug(e.name + ": " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", 1);
          while (this.candidate.length)
            this.candidate.pop();
          while (this.instruction.length)
            this.instruction.pop();
        }
        this.rule = rule;
      }
      else if (typeof replace == "string")
        this.instruction.push(replace);
      else // should be an Array.
        for (var i = 0; i < replace.length; i++)
          this.candidate.push(replace[i]);
    };
  }

  /***
   *** markBlocks()
   ***
   *
   * Mark all parsed blocks.
   */
  function markBlocks()
  {
    var block = blocks[0];
    while (block) {
      markBlock(block);
      block = block.next;
    }
  }

  /***
   *** markBlock(block)
   ***
   *
   * Mark one parsed block.
   */
  function markBlock(block)
  {
    /*
     * Check if two text ranges are overlapped.
     * Return value:
     *     0: overlapped.
     *     1/-1: not overlapped.
     */
    function compareRanges(xindex, xlength, yindex, ylength)
    {
      if (xindex == yindex && xlength == 0 && ylength == 0)
          // zero-width ranges
          return 0;
      else if (xindex <= yindex) {
        if (xindex + xlength <= yindex)
          return -1;
        else
          return 0;
      }
      else if (yindex + ylength <= xindex)
        return 1;
      else
        return 0;
    }
    
    // debugging output
    var s = block.index;
    s  += " (" + block.wtag + ")";
    if (block.texts)
      s += " " + block.texts.plain;
    s += "\n";
    debug(s);

    if (!block.texts)
      return;
    if (!rules || !rules.length)
      return;
    
    var plain = block.texts.plain;
    
    /**
     ** do match.
     **/
    var results = new Array();
    var len = rules.length;
    for (var i = 0; i < len; i++) {
      var rule = rules[i];
      
      // skip disabled rule.
      if (!rule.enabled)
        continue;
      
      // skip rules without regexps.
      var match = rule.match;
      if (!match || !match.length)
        continue;
      
      var mlen = match.length;
      for (var j = 0; j < mlen; j++) {
        var re = match[j];
        if (!re) // skip disabled RegExp
          next;
        
        try {
          re.lastIndex = 0;
          var groups;
          while (groups = re.exec(plain)) {
            var index = groups.index + groups[1].length;
            var length = groups[2].length;
            
            // insert matched range if it doesn't overlap with other ranges.
            var k;
            for (k = results.length; k; k--) {
              var c = compareRanges(index, length,
                                    results[k-1].index, results[k-1].length);
              if (c == 0) { // overlapped
                k = -1;
                break;
              }
              else if (c == 1)
                break;
            }
            if (0 <= k) {
              var result = new Result(rule, block, index, length);
              results.splice(k, 0, result);
            }
          }
        } catch (e) { // catch RegExp error
          // report error.
          debug(e.name + ": " + e.message +
                " (" + e.fileName + ":" + e.lineNumber + ") " +
                " (" + rules[i].id + ") “" +
                rules[i].match[j].source + "”", 1);
          rules[i].match[j] = null; // disable errorneous RegExp.
        }
      }
    }
    
    if (!results.length)
      return;
    
    /**
     ** split matched substrings then replace with decorated ones.
     **/
    var plain = block.texts.plain;
    var rlen = results.length;
    var tlen = block.texts.length;
    var previndex;
    var ti, tindex, tlength, ri, rindex, rlength, res, frag;
    
    ti = ri = 0;
    for ( ; ti < tlen; ti++) {
      tindex = block.texts.indexes[ti];
      tlength = block.texts.lengths[ti];
      
      res = new Array();
      previndex = tindex;
      for ( ; ri < rlen; ri++, seq++) {
        rindex = results[ri].subindex;
        rlength = results[ri].sublength;
        if (tindex + tlength <= rindex)
          break;
        
        if (previndex < rindex)
          res.push(plain.slice(previndex, rindex));
        else if (rindex < previndex)
          debug("unknown situation: [" + index + "] reindex=" + rindex +
                 " previndex=" + previndex, 2); 
        previndex = rindex;
        
        if (rindex + rlength <= tindex + tlength) {
          res.push(results[ri].shift(rlength));
          previndex = rindex + rlength;
          if (results[ri].sublength != 0)	 
            debug("unknown situation: [" + index + "] results[" + ri +	 
            "].sublength=" + results[ri].sublength, 2);
          continue; // step over next text fragment.
        }
        else {
          res.push(results[ri].shift(tindex + tlength - rindex));
          previndex = tindex + tlength;
          break; // continue with remainder of current text fragment.
        }
      } /* for ( ; ri < rlen; ri++) */
      
      if (previndex < tindex + tlength)
        res.push(plain.slice(previndex, tindex + tlength));
      else if (tindex + tlength < previndex)
        debug("unknown situation: [" + index + "] previndex=" + previndex, 2); 
      
      /*
       * replace text fragment...
       */
      if (!res.length)
        ;
      else if (res.length == 1 && typeof res[0] == "stirng")
        ;
      else
        replaceBlockText(block.texts[ti], res, plain, block.index);
      
    } /* for ( ; ti < tlen; ti++) */
    
    /**
     ** append remainders.
     **/
    if (ri < rlen) {
      var last;
      if (block.texts.length)
        last = block.texts[block.texts.length - 1];
      else
        return; // FIXME: how about the block without children?
      
      previndex = plain.length;
      for ( ; ri < rlen; ri++, seq++) {
        rindex = results[ri].subindex;
        rlength = results[ri].sublength;
        
        if (rindex == previndex) {
          appendBlockText(last, results[ri], plain, block.index);
          previndex += rlength;
        }
        else
          debug("Beyond the text boundary: "+ rindex + "+" + rlength, 2);
      }
    }
  }

  /***
   *** replaceBlockText(node, frags, plain, blockIndex)
   ***
   *
   * replace fragment with marked nodes.
   */
  function replaceBlockText(node, frags, plain, blockIndex)
  {
    var sideNote = document.getElementById("AnansiSideNote-" + blockIndex);
    // incorrect logic?
    if (!sideNote) {
      debug("Unknown sidenote for candidates: " + blockIndex, 2);
      return;
    }
    
    var parent = node.parentNode;
    
    var len = frags.length;
    for (var i = 0; i < len; i++) {
      var n = createReplacement(sideNote, frags[i], plain, blockIndex);
      // FIXME: might be inserted into upper-most ancester node.
      parent.insertBefore(n, node);
    }
    parent.removeChild(node);
  }

  /***
   *** appendBlockText(node, frags, plain, blockIndex)
   ***
   *
   * Special case: append marked nodes at end of paragraph (block).
   */
  function appendBlockText(node, frags, plain, blockIndex)
  {
    var sideNote = document.getElementById("AnansiSideNote-" + blockIndex);
    // incorrect logic?
    if (!sideNote) {
      debug("Unknown sidenote for candidates: " + blockIndex, 2);
      return;
    }
    
    var next = node.nextSibling;
    var parent = node.parentNode;
    
    for (var i = 0; i < frags.length; i++) {
      var n = createReplacement(sideNote, frags[i], plain, blockIndex);
      if (next)
        parent.insertBefore(n, next);
      else if (parent)
        parent.appendChild(n);
      else {
        debug("Unknown fragment node", 2);
        return;
      }
    }
  }

  /***
   *** createReplacement(sideNote, frag, plain, blockIndex)
   ***
   *
   * decorate declined fragment then insert replacement candidates to side note.
   */
  var lineSpaceRe = new RegExp("^[ \r\n]*$");
  function createReplacement(sideNote, frag, plain, blockIndex)
  {
    var klass, replId, repl;
    
    // raw text won't be replaced.
    if (typeof frag == "string")
      return document.createTextNode(frag);
    
    // compute candidate(s).
    frag.apply();
    
    var candidate = frag.candidate;
    var instruction = frag.instruction;
    if (!candidate.length && !instruction.length) // no fixes needed.
      return document.createTextNode(frag.substring);
    
    // text decoration.
    if (!frag.subseq)
      replId = "AnansiDeclined-" + frag.seq;
    else
      replId = "AnansiDeclined-" + frag.seq + "-" + frag.subseq;
    klass = "AnansiFixS" + frag.id;
    
    if (frag.length == 0) {
      repl = document.createElement("ins");
      repl.id = replId;
      
      if (candidate.length == 1 && candidate[0] == " ") { // narrow space
        repl.setAttribute("class", klass + " AnansiInsertSPNa");
        repl.setAttribute("className", klass + " AnansiInsertSPNa");
        repl.appendChild(document.createTextNode("\u2002"));

        candidate = new Array();
        if (!instruction.length)
          instruction = new Array("空ける");
      }
      else if (candidate.length == 1 && candidate[0] == " ") { // wide space
        repl.setAttribute("class", klass + " AnansiInsertSP");
        repl.setAttribute("className", klass + " AnansiInsertSP");
        repl.appendChild(document.createTextNode("\u2003"));

        candidate = new Array();
        if (!instruction.length)
          instruction = new Array("空ける");
      }
      else {
        repl.setAttribute("class", klass + " AnansiInsert");
        repl.setAttribute("className", klass + " AnansiInsert");
        repl.appendChild(document.createTextNode("\u2003"));
      }
    }
    else if (candidate.length == 1 && candidate[0] == "") {
      repl = document.createElement("del");
      repl.id = replId;
      
      switch (frag.substring) {
      case " ":       // narrow spaces
      case "\u00A0":
      case "\u2000":
      case "\u2002":
      case "\u2004":
      case "\u2005":
      case "\u2006":
      case "\u2007":
      case "\u2008":
      case "\u2009":
      case "\u200A":
        repl.setAttribute("class", klass + " AnansiRemoveSPNa");
        repl.setAttribute("className", klass + " AnansiRemoveSPNa");
        
        candidate = new Array();
        if (!instruction.length)
          instruction = new Array("詰める");
        repl.appendChild(document.createTextNode("\u2002"));
        break;
      
      case "\u2001": // wide spaces
      case "\u2003":
      case " ":
        repl.setAttribute("class", klass + " AnansiRemoveSP");
        repl.setAttribute("className", klass + " AnansiRemoveSP");
        
        candidate = new Array();
        if (!instruction.length)
          instruction = new Array("詰める");
        repl.appendChild(document.createTextNode("\u2003"));
        break;
      
      default:
        if (lineSpaceRe.test(plain) && frag.substring == plain) { // line space
          repl.setAttribute("class", klass + " AnansiRemoveLS");
          repl.setAttribute("className", klass + " AnansiRemoveLS");
          
          candidate = new Array();
          if (!instruction.length)
            instruction = new Array("行を詰める");
          repl.appendChild(document.createTextNode("\u2003"));
        }
        else { // others; remove texts
          repl.setAttribute("class", klass + " AnansiDeclined");
          repl.setAttribute("className", klass + " AnansiDeclined");
          
          candidate = new Array();
          if (!instruction.length)
            instruction = new Array("取る詰め");
          repl.appendChild(document.createTextNode(frag.substring));
        }
        break;
      }
    }
    else {
      repl = document.createElement("del");
      repl.id = replId;
      
      repl.setAttribute("class", klass + " AnansiDeclined");
      repl.setAttribute("className", klass + " AnansiDeclined");
      repl.appendChild(document.createTextNode(frag.substring));
    }
    // Set event handler.
    addHandler(repl, "resize", drawLine);
    addHandler(repl, "mouseover", emphLine);
    addHandler(repl, "mouseout", unemphLine);
    
    // append candidate(s) and instructions to side note.
    var list = sideNote.firstChild;
    if (!list) {
      list = document.createElement("ul");
      sideNote.appendChild(list);
    }
    
    var g = document.createElement("li");
    klass = "AnansiFixD" + frag.id;
    if (instruction.length) {
      g.id = "AnansiInstruction-" + frag.seq;
      g.setAttribute("class", klass + " AnansiInstruction");
      g.setAttribute("className", klass + " AnansiInstruction");
      for (var j = 0; j < instruction.length; j++) {
        if (j)
          g.appendChild(document.createTextNode(" "));
        g.appendChild(document.createTextNode(instruction));
      }
    }
    if (candidate.length) {
      g.id = "AnansiCandidateGroup-" + frag.seq;
      g.setAttribute("class", "AnansiCandidateGroup");
      g.setAttribute("className", "AnansiCandidateGroup");
      for (var j = 0; j < candidate.length; j++) {
        if (j)
          g.appendChild(document.createTextNode(" "));
        var c = document.createElement("ins");
        if (j)
          c.id = "AnansiCandidate-" + frag.seq + "-" + j;
        else
          c.id = "AnansiCandidate-" + frag.seq;
        
        c.setAttribute("class", klass + " AnansiCandidate");
        c.setAttribute("className", klass + " AnansiCandidate");
        c.appendChild(document.createTextNode(candidate[j]));
        g.appendChild(c);
      }
    }
    
    // add link to description page(s).
    var desc = null;
    for (var i = 0; i < rules.length; i++) {
      if (rules[i].id == frag.id) {
        desc = rules[i].description;
      }
    }
    if (desc) {
      g.appendChild(document.createTextNode(" "));
      for (var di = 0; di < desc.length; di++) {
        var link = document.createElement("a");
        link.setAttribute("href",
                            wgArticlePath.replace("$1",
                                                     encodeURIComponent(desc[di].replace(/ /g, "_"))));
        link.setAttribute("target", "_blank");
        var img = document.createElement("img");
        img.setAttribute("width", "16");
        img.setAttribute("height", "16");
        img.setAttribute("border", "0");
        img.setAttribute("alt", "説明");
        img.setAttribute("src", ICON_QUESTION);
        link.appendChild(img);
        g.appendChild(link);
      }
    }
    // Set event handler.
    addHandler(g, "resize", drawLine);
    addHandler(g, "mouseover", emphLine);
    addHandler(g, "mouseout", unemphLine);
    
    list.appendChild(g);
    sideNote.style.display = "";
    
    return repl;
  }

  /****
   **** Event Handler: Line Drawer.
   ****/

  /***
   *** drawLine()
   ***
   *
   */

  function drawLine()
  {
    if (!platform.graphicFormat)
      return;
    if (config.lineshape.toLowerCase() == "none")
      return;
    
    var elm;
    if (platform.name == "MSIE" && !this.id) // MSIE kludge.
      elm = event.srcElement;
    else
      elm = this;
    
    var parent = elm.offsetParent;
    if (!parent)
      return;
    
    var container = document.getElementById("AnansiLines-" + parent.id);
    var root;
    if (!container) {
      container = document.createElement("div");
      container.id = "AnansiLines-" + parent.id;
      container.style.position = "absolute";
      container.style.left = 0;
      container.style.top = 0;
      
      switch (platform.graphicFormat) {
      case "SVG":
        root = document.createElementNS(SVGNS, "svg");
        root.setAttribute("xmlns", SVGNS);
        root.setAttribute("version", SVGVER);
        break;
      
      case "VML":
        root = document.createElement("v:group");
        var rect = document.createElement("v:rect");
        root.appendChild(rect);
        break;
      }
      
      container.appendChild(root);
      parent.appendChild(container);
      
      // set background-color.
      container.style.zIndex = -1;
      var bgColor = getComputedElementStyle(parent).backgroundColor || "white";
      switch (platform.graphicFormat) {
      case "SVG":
        container.style.backgroundColor = bgColor;
        break;
      
      case "VML":
        container.style.backgroundColor = bgColor;
        var rect = root.firstChild;
        rect.setAttribute("strokecolor", bgColor);
        rect.setAttribute("fillcolor", bgColor);
        /* rect.setAttribute("opacity", 0); */
        break;
      }
      parent.style.backgroundColor = "transparent";
    }
    else
      root = container.firstChild;
    
    // fix size of viewbox.
    switch (platform.graphicFormat) {
    case "SVG":
      root.setAttribute("width", parent.offsetWidth);
      root.setAttribute("height", parent.offsetHeight);
      root.setAttribute("viewBox", "0 0 " + parent.offsetWidth + " " + parent.offsetHeight);
      break;
    
    case "VML":
      root.style.width = parent.offsetWidth;
      root.style.height = parent.offsetHeight;
      root.setAttribute("coordorigin", "0 0");
      root.setAttribute("coordsize", parent.offsetWidth + " " + parent.offsetHeight);
      var rect = root.firstChild;
      rect.style.width = parent.offsetWidth;
      rect.style.height = parent.offsetHeight;
      break;
    }
    
    // Get (or create if it wasn't exist) elements for line drawing.
    getLineElements(elm, root);
  }

  /***
   *** drawAllLines()
   ***
   *
   */
  function drawAllLines()
  {
    var elms = document.getElementsByTagName("*");
    var elen = elms.length;
    for (var i = 0; i < elen; i++)
      if (elms[i].id.indexOf("AnansiDeclined-") == 0)
        drawLine.call(elms[i]);
  }

  /***
   *** emphLine()
   ***
   *
   */
  function emphLine()
  {
    if (!platform.graphicFormat)
      return;
    if (config.lineshape.toLowerCase() == "none")
      return;
    
    var elm;
    if (platform.name == "MSIE" && !this.id) // MSIE kludge.
      elm = event.srcElement;
    else
      elm = this;
    
    drawLine.call(elm);
    var elms = getLineElements(elm);
    if (!elms[2])
      return;
    
    elms[1].style.fontWeight = FONTWEIGHT_EMPH;
    switch (platform.graphicFormat) {
    case "SVG":
      elms[2].parentNode.setAttribute("stroke-width", STROKEWIDTH_EMPH);
      break;
    
    case "VML":
      elms[2].parentNode.setAttribute("strokeweight", STROKEWIDTH_EMPH);
      break;
    }
  }

  /***
   *** unemphLine()
   ***
   *
   */
  function unemphLine()
  {
    if (!platform.graphicFormat)
      return;
    if (config.lineshape.toLowerCase() == "none")
      return;
    
    var elm;
    if (platform.name == "MSIE" && !this.id) // MSIE kludge.
      elm = event.srcElement;
    else
      elm = this;
    
    drawLine.call(elm);
    var elms = getLineElements(elm);
    if (!elms[2])
      return;
    
    elms[1].style.fontWeight = "";
    switch (platform.graphicFormat) {
    case "SVG":
      elms[2].parentNode.setAttribute("stroke-width", STROKEWIDTH_NORMAL);
      break;
    
    case "VML":
      elms[2].parentNode.setAttribute("strokeweight", STROKEWIDTH_NORMAL);
      break;
    }
  }

  /***
   *** getLineElements(elm, root)
   ***
   *
   * utility function.
   */
  function getLineElements(elm, root)
  {
    if (!platform.graphicFormat)
      return;
    if (config.lineshape.toLowerCase() == "none")
      return;
    
    var pixelRe = new RegExp("^([0-9]+(\\.[0-9]+)?)(px)?$", "i");
    function pixelToNumber(s)
    {
      var m = pixelRe.exec(s);
      if (m)
        return eval(m[1]);
      else
        return null;
    }
    
    /* start here. */
    
    // get decorated element on preview.
    var seq, sElm, dElm, g, path;
    seq = elm.id.split("-")[1];
    sElm = document.getElementById("AnansiDeclined-" + seq);
    dElm = document.getElementById("AnansiInstruction-" + seq) ||
           document.getElementById("AnansiCandidateGroup-" + seq);
    if (!sElm || !dElm)
      return new Array();
    
    g = document.getElementById("AnansiLine-" + seq);
    if (!g) {
      switch (platform.graphicFormat) {
      case "SVG":
        g = document.createElementNS(SVGNS, "g");
        g.setAttribute("stroke", "red");
        g.setAttribute("stroke-width", STROKEWIDTH_NORMAL);
        g.setAttribute("fill", "none");
        path = document.createElementNS(SVGNS, "path");
        break;
      
      case "VML":
        g = document.createElement("v:shape");
        g.style.position = "absolute";
        g.style.left = 0;
        g.style.top = 0;
        g.style.width = 1;
        g.style.height = 1;
        g.setAttribute("coordorigin", "0 0");
        g.setAttribute("coordsize", "1 1");
        g.setAttribute("strokecolor", "red");
        g.setAttribute("strokeweight", STROKEWIDTH_NORMAL);

        path = document.createElement("v:path");
        break;
      }
      g.id = "AnansiLine-" + seq;
      g.appendChild(path);

      if (platform.graphicFormat == "VML") {
        var fill = document.createElement("v:fill");
        fill.setAttribute("opacity", "0");
        g.appendChild(fill);
      }

      if (root)
        root.appendChild(g);
    }
    else
      path = g.firstChild;
    
    var sLeft = pixelToNumber(sElm.offsetLeft);
    var sTop = pixelToNumber(sElm.offsetTop);
    var dLeft = pixelToNumber(dElm.offsetLeft);
    var dTop = pixelToNumber(dElm.offsetTop);

    // on MSIE, offset parent of nodes is side note etc, not content division.
    var sParent = sElm.offsetParent;
    while (sParent && sParent.id != "content") {
        sLeft += pixelToNumber(sParent.offsetLeft);
        sTop += pixelToNumber(sParent.offsetTop);
        sParent = sParent.offsetParent;
    }
    var dParent = dElm.offsetParent;
    while (dParent && dParent.id != "content") {
        dLeft += pixelToNumber(dParent.offsetLeft);
        dTop += pixelToNumber(dParent.offsetTop);
        dParent = dParent.offsetParent;
    }

    if (!sLeft || !sTop || !dLeft || !dTop) {
      debug("couldn't get offset geometries of source/dest element(s).", 2);
      return new Array();
    }
/*
if (platform.name == "MSIE") debug("s.parent="+sElm.offsetParent.id+";sLeft="+sLeft+";sTop="+sTop+";d.parent="+dElm.offsetParent.id+";dLeft="+dLeft+";dTop="+dTop+";", 1);
 */
    var sStyle = getComputedElementStyle(sElm);
    var dStyle = getComputedElementStyle(dElm);
    var sFontSize = pixelToNumber(sStyle.fontSize) || 12;
    var dFontSize = pixelToNumber(dStyle.fontSize) || 12;
    var sLineHeight = pixelToNumber(sStyle.lineHeight) || 16;
    var dLineHeight = pixelToNumber(dStyle.lineHeight) || 16;
    var pathExpr;
    if (sTop < dTop)
      pathExpr = "M " + parseInt(sLeft + sFontSize / 2) + "," + 
                  parseInt(sTop + sFontSize) +
                  " L " + parseInt(sLeft + sFontSize / 2) + "," +
                  parseInt(sTop + (sLineHeight + sFontSize) / 2) +
                  " L " + parseInt(dLeft - dFontSize * 3 / 2) + "," +
                  parseInt(dTop + dLineHeight / 2) +
                  " L " + parseInt(dLeft - dFontSize / 2) + "," +
                  parseInt(dTop + dLineHeight / 2);
    else if (dLeft < sLeft)
      pathExpr = "M " + parseInt(sLeft + sFontSize / 2) + "," + 
                  parseInt(sTop) +
                  " L " + parseInt(sLeft + sFontSize / 2) + "," +
                  parseInt(sTop - (sLineHeight - sFontSize) / 2) +
                  " L " + parseInt(dLeft - dFontSize / 2) + "," +
                  parseInt(dTop + dLineHeight) +
                  " L " + parseInt(dLeft - dFontSize / 2) + "," +
                  parseInt(dTop + dLineHeight / 2);
    else
      pathExpr = "M " + parseInt(sLeft + sFontSize / 2) + "," + 
                  parseInt(sTop) +
                  " L " + parseInt(sLeft + sFontSize / 2) + "," +
                  parseInt(sTop - (sLineHeight - sFontSize) / 2) +
                  " L " + parseInt(dLeft - dFontSize * 3 / 2) + "," +
                  parseInt(dTop + dLineHeight / 2) +
                  " L " + parseInt(dLeft - dFontSize / 2) + "," +
                  parseInt(dTop + dLineHeight / 2);
    switch (platform.graphicFormat) {
    case "SVG":
      path.setAttribute("d", pathExpr);
      break;
    
    case "VML":
      path.setAttribute("v", pathExpr.toLowerCase() + " e");
      break;
    }
    
    return new Array(sElm, dElm, path);
  }


  /****
   **** Rule Manipulater functions.
   ****/

  /***
   *** Rule object
   ***
   *
   * Definitions for prototype of proofreading rule.
   *
   * Properties:
   *
   *   id:      unique id (name of this rule).
   *   match:   array of (one or more) RegExp object(s).
   *   pattern: array of triplets of pattern.
   *   replace: replacement candidate(s).
   *   desc: 
   *   enabled: enable this rule;
   *
   * Methods:
   *   register(): register this rule.
   *
   * where the source of ''RegExp object'' should be formatted as:
   *
   *   (''previous'')(''to be replaced'')(?=''following'')
   *
   * - ''previous'' pattern is assumed not to contain grouping “(...)”.
   *   Instead, use “(?:...)”.
   * - ''to be replaced'' pattern will be matched with that may be replaced.
   * - ''following'' pattern is applied as “lookahead assersion”.
   *
   * replacement candidate(s) may be an array of static strings,
   * or may be a Function object (see function Result()):
   *
   */
  var idRe = new RegExp("^[A-Z][0-9A-Z]*$", "i");
  function Rule()
  {
    // this.id;
    // this.replace;
    // this.description;
    // this.pattern;
    this.match = null;
    
    this.register = function() {
      function safeRegExp(pattern) {
        if (!pattern)
          return "";
        else if (typeof pattern == "string")
          return pattern;
        else if (pattern.source != undefined)
          return pattern.source;
        else // Unknown object.
          return pattern.toString();
      }
      
      if (!this.id)
        this.id = "Rule" + (rules.length + 1);
      else if (!idRe.test(this.id)) {
        debug(this.id + ": Bad rule ID", 1);
        return;
      }
      for (var i = 0; i < rules.length; i++)
        if (rules[i].id.toLowerCase() == this.id.toLowerCase()) {
          debug(this.id + ": Already registered.", 1);
          return;
        }
      
      if (typeof this.replace == "string")
        this.replace = new Array(this.replace);
      else if (typeof this.replace == "undefined" || this.replace === null) {
        debug(this.id + ": Replacement string(s) should be given for the rule.", 1);
        return;
      }
      
      if (typeof this.description == "string")
        this.description = [this.description];
      else if (!this.description || !this.description.length)
        this.description = new Array();
      
      var regexp;
      this.match = new Array();
      try {
        for (var i = 0; i + 2 < this.pattern.length; i += 3) {
          // Avoid grouping (...) in preceding pattern.
          regexp = "(" + safeRegExp(this.pattern[i]).replace(
                           /(\\.|\[\^?\]?(\\.|[^\\\]])*\]|\(\?.|\()/g,
                           function(m) {
                             if (m == "(")
                               return "(?:";
                             else
                               return m;
                           }
                         ) + ")";
          regexp += "(" + safeRegExp(this.pattern[i+1]) + ")";
          // Optional succeeding pattern.
          if (this.pattern[i+2])
            regexp += "(?=" + safeRegExp(this.pattern[i+2]) + ")";
          this.match.push(new RegExp(regexp, "g"));
        }
      } catch (e) {
        debug(this.id + ": " + e.name + ": " + e.message + " (" + regexp + ")",
              1);
        this.match = null;
        return;
      }
      if (!this.match || !this.match.length) {
        debug(this.id + ": No matching rules", 1);
        return;
      }

      if (typeof this.enabled == "undefined" || this.enabled === null)
        this.enabled = true;
      delete this.register;
      rules.push(this);
    }
  }

  /***
   *** Anansi.registerRule(definition)
   ***
   *
   */
  this.registerRule = function(definition) {
    if (!enableMe())
      return;
    
    definition.prototype = new Rule;
    try {
      (new definition).register();
    } catch (e) {
      debug(e.name + ": " + e.message + " (" + e.fileName + ":" + e.lineNumber + ")", 1);
    }
  };

  /***
   *** Anansi.enableRule(id)
   ***
   *
   */
  this.enableRule = function(id) {
    if (!enableMe())
      return;
    if (!idRe.test(id)) {
      debug(id + ": Bad rule ID: ", 1);
      return;
    }
    
    for (var i = 0; i < rules.length; i++)
      if (rules[i].id.toLowerCase() == id.toLowerCase())
        rules[i].enabled = true;
  }

  /***
   *** Anansi.disableRule(id)
   ***
   *
   */
  this.disableRule = function(id) {
    if (!enableMe())
      return;
    if (!idRe.test(id)) {
      debug(id + ": Bad rule ID: ", 1);
      return;
    }
    
    for (var i = 0; i < rules.length; i++)
      if (rules[i].id.toLowerCase() == id.toLowerCase())
        rules[i].enabled = false;
  }


  /****
   **** Debugger Functions.
   ****/

  /***
   *** debug(message, severity)
   ***
   *
   * show debug messages.
   *
   * severity:
   * - 0: for the purpose of internal debugging.
   * - 1: data format error (registered rules, config variables, ...).
   * - 2: failures or overlookings in programmed logic.
   * - 3: crash (syntax error, undefined object etc.).
   */
  var debugArea = null;
  function debug(message, severity)
  {
    if (!severity || severity < 0)
      severity = 0;
    if (severity < config.debuglevel)
      return;
    if (severity > 3)
      severity = 3;
    
    if (platform.name == "MSIE" &&
        platform.minVersion && platform.minVersion < 7) {
      // MSIE <=6 crashes when “fixed” position is set to element.
      // So we won't support internal debug message (severity = 0)
      // and will use alert box for other higher level messages.
      if (0 < severity)
        alert("<" + severity + "> " + message);
      return;
    }
    else if (!debugArea) {
      var node = document.createElement("div");
      node.id = "AnansiDebug";
      if (platform.name == "MSIE")
        node.setAttribute("className", "AnansiDebug");
      else
        node.setAttribute("class", "AnansiDebug");
      var buttonarea = document.createElement("div");
      buttonarea.style.textAlign = "right";
      var button = document.createElement("img");
      button.setAttribute("src", ICON_CLOSE);
      button.style.width = "14px";
      button.style.height = "14px";
      addHandler(button, "click",
                 function() {
                   document.getElementById("AnansiDebug").style.display = "none";
                 });
      buttonarea.appendChild(button);
      node.appendChild(buttonarea);
      debugArea = document.createElement("ul");
      debugArea.id = "AnansiDebugArea";
      debugArea.setAttribute("class", "AnansiDebugArea");
      debugArea.setAttribute("className", "AnansiDebugArea");
      
      if (platform.ua)
        debugArea.appendChild(document.createTextNode("; " + platform.ua.replace(/:/g, "&#58;")));
      else
        debugArea.appendChild(document.createTextNode("; (name and version of your browser)"));
      debugArea.appendChild(document.createTextNode(": [{{fullurl:" + mw.config.get('wgPageName') +
                                                          "|oldid=" + wgCurRevisionId + "}} " +
                                                          mw.config.get('wgPageName') + "]"));
      debugArea.appendChild(document.createTextNode(": Detected&#58; " + platform.name + " " +
                                                          platform.minVersion));
      debugArea.appendChild(document.createElement("br"));
      
      node.appendChild(debugArea);
      if (platform.name == "MSIE") {
        var preview = document.getElementById("wikiPreview");
        if (preview)
          preview.insertBefore(node, preview.firstChild);
      }
      else
        document.getElementsByTagName("body")[0].appendChild(node);
    }
    
    var msgnode = document.createElement("li");
    var klass = "AnansiDebugMessage";
    if (severity > 0)
      klass += " AnansiSeverity" + severity;
    msgnode.setAttribute("class", klass);
    msgnode.setAttribute("className", klass);
    msgnode.appendChild(document.createTextNode(message));
    
    debugArea.appendChild(msgnode);
    debugArea.parentNode.style.display = "";
  }

  /****
   **** Utilities
   ****/

  /***
   *** isAutoAnchor(node)      - Anchor for headdings: “a” or an paragraph
   ***                           having just only one “a”.
   *** isBlock(node)           - Block-level elements excluding list items.
   *** isEmptytParagraph(node) - Empty paragraph.
   *** isIgnorable(node)       - Ignorable text node.
   *** isOmittable(node)       - Omittable inline nodes.
   *** isList(node)            - Block-level elements especially the lists.
   *** isListItem(node)        - List items; “dd”, “dt” or “li”.
   *** isMeta(node)            - “script” or “style” element.
   ***
   *
   * Classify nodes.
   */

  function isBlock(node)
  {
    if (!node)
      return false;
    if (node.nodeType != 1)
      return false;
    switch (node.nodeName.toLowerCase()) {
    case "blockquote":
    case "center":
    case "div":
    case "dl":
    case "h1":
    case "h2":
    case "h3":
    case "h4":
    case "h5":
    case "h6":
    case "hr":
    case "ol":
    case "p":
    case "pre":
    case "table":
    case "ul":
      return true;
    default:
      return false;
    }
  }

  function isList(node)
  {
    if (!node)
      return false;
    if (node.nodeType != 1)
      return false;
    switch (node.nodeName.toLowerCase()) {
    case "dl":
    case "ol":
    case "ul":
      return true;
    default:
      return false;
    }
  }

  function isListItem(node)
  {
    if (!node)
      return false;
    if (node.nodeType != 1)
      return false;
    switch (node.nodeName.toLowerCase()) {
    case "dd":
    case "dt":
    case "li":
      return true;
    default:
      return false;
    }
  }

  function isMeta(node)
  {
    if (!node)
      return false;
    if (node.nodeType != 1)
      return false;
    switch (node.nodeName.toLowerCase()) {
    case "script":
    case "style":
      return true;
    default:
      return false;
    }
  }

  function isIgnorable(node)
  {
    switch (node.nodeType) {
    case 3: // A text node that...
      var n;
      // 1. contains whitespaces only (or empty), and...
      if (node.nodeValue.match(/[^\r\n ]/))
        return false;
      // 2.1. succeeds to: (a) no nodes (as a child of block-level node),
      // (b) block-level node, (c) script or style (d) or comment node.
      // or...
      n = node.previousSibling;
      if (!n && (isBlock(node.parentNode) || isListItem(node.parentNode)) ||
          n && (isBlock(n) || isListItem(n) || isMeta(n) || n.nodeType == 8))
        return true;
      // 2.2. preceeds to (a) no nodes (as a child of block-level node),
      // (b) block-level node, (c) script or style (d) or comment node.
      n = node.nextSibling;
      if (!n && (isBlock(node.parentNode) || isListItem(node.parentNode)) ||
          n && (isBlock(n) || isListItem(n) || isMeta(n) || n.nodeType == 8))
        return true;
      // ...is ignorable.  Otherwise, text nodes aren't ignorable.
      return false;
    
    case 8: // Comment nodes are ignorable.
      return true;
    
    default: // The others aren't ignorable.
      return false;
    }
  }

  /*
   * Following inline nodes are omittable.
   * * <sub> & <sup>
   * * The nodes with "noprint" class.
   * * Reference marks; the nodes with "reference" class etc.
   *
   * See [[User:Hatukanezumi/JIS X 4051の字間空き量#その他の処理]].
   *
   */
  var OMITTABLE_CLASSES = new Array("noprint", "reference", "external autonumber");
  function isOmittable(node)
  {
    if (node.nodeType != 1)
      return false;
    
    switch (node.nodeName.toLowerCase()) {
    case "sub":
    case "sup":
      return true;

    default:
      var klass = node.getAttribute("class") || node.getAttribute("className");
      for (var i = 0; i < OMITTABLE_CLASSES.length; i++)
        if (0 <= (" " + klass + " ").indexOf(" " + OMITTABLE_CLASSES[i] + " "))
          return true;
    }
    return false;
  }

  function isEmptyParagraph(node)
  {
    if (node.nodeType != 1 || node.nodeName.toLowerCase() != "p")
      return false;
    
    var nodes = new Array();
    var n = node.firstChild;
    while (n) {
      if (!isIgnorable(n))
        nodes.push(n);
      n = n.nextSibling;
    }
    if (nodes.length == 1 &&
        nodes[0].nodeType == 1 &&
        nodes[0].nodeName.toLowerCase() == "br")
      return true;
    else
      return false;
  }

  var headdingRe = new RegExp("^h[1-6]$", "i");
  function isAutoAnchor(node)
  {
    if (node.nodeType != 1)
      return false;
    else if (node.nodeName.toLowerCase() == "a" &&
             node.getAttribute("name")) {
      var n = node.nextSibling;
      while (n && isIgnorable(n))
        n = n.nextSibling;
      if (n && n.nodeType == 1 && headdingRe.test(n.nodeName))
        return true;
      
      return false;
    }
    else if (node.nodeName.toLowerCase() != "p")
      return false;
    
    var nodes = new Array();
    var n = node.firstChild;
    while (n) {
      if (!isIgnorable(n))
        nodes.push(n);
      n = n.nextSibling;
    }
    
    if (nodes.length == 1 &&
        nodes[0].nodeType == 1 &&
        nodes[0].nodeName.toLowerCase() == "a" &&
        nodes[0].getAttribute("name")) {
      var n = node.nextSibling;
      while (n && isIgnorable(n))
        n = n.nextSibling;
      if (n && n.nodeType == 1 && headdingRe.test(n.nodeName))
        return true;
    }
    
    return false;
  }

  /***
   *** getComputedElementStyle(element)
   ***
   *
   * Get actual style object of element.
   */

  function getComputedElementStyle(element)
  {
    if (element.currentStyle) // IE
      return element.currentStyle;
    else if (window.getComputedStyle) // Firefox, Opera
      return window.getComputedStyle(element, "");
    else if (document.defaultView) // Safari
      return document.defaultView.getComputedStyle(element, "");
    else if (element.style)
      return element.style;
    else
      return {};
  }

  /***
   *** getCookie(name)
   ***
   *
   * Get cookie value.
   */
  
  function getCookie(name) {
    var cookie = ' ' + document.cookie;
    var search = ' ' + name + '=';
    var value = null;
    var off;
    var end;
    off = cookie.indexOf(search);
    if (off != -1) {
      off += search.length;
      end = cookie.indexOf(';', off)
      if (end == -1)
        end = cookie.length;
      value = decodeURIComponent(cookie.substring(off, end).replace(/\+/g, ' '));
    }
    return(value);
  }


}

/****
 **** Startup.
 ****/

anansi = new Anansi();
if (anansi.platform.supported) {
  addOnloadHook(anansi.onloadHook);
  importScript("User:Hatukanezumi/AnansiRules.js");
  importScript("User:" + wgUserName + "/AnansiConfig.js");
}
else if (mw.config.get('wgAction') == "submit")
  alert("Anansi does not support browser that you are using now.");

/* end of script */