]*)?>([^<]+)<\/version[ \t\r\n]*>/.exec(m);
                if (m){
                    version = m[1];
                }
            }
            serverResource = {version: version, source: source, headers: headers};
        }
        if (!localResource['Last-Modified']){
            localResource['Last-Modified'] = serverResource.headers['Last-Modified'];
            store.set(this.name, localResource);
        }
        return [localResource, serverResource, store];
    },
    overwritePlugin: function(serverResource){
        /*
        if (!plugin[0] || plugin[0][0] != 'path')
            return 'plugin localpath was not found.';
        var localpath = plugin[0][1];
        */
        var source = serverResource.source;
        var file = io.File(this.path);
        if (!source)
            return 'source is null.';
        try {
            file.write(source);
        } catch (e){
            liberator.log('Could not write to ' + file.path + ': ' + e.message);
            return 'E190: Cannot open ' + file.path.quote() + ' for writing';
        }
        try {
            io.source(this.path);
        } catch (e){
            return e.message;
        }
        return 'update complete.';
    },
    compVersion: function(a, b){
        const comparator = Cc["@mozilla.org/xpcom/version-comparator;1"].getService(Ci.nsIVersionComparator);
        return comparator.compare(a, b);
    }
}; // }}}
// }}}
// --------------------------------------------------------
// WikiParser
// -----------------------------------------------------{{{
var WikiParser = (function () {
  function State (lines, result, indents) {
    if (!(this instanceof arguments.callee))
        return new arguments.callee(lines, result, indents);
    this.lines = lines;
    this.result = result || <>>;
    this.indents = indents || [];
  }
  State.prototype = {
    get end () !(this.lines.length && let (i = this.indents[this.indents.length - 1])
                                             /^\s*$/.test(this.head) || !i || i.test(this.head)),
    get realEnd () !this.lines.length,
    get head () this.lines[0],
    set head (value) this.lines[0] = value,
    get clone () State(cloneArray(this.lines), this.result, cloneArray(this.indents)),
    get next () {
      let result = this.clone;
      result.lines = this.lines.slice(1);
      return result;
    },
    indent: function (indent, more) {
      let result = this.clone;
      let re = RegExp('^' + indent.replace(/\S/g, ' ') + (more ? '\\s+' : '') + '(.*)$');
      result.indents.push(re);
      return result;
    },
    indentBack: function () {
      let result = this.clone;
      result.indents.pop();
      return result;
    },
    wrap: function (name) {
      let result = this.clone;
      result.result = <{name}>{this.result}{name}>;
      return result;
    },
    set: function (v) {
      let result = this.clone;
      result.result = v instanceof State ? v.result : v;
      return result;
    },
  };
  function Error (name, state) {
    if (!(this instanceof arguments.callee))
        return new arguments.callee(name, state);
    this.name = name;
    this.state = state;
  }
  function ok (v)
    v instanceof State;
  function cloneArray (ary)
    Array.concat(ary);
  function xmlJoin (xs, init) {
    let result = init || <>>;
    for (let i = 0, l = xs.length; i < l; i++)
      result += xs[i];
    return result;
  }
  function strip (s)
    s.replace(/^\s+|\s+$/g, '');
  function trimAll (lines) {
    let min = null;
    lines.forEach(function (line) {
      let s = line.match(/^\s*/).toString();
      if (min) {
        if (min.indexOf(s) == 0)
          min = s;
      } else {
        min = s;
      }
    });
    if (min) {
      let spre = RegExp('^' + min);
      liberator.log(min.length)
      return lines.map(function (line) line.replace(spre, ''));
    }
    return lines;
  }
  // FIXME
  function link (s) {
    let m;
    let result = <>>;
    while (s && (m = s.match(/(?:https?:\/\/|mailto:)\S+/))) {
      result += <>{RegExp.leftContext || ''}{m[0]}>;
      s = RegExp.rightContext;
    }
    if (s)
      result += <>{s}>;
    return result;
  }
  function stripAndLink (s)
    link(strip(s));
  function isEmptyLine (s)
    /^\s*$/.test(s);
  ////////////////////////////////////////////////////////////////////////////////
  let C = {
    // [Parser a] -> Parser b
    or: function or () {
      let as = [];
      for (let i = 0, l = arguments.length; i < l; i++)
        as.push(arguments[i]);
      return function (st) {
        for each (let a in as) {
          let r = a(st);
          if (ok(r))
            return r;
        }
        return Error('or-end', st);
      };
    },
    // Parser a -> Parser a
    many: function many (p) {
      return function (st) {
        let result = [];
        let cnt = 0;
        while (!st.end) {
          let r = p(st);
          if (ok(r))
            result.push(r.result);
          else
            break;
          st = r;
          if (cnt++ > 100) { liberator.log('force break: many-while'); break; }
        }
        if (ok(st))
          return st.set(result);
        return Error('many', st);
      }
    },
    // Parser a -> Parser a
    many1: function many1 (p) {
      return function (st) {
        let result = [];
        let cnt = 0;
        while (!st.end) {
          let r = p(st);
          if (ok(r))
            result.push(r.result);
          else
            break;
          st = r;
          if (cnt++ > 100) { liberator.log('force break: many1-while'); break; }
        }
        if (result.length) {
          return st.set(result);
        }
        return Error('many1', st);
      };
    },
    // Parser a -> Parser a
    indent: function indent (p) {
      return function (st) {
        if (st.end)
          return Error('EOL', 'st');
        return p(st);
      };
    }
  };
  ////////////////////////////////////////////////////////////////////////////////
  let P = (function () {
    // Int -> Parser XML
    function hn (n) {
      let re = RegExp('^\\s*=={' + n + '}\\s+(.*)\\s*=={' + n + '}\\s*$');
      return function (st) {
        let m = st.head.match(re);
        if (m) {
          let hn = 'h' + n;
          return st.next.set(<{hn} style={'font-size:'+(0.75+1/n)+'em'}>{stripAndLink(m[1])}{hn}>)
        }
        return Error('not head1', st);
      };
    }
    // String -> Parser XML
    function list (name) {
      return function (st) {
        let lis = C.many1(self[name + 'i'])(st);
        if (ok(lis)) {
          return lis.set(xmlJoin(lis.result)).wrap(name);
        }
        return Error(name, st);
      };
    }
    // String -> Parser XML
    function listItem (c) {
      let re = RegExp('^(\\s*\\' + c + ' )(.*)$');
      return function li (st) {
        let m = st.head.match(re);
        if (m) {
          let h = m[2];
          let next = C.many(self.wikiLine)(st.next.indent(m[1]));
          return next.indentBack().set(xmlJoin([<>{h}
>].concat(next.result))).wrap('li');
        }
        return Error(c, st);
      };
    }
    let self = {
      // St -> St
      debug: function debug (st) {
        //liberator.log({ head: st.head, indent: st.indents[st.indents.length - 1] });
        return Error('debug', st);
      },
      emptyLine: function emptyLine (st) {
        if (/^\s*$/.test(st.head)) {
          return st.next.set(<>>);
        }
        return Error('spaces', st);
      },
      // St -> St XML
      plain: function plain (st) {
        let text = st.head;
        return st.next.set(<>{stripAndLink(text)}
>);
      },
      // St -> St XML
      h1: hn(1),
      h2: hn(2),
      h3: hn(3),
      h4: hn(4),
      // St -> St XML
      pre: function pre (st) {
        let m = st.head.match(/^(\s*)>\|\|\s*$/);
        if (m) {
          let result = [];
          let cnt = 0;
          while (!st.realEnd) {
            st = st.next;
            if (/^(\s*)\|\|<\s*$/.test(st.head)){
              st = st.next;
              break;
            }
            result.push(st.head);
            if (cnt++ > 100) { liberator.log('force break: pre-while'); break; }
          }
          return st.set({trimAll(result).join('\n')});
        }
        return Error('pre', st);
      },
      // St -> St XML
      ul: list('ul'),
      // St -> St XML
      uli: listItem('-'),
      // St -> St XML
      ol: list('ol'),
      // St -> St XML
      oli: listItem('+'),
      // St -> St XML
      dl: function dl (st) {
        let r = C.many1(self.dtdd)(st);
        if (ok(r)) {
          let body = xmlJoin(r.result);
          return r.set(body).wrap('dl');
        }
        return Error('dl', st);
      },
      // St -> St XML
      dtdd: function dtdd (st) {
        let r = self.dt(st);
        if (ok(r)) {
          let [indent, dt] = r.result;
          let dd = C.many(self.wikiLine)(r.indent(indent, true));
          return dd.indentBack().set(dt + {xmlJoin(dd.result)});
        }
        return r;
      },
      // St -> St (lv, XML)
      dt: function dt (st) {
        let m = st.head.match(/^(\s*)(.+):\s*$/);
        if (m) {
          return st.next.set([m[1], {m[2]}]);
        }
        return Error('not dt', st);
      },
    };
    // St -> St XML
    with (self) {
      self.wikiLine = C.or(debug, emptyLine, h1, h2, h3, h4, dl, ol, ul, pre, plain);
      // St -> St [XML]
      self.wikiLines = C.many(wikiLine);
      self.wiki = function (st) {
        let r = wikiLines(st);
        if (ok(r)) {
          let xs = r.result;
          return r.set(xmlJoin(xs)).wrap('div');
        }
        return Error('wiki', st);
      }
    }
    for (let [name, p] in Iterator(self)) {
      self[name] = C.indent(p);
    }
    return self;
  })();
  return liberator.plugins.PMWikiParser = {
    parsers: P,
    combs: C,
    classes: {
      State: State,
    },
    parse: function (src) {
      let r = P.wiki(State(src.split(/\r\n|[\r\n]/)));
      if (ok(r))
        return r.result;
      else
        liberator.echoerr(r.name);
    }
  };
})();
// End WikiParser }}}
// --------------------------------------------------------
// HTML Stack
// -----------------------------------------------------{{{
function HTMLStack(){
    this.stack = [];
}
HTMLStack.prototype = { // {{{
    get length() this.stack.length,
    get last() this.stack[this.length-1],
    get lastLocalName() this.last[this.last.length()-1].localName(),
    get inlineElements() 'a abbr acronym b basefont bdo big br button cite code dfn em font i iframe img inout kbd label map object q s samp script select small span strike strong sub sup textarea tt u var'.split(' '),
    isInline: function(xml)
        xml.length() > 1 || xml.nodeKind() == 'text' || this.inlineElements.indexOf(xml.localName()) >= 0,
    push: function(xml) this.stack.push(xml),
    append: function(xml){
        if (this.length == 0){
            this.push(xml);
            return xml;
        }
        var buf = this.last[this.last.length()-1];
        if (buf.nodeKind() == 'text'){
            this.last[this.last.length()-1] += this.isInline(xml) ? <>
{xml}> : xml;
        } else if (this.isInline(xml)){
            this.stack[this.length-1] += xml;
        } else if (buf.localName() == xml.localName()){
            buf.* += xml.*;
        } else {
            this.stack[this.length-1] += xml;
        }
        return this.last;
    },
    appendChild: function(xml){
        if (this.length == 0){
            this.push(xml);
            return xml;
        }
        var buf = this.stack[this.length-1];
        if (buf[buf.length()-1].localName() == xml.localName()){
            if (this.isInline(xml.*[0]))
                buf[buf.length()-1].* += 
 + xml.*;
            else
                buf[buf.length()-1].* += xml.*;
        } else
            this.stack[this.length-1] += xml;
        return this.last;
    },
    appendLastChild: function(xml){
        var buf = this.last[this.last.length()-1].*;
        if (buf.length() > 0 && buf[buf.length()-1].nodeKind() == 'element'){
            let tmp = buf[buf.length()-1].*;
            if (tmp[tmp.length()-1].nodeKind() == 'element'){
                buf[buf.length()-1].* += xml;
            } else {
                buf[buf.length()-1].* += <>
{xml}>;
            }
        } else {
            this.last[this.last.length()-1].* += xml;
        }
        return this.last;
    },
    reorg: function(from){
        if (this.length == 0) return;
        if (!from) from = 0;
        var xmllist = this.stack.splice(from);
        var xml;
        if (xmllist.length > 1){
            xml = xmllist.reduceRight(function(p, c){
                let buf = c[c.length()-1].*;
                if (buf.length() > 0){
                    if (buf[buf.length()-1].nodeKind() == 'text'){
                        c += p;
                    } else {
                        buf[buf.length()-1].* += p;
                    }
                } else {
                    c += p;
                }
                return c;
            });
        } else if (xmllist.length > 0){
            xml = xmllist[0];
        }
        this.push(xml);
        return this.last;
    }
}; // }}}
// }}}
// --------------------------------------------------------
// CODEREPOS_PLUGINS
// -----------------------------------------------------{{{
var CODEREPOS = (function(){
    const indexURL = 'http://vimperator.kurinton.net/plugins/info.xml';
    var public = {
        plugins: [],
        init: function(){
            this.plugins = [];
            util.httpGet(indexURL, function(xhr){
                let xml = new XMLList(xhr.responseText);
                let plugins = xml.plugin;
                for (let i=0, length = plugins.length(); i < length; i++){
                    public.plugins.push(new CodeReposPlugin(plugins[i]));
                }
            });
        }
    };
    function CodeReposPlugin(xml){
        this.name = tags.name(xml);
        this.URL = tags.updateURL(xml);
        this.description = tags.description(xml);
        this.version = tags.version(xml);
    }
    public.init();
    return public;
})();
// }}}
// --------------------------------------------------------
// Vimperator Command
// -----------------------------------------------------{{{
commands.addUserCommand(['plugin[help]'], 'list Vimperator plugins',
    function(args){
        var xml;
        if (args["-check"])
            xml = liberator.plugins.pluginManager.checkVersion(args);
        else if (args["-update"])
            xml = liberator.plugins.pluginManager.update(args);
        else if (args["-source"]) {
            if (args.length < 1)
                return liberator.echoerr('Argument(plugin name) required');
            return liberator.plugins.pluginManager.source(args);
        } else
            xml = liberator.plugins.pluginManager.list(args, args["-verbose"]);
        liberator.echo(xml, true);
    }, {
        argCount: '*',
        options: [
            [['-verbose', '-v'], commands.OPTION_NOARG],
            [['-check', '-c'], commands.OPTION_NOARG],
            [['-update', '-u'], commands.OPTION_NOARG],
            [['-source', '-s'], commands.OPTION_NOARG],
        ],
        completer: function(context){
            context.title = ['PluginName', '[Version]Description'];
            context.completions = getPlugins().map(function(plugin) [
                plugin.name,
                '[' + (plugin.items.version || 'unknown') + ']' +
                (plugin.items.description || '-')
            ]).filter(function(row)
                row[0].toLowerCase().indexOf(context.filter.toLowerCase()) >= 0);
        }
    }, true);
commands.addUserCommand(['pluginmanager', 'pm'], 'Manage Vimperator plugins',
    function(args){
        if (args.length < 1)
            liberator.echoerr('Not enough arguments. Sub-command required.');
        var xml;
        var sub = args[0];
        var subArgs = args.slice(1);
        function s2b (s, d) (!/^(\d+|false)$/i.test(s)|parseInt(s)|!!d*2)&1<= 0);
                } else if (args[0] == "install"){
                    context.anchored = false;
                    context.title = ["PluginName", "Name: Description"];
                    context.completions = CODEREPOS.plugins.filter(function($_){
                        return !getPlugins().some(function(installed){
                            return installed.items.updateURL ? installed.items.updateURL == $_.URL : false;
                        });
                    }).map(function(plugin) [plugin.URL, plugin.name + ": " + plugin.description]);
                }
            }
        }
    }, true); // }}}
// --------------------------------------------------------
// Public Member (liberator.plugins.pluginManger)
// -----------------------------------------------------{{{
var public = {
    getPlugins: function(names, forceReload){
        let plugins = getPlugins(forceReload);
        if (!names || names.length == 0)
            return plugins;
        return plugins.filter(function(plugin) names.indexOf(plugin.name) >= 0);
    },
    checkVersion: function(names){
        let xml = <>>;
        this.getPlugins(names).forEach(function(plugin){
            xml += plugin.checkVersion();
        });
        return xml;
    },
    update: function(names){
        let xml = <>>;
        this.getPlugins(names).forEach(function(plugin){
            xml += plugin.updatePlugin();
        });
        return xml;
    },
    source: function(names){
        // XXX 一度に開くようにするべき? (ref: editor.js L849)
        this.getPlugins(names).forEach(function(plugin){
            editor.editFileExternally(plugin.path);
        });
        return;
    },
    list: function(names, verbose){
        let xml = <>>
        this.getPlugins(names).forEach(function(plugin){
            xml += plugin.itemFormatter(verbose);
        });
        return xml;
    },
    install: function(urls){
        function makeURL(s){
            let url = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIURL);
            url.spec = s;
            return url;
        }
        function fixURL(url){
            const cr = RegExp('^http://coderepos\\.org/share/browser/lang/javascript/vimperator-plugins/trunk/[^/]+\\.js$');
            const pi = RegExp('^http://vimperator\\.kurinton\\.net/plugins/');
            const npi = /\/(all|index)\.html/;
            const js = /\.js$/i;
            function xe(xpath){
                let ss = util.evaluateXPath(xpath);
                return (ss.snapshotLength > 0) && ss.snapshotItem(0).href;
            }
            if (cr.test(url)) {
                return url.replace(/(?=coderepos\.org\/)/, 'svn.').replace(/browser\//, '');
            }
            if (pi.test(url) && !npi.test(url)) {
                return xe('//a[@id="file-link"]');
            }
            if (js.test(url)) {
                return url;
            }
            throw 'Current URL is not a pluginFile';
        }
        function download(url){
            let wbp = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
            let progressListener = {
                onStateChange: function (_webProgress, _request, _stateFlags, _status) {
                    if (_stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
                        if (file.exists()) {
                            io.source(file.path);
                            liberator.echo('Executed: ' + file.path)
                        } else {
                            return liberator.echoerr('Download error!');
                        }
                    }
                },
                onProgressChange: function () undefined,
                onLocationChange: function () undefined,
                onStatusChange: function () undefined,
                onSecurityChange: function () undefined,
            };
            let filename = url.match(/[^\/]+$/).toString();
            let file = io.getRuntimeDirectories('plugin')[0];
            file.append(filename);
            if (file.exists())
                return liberator.echoerr('Already exists: ' + file.path);
            let fileuri = makeFileURI(file);
            wbp.progressListener = progressListener;
            wbp.persistFlags |= wbp.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
            wbp.saveURI(makeURL(url), null, null, null, null, fileuri);
        }
        var url = urls.length ? urls[0] : buffer.URL;
        var sourceURL = fixURL(url);
        if (sourceURL == url) {
            liberator.log(url);
            download(url);
        } else {
            liberator.open(sourceURL);
            liberator.echoerr('Please check the source code of plugin, and retry ":pluginmanager install"');
        }
    }
};
return public;
// }}}
})();
// vim: sw=4 ts=4 et fdm=marker: