tree view for qwerty

くわツリービュー。あやしいわーるど@上海の投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。

当前为 2015-06-13 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

(function() {
/* Zepto v1.1.4-84-gc454cfe - zepto event ajax selector deferred callbacks - zeptojs.com/license */

var Zepto = (function() {
  var undefined, key, $, classList, emptyArray = [], concat = emptyArray.concat, filter = emptyArray.filter, slice = emptyArray.slice,
    document = window.document,
    elementDisplay = {}, classCache = {},
    cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 },
    fragmentRE = /^\s*<(\w+|!)[^>]*>/,
    singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
    tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
    rootNodeRE = /^(?:body|html)$/i,
    capitalRE = /([A-Z])/g,

    // special attributes that should be get/set via method calls
    methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'],

    adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ],
    table = document.createElement('table'),
    tableRow = document.createElement('tr'),
    containers = {
      'tr': document.createElement('tbody'),
      'tbody': table, 'thead': table, 'tfoot': table,
      'td': tableRow, 'th': tableRow,
      '*': document.createElement('div')
    },
    readyRE = /complete|loaded|interactive/,
    simpleSelectorRE = /^[\w-]*$/,
    class2type = {},
    toString = class2type.toString,
    zepto = {},
    camelize, uniq,
    tempParent = document.createElement('div'),
    propMap = {
      'tabindex': 'tabIndex',
      'readonly': 'readOnly',
      'for': 'htmlFor',
      'class': 'className',
      'maxlength': 'maxLength',
      'cellspacing': 'cellSpacing',
      'cellpadding': 'cellPadding',
      'rowspan': 'rowSpan',
      'colspan': 'colSpan',
      'usemap': 'useMap',
      'frameborder': 'frameBorder',
      'contenteditable': 'contentEditable'
    },
    isArray = Array.isArray ||
      function(object){ return object instanceof Array }

  zepto.matches = function(element, selector) {
    if (!selector || !element || element.nodeType !== 1) return false
    var matchesSelector = element.webkitMatchesSelector || element.mozMatchesSelector ||
                          element.oMatchesSelector || element.matchesSelector
    if (matchesSelector) return matchesSelector.call(element, selector)
    // fall back to performing a selector:
    var match, parent = element.parentNode, temp = !parent
    if (temp) (parent = tempParent).appendChild(element)
    match = ~zepto.qsa(parent, selector).indexOf(element)
    temp && tempParent.removeChild(element)
    return match
  }

  function type(obj) {
    return obj == null ? String(obj) :
      class2type[toString.call(obj)] || "object"
  }

  function isFunction(value) { return type(value) == "function" }
  function isWindow(obj)     { return obj != null && obj == obj.window }
  function isDocument(obj)   { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }
  function isObject(obj)     { return type(obj) == "object" }
  function isPlainObject(obj) {
    return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
  }
  function likeArray(obj) { return typeof obj.length == 'number' }

  function compact(array) { return filter.call(array, function(item){ return item != null }) }
  function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array }
  camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) }
  function dasherize(str) {
    return str.replace(/::/g, '/')
           .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
           .replace(/([a-z\d])([A-Z])/g, '$1_$2')
           .replace(/_/g, '-')
           .toLowerCase()
  }
  uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) }

  function classRE(name) {
    return name in classCache ?
      classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
  }

  function maybeAddPx(name, value) {
    return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value
  }

  function defaultDisplay(nodeName) {
    var element, display
    if (!elementDisplay[nodeName]) {
      element = document.createElement(nodeName)
      document.body.appendChild(element)
      display = getComputedStyle(element, '').getPropertyValue("display")
      element.parentNode.removeChild(element)
      display == "none" && (display = "block")
      elementDisplay[nodeName] = display
    }
    return elementDisplay[nodeName]
  }

  function children(element) {
    return 'children' in element ?
      slice.call(element.children) :
      $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node })
  }

  function Z(dom, selector) {
    var i, len = dom ? dom.length : 0
    for (i = 0; i < len; i++) this[i] = dom[i]
    this.length = len
    this.selector = selector || ''
  }

  // `$.zepto.fragment` takes a html string and an optional tag name
  // to generate DOM nodes nodes from the given html string.
  // The generated DOM nodes are returned as an array.
  // This function can be overriden in plugins for example to make
  // it compatible with browsers that don't support the DOM fully.
  zepto.fragment = function(html, name, properties) {
    var dom, nodes, container

    // A special case optimization for a single tag
    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))

    if (!dom) {
      if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
      if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
      if (!(name in containers)) name = '*'

      container = containers[name]
      container.innerHTML = '' + html
      dom = $.each(slice.call(container.childNodes), function(){
        container.removeChild(this)
      })
    }

    if (isPlainObject(properties)) {
      nodes = $(dom)
      $.each(properties, function(key, value) {
        if (methodAttributes.indexOf(key) > -1) nodes[key](value)
        else nodes.attr(key, value)
      })
    }

    return dom
  }

  // `$.zepto.Z` swaps out the prototype of the given `dom` array
  // of nodes with `$.fn` and thus supplying all the Zepto functions
  // to the array. This method can be overriden in plugins.
  zepto.Z = function(dom, selector) {
    return new Z(dom, selector)
  }

  // `$.zepto.isZ` should return `true` if the given object is a Zepto
  // collection. This method can be overriden in plugins.
  zepto.isZ = function(object) {
    return object instanceof zepto.Z
  }

  // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and
  // takes a CSS selector and an optional context (and handles various
  // special cases).
  // This method can be overriden in plugins.
  zepto.init = function(selector, context) {
    var dom
    // If nothing given, return an empty Zepto collection
    if (!selector) return zepto.Z()
    // Optimize for string selectors
    else if (typeof selector == 'string') {
      selector = selector.trim()
      // If it's a html fragment, create nodes from it
      // Note: In both Chrome 21 and Firefox 15, DOM error 12
      // is thrown if the fragment doesn't begin with <
      if (selector[0] == '<' && fragmentRE.test(selector))
        dom = zepto.fragment(selector, RegExp.$1, context), selector = null
      // If there's a context, create a collection on that context first, and select
      // nodes from there
      else if (context !== undefined) return $(context).find(selector)
      // If it's a CSS selector, use it to select nodes.
      else dom = zepto.qsa(document, selector)
    }
    // If a function is given, call it when the DOM is ready
    else if (isFunction(selector)) return $(document).ready(selector)
    // If a Zepto collection is given, just return it
    else if (zepto.isZ(selector)) return selector
    else {
      // normalize array if an array of nodes is given
      if (isArray(selector)) dom = compact(selector)
      // Wrap DOM nodes.
      else if (isObject(selector))
        dom = [selector], selector = null
      // If it's a html fragment, create nodes from it
      else if (fragmentRE.test(selector))
        dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
      // If there's a context, create a collection on that context first, and select
      // nodes from there
      else if (context !== undefined) return $(context).find(selector)
      // And last but no least, if it's a CSS selector, use it to select nodes.
      else dom = zepto.qsa(document, selector)
    }
    // create a new Zepto collection from the nodes found
    return zepto.Z(dom, selector)
  }

  // `$` will be the base `Zepto` object. When calling this
  // function just call `$.zepto.init, which makes the implementation
  // details of selecting nodes and creating Zepto collections
  // patchable in plugins.
  $ = function(selector, context){
    return zepto.init(selector, context)
  }

  function extend(target, source, deep) {
    for (key in source)
      if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
        if (isPlainObject(source[key]) && !isPlainObject(target[key]))
          target[key] = {}
        if (isArray(source[key]) && !isArray(target[key]))
          target[key] = []
        extend(target[key], source[key], deep)
      }
      else if (source[key] !== undefined) target[key] = source[key]
  }

  // Copy all but undefined properties from one or more
  // objects to the `target` object.
  $.extend = function(target){
    var deep, args = slice.call(arguments, 1)
    if (typeof target == 'boolean') {
      deep = target
      target = args.shift()
    }
    args.forEach(function(arg){ extend(target, arg, deep) })
    return target
  }

  // `$.zepto.qsa` is Zepto's CSS selector implementation which
  // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`.
  // This method can be overriden in plugins.
  zepto.qsa = function(element, selector){
    var found,
        maybeID = selector[0] == '#',
        maybeClass = !maybeID && selector[0] == '.',
        nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
        isSimple = simpleSelectorRE.test(nameOnly)
    return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById
      ( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
      (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
      slice.call(
        isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName
          maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
          element.getElementsByTagName(selector) : // Or a tag
          element.querySelectorAll(selector) // Or it's not simple, and we need to query all
      )
  }

  function filtered(nodes, selector) {
    return selector == null ? $(nodes) : $(nodes).filter(selector)
  }

  $.contains = document.documentElement.contains ?
    function(parent, node) {
      return parent !== node && parent.contains(node)
    } :
    function(parent, node) {
      while (node && (node = node.parentNode))
        if (node === parent) return true
      return false
    }

  function funcArg(context, arg, idx, payload) {
    return isFunction(arg) ? arg.call(context, idx, payload) : arg
  }

  function setAttribute(node, name, value) {
    value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
  }

  // access className property while respecting SVGAnimatedString
  function className(node, value){
    var klass = node.className || '',
        svg   = klass && klass.baseVal !== undefined

    if (value === undefined) return svg ? klass.baseVal : klass
    svg ? (klass.baseVal = value) : (node.className = value)
  }

  // "true"  => true
  // "false" => false
  // "null"  => null
  // "42"    => 42
  // "42.5"  => 42.5
  // "08"    => "08"
  // JSON    => parse if valid
  // String  => self
  function deserializeValue(value) {
    try {
      return value ?
        value == "true" ||
        ( value == "false" ? false :
          value == "null" ? null :
          +value + "" == value ? +value :
          /^[\[\{]/.test(value) ? $.parseJSON(value) :
          value )
        : value
    } catch(e) {
      return value
    }
  }

  $.type = type
  $.isFunction = isFunction
  $.isWindow = isWindow
  $.isArray = isArray
  $.isPlainObject = isPlainObject

  $.isEmptyObject = function(obj) {
    var name
    for (name in obj) return false
    return true
  }

  $.inArray = function(elem, array, i){
    return emptyArray.indexOf.call(array, elem, i)
  }

  $.camelCase = camelize
  $.trim = function(str) {
    return str == null ? "" : String.prototype.trim.call(str)
  }

  // plugin compatibility
  $.uuid = 0
  $.support = { }
  $.expr = { }
  $.noop = function() {}

  $.map = function(elements, callback){
    var value, values = [], i, key
    if (likeArray(elements))
      for (i = 0; i < elements.length; i++) {
        value = callback(elements[i], i)
        if (value != null) values.push(value)
      }
    else
      for (key in elements) {
        value = callback(elements[key], key)
        if (value != null) values.push(value)
      }
    return flatten(values)
  }

  $.each = function(elements, callback){
    var i, key
    if (likeArray(elements)) {
      for (i = 0; i < elements.length; i++)
        if (callback.call(elements[i], i, elements[i]) === false) return elements
    } else {
      for (key in elements)
        if (callback.call(elements[key], key, elements[key]) === false) return elements
    }

    return elements
  }

  $.grep = function(elements, callback){
    return filter.call(elements, callback)
  }

  if (window.JSON) $.parseJSON = JSON.parse

  // Populate the class2type map
  $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
    class2type[ "[object " + name + "]" ] = name.toLowerCase()
  })

  // Define methods that will be available on all
  // Zepto collections
  $.fn = {
    constructor: zepto.Z,
    length: 0,

    // Because a collection acts like an array
    // copy over these useful array functions.
    forEach: emptyArray.forEach,
    reduce: emptyArray.reduce,
    push: emptyArray.push,
    sort: emptyArray.sort,
    splice: emptyArray.splice,
    indexOf: emptyArray.indexOf,
    concat: function(){
      var i, value, args = []
      for (i = 0; i < arguments.length; i++) {
        value = arguments[i]
        args[i] = zepto.isZ(value) ? value.toArray() : value
      }
      return concat.apply(zepto.isZ(this) ? this.toArray() : this, args)
    },

    // `map` and `slice` in the jQuery API work differently
    // from their array counterparts
    map: function(fn){
      return $($.map(this, function(el, i){ return fn.call(el, i, el) }))
    },
    slice: function(){
      return $(slice.apply(this, arguments))
    },

    ready: function(callback){
      // need to check if document.body exists for IE as that browser reports
      // document ready when it hasn't yet created the body element
      if (readyRE.test(document.readyState) && document.body) callback($)
      else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false)
      return this
    },
    get: function(idx){
      return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length]
    },
    toArray: function(){ return this.get() },
    size: function(){
      return this.length
    },
    remove: function(){
      return this.each(function(){
        if (this.parentNode != null)
          this.parentNode.removeChild(this)
      })
    },
    each: function(callback){
      emptyArray.every.call(this, function(el, idx){
        return callback.call(el, idx, el) !== false
      })
      return this
    },
    filter: function(selector){
      if (isFunction(selector)) return this.not(this.not(selector))
      return $(filter.call(this, function(element){
        return zepto.matches(element, selector)
      }))
    },
    add: function(selector,context){
      return $(uniq(this.concat($(selector,context))))
    },
    is: function(selector){
      return this.length > 0 && zepto.matches(this[0], selector)
    },
    not: function(selector){
      var nodes=[]
      if (isFunction(selector) && selector.call !== undefined)
        this.each(function(idx){
          if (!selector.call(this,idx)) nodes.push(this)
        })
      else {
        var excludes = typeof selector == 'string' ? this.filter(selector) :
          (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector)
        this.forEach(function(el){
          if (excludes.indexOf(el) < 0) nodes.push(el)
        })
      }
      return $(nodes)
    },
    has: function(selector){
      return this.filter(function(){
        return isObject(selector) ?
          $.contains(this, selector) :
          $(this).find(selector).size()
      })
    },
    eq: function(idx){
      return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1)
    },
    first: function(){
      var el = this[0]
      return el && !isObject(el) ? el : $(el)
    },
    last: function(){
      var el = this[this.length - 1]
      return el && !isObject(el) ? el : $(el)
    },
    find: function(selector){
      var result, $this = this
      if (!selector) result = $()
      else if (typeof selector == 'object')
        result = $(selector).filter(function(){
          var node = this
          return emptyArray.some.call($this, function(parent){
            return $.contains(parent, node)
          })
        })
      else if (this.length == 1) result = $(zepto.qsa(this[0], selector))
      else result = this.map(function(){ return zepto.qsa(this, selector) })
      return result
    },
    closest: function(selector, context){
      var node = this[0], collection = false
      if (typeof selector == 'object') collection = $(selector)
      while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector)))
        node = node !== context && !isDocument(node) && node.parentNode
      return $(node)
    },
    parents: function(selector){
      var ancestors = [], nodes = this
      while (nodes.length > 0)
        nodes = $.map(nodes, function(node){
          if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) {
            ancestors.push(node)
            return node
          }
        })
      return filtered(ancestors, selector)
    },
    parent: function(selector){
      return filtered(uniq(this.pluck('parentNode')), selector)
    },
    children: function(selector){
      return filtered(this.map(function(){ return children(this) }), selector)
    },
    contents: function() {
      return this.map(function() { return this.contentDocument || slice.call(this.childNodes) })
    },
    siblings: function(selector){
      return filtered(this.map(function(i, el){
        return filter.call(children(el.parentNode), function(child){ return child!==el })
      }), selector)
    },
    empty: function(){
      return this.each(function(){ this.innerHTML = '' })
    },
    // `pluck` is borrowed from Prototype.js
    pluck: function(property){
      return $.map(this, function(el){ return el[property] })
    },
    show: function(){
      return this.each(function(){
        this.style.display == "none" && (this.style.display = '')
        if (getComputedStyle(this, '').getPropertyValue("display") == "none")
          this.style.display = defaultDisplay(this.nodeName)
      })
    },
    replaceWith: function(newContent){
      return this.before(newContent).remove()
    },
    wrap: function(structure){
      var func = isFunction(structure)
      if (this[0] && !func)
        var dom   = $(structure).get(0),
            clone = dom.parentNode || this.length > 1

      return this.each(function(index){
        $(this).wrapAll(
          func ? structure.call(this, index) :
            clone ? dom.cloneNode(true) : dom
        )
      })
    },
    wrapAll: function(structure){
      if (this[0]) {
        $(this[0]).before(structure = $(structure))
        var children
        // drill down to the inmost element
        while ((children = structure.children()).length) structure = children.first()
        $(structure).append(this)
      }
      return this
    },
    wrapInner: function(structure){
      var func = isFunction(structure)
      return this.each(function(index){
        var self = $(this), contents = self.contents(),
            dom  = func ? structure.call(this, index) : structure
        contents.length ? contents.wrapAll(dom) : self.append(dom)
      })
    },
    unwrap: function(){
      this.parent().each(function(){
        $(this).replaceWith($(this).children())
      })
      return this
    },
    clone: function(){
      return this.map(function(){ return this.cloneNode(true) })
    },
    hide: function(){
      return this.css("display", "none")
    },
    toggle: function(setting){
      return this.each(function(){
        var el = $(this)
        ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide()
      })
    },
    prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') },
    next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') },
    html: function(html){
      return 0 in arguments ?
        this.each(function(idx){
          var originHtml = this.innerHTML
          $(this).empty().append( funcArg(this, html, idx, originHtml) )
        }) :
        (0 in this ? this[0].innerHTML : null)
    },
    text: function(text){
      return 0 in arguments ?
        this.each(function(idx){
          var newText = funcArg(this, text, idx, this.textContent)
          this.textContent = newText == null ? '' : ''+newText
        }) :
        (0 in this ? this[0].textContent : null)
    },
    attr: function(name, value){
      var result
      return (typeof name == 'string' && !(1 in arguments)) ?
        (!this.length || this[0].nodeType !== 1 ? undefined :
          (!(result = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : result
        ) :
        this.each(function(idx){
          if (this.nodeType !== 1) return
          if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
          else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
        })
    },
    removeAttr: function(name){
      return this.each(function(){ this.nodeType === 1 && name.split(' ').forEach(function(attribute){
        setAttribute(this, attribute)
      }, this)})
    },
    prop: function(name, value){
      name = propMap[name] || name
      return (1 in arguments) ?
        this.each(function(idx){
          this[name] = funcArg(this, value, idx, this[name])
        }) :
        (this[0] && this[0][name])
    },
    data: function(name, value){
      var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()

      var data = (1 in arguments) ?
        this.attr(attrName, value) :
        this.attr(attrName)

      return data !== null ? deserializeValue(data) : undefined
    },
    val: function(value){
      return 0 in arguments ?
        this.each(function(idx){
          this.value = funcArg(this, value, idx, this.value)
        }) :
        (this[0] && (this[0].multiple ?
           $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') :
           this[0].value)
        )
    },
    offset: function(coordinates){
      if (coordinates) return this.each(function(index){
        var $this = $(this),
            coords = funcArg(this, coordinates, index, $this.offset()),
            parentOffset = $this.offsetParent().offset(),
            props = {
              top:  coords.top  - parentOffset.top,
              left: coords.left - parentOffset.left
            }

        if ($this.css('position') == 'static') props['position'] = 'relative'
        $this.css(props)
      })
      if (!this.length) return null
      if (!$.contains(document.documentElement, this[0]))
        return {top: 0, left: 0}
      var obj = this[0].getBoundingClientRect()
      return {
        left: obj.left + window.pageXOffset,
        top: obj.top + window.pageYOffset,
        width: Math.round(obj.width),
        height: Math.round(obj.height)
      }
    },
    css: function(property, value){
      if (arguments.length < 2) {
        var computedStyle, element = this[0]
        if(!element) return
        computedStyle = getComputedStyle(element, '')
        if (typeof property == 'string')
          return element.style[camelize(property)] || computedStyle.getPropertyValue(property)
        else if (isArray(property)) {
          var props = {}
          $.each(property, function(_, prop){
            props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
          })
          return props
        }
      }

      var css = ''
      if (type(property) == 'string') {
        if (!value && value !== 0)
          this.each(function(){ this.style.removeProperty(dasherize(property)) })
        else
          css = dasherize(property) + ":" + maybeAddPx(property, value)
      } else {
        for (key in property)
          if (!property[key] && property[key] !== 0)
            this.each(function(){ this.style.removeProperty(dasherize(key)) })
          else
            css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
      }

      return this.each(function(){ this.style.cssText += ';' + css })
    },
    index: function(element){
      return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0])
    },
    hasClass: function(name){
      if (!name) return false
      return emptyArray.some.call(this, function(el){
        return this.test(className(el))
      }, classRE(name))
    },
    addClass: function(name){
      if (!name) return this
      return this.each(function(idx){
        if (!('className' in this)) return
        classList = []
        var cls = className(this), newName = funcArg(this, name, idx, cls)
        newName.split(/\s+/g).forEach(function(klass){
          if (!$(this).hasClass(klass)) classList.push(klass)
        }, this)
        classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
      })
    },
    removeClass: function(name){
      return this.each(function(idx){
        if (!('className' in this)) return
        if (name === undefined) return className(this, '')
        classList = className(this)
        funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){
          classList = classList.replace(classRE(klass), " ")
        })
        className(this, classList.trim())
      })
    },
    toggleClass: function(name, when){
      if (!name) return this
      return this.each(function(idx){
        var $this = $(this), names = funcArg(this, name, idx, className(this))
        names.split(/\s+/g).forEach(function(klass){
          (when === undefined ? !$this.hasClass(klass) : when) ?
            $this.addClass(klass) : $this.removeClass(klass)
        })
      })
    },
    scrollTop: function(value){
      if (!this.length) return
      var hasScrollTop = 'scrollTop' in this[0]
      if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
      return this.each(hasScrollTop ?
        function(){ this.scrollTop = value } :
        function(){ this.scrollTo(this.scrollX, value) })
    },
    scrollLeft: function(value){
      if (!this.length) return
      var hasScrollLeft = 'scrollLeft' in this[0]
      if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
      return this.each(hasScrollLeft ?
        function(){ this.scrollLeft = value } :
        function(){ this.scrollTo(value, this.scrollY) })
    },
    position: function() {
      if (!this.length) return

      var elem = this[0],
        // Get *real* offsetParent
        offsetParent = this.offsetParent(),
        // Get correct offsets
        offset       = this.offset(),
        parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()

      // Subtract element margins
      // note: when an element has margin: auto the offsetLeft and marginLeft
      // are the same in Safari causing offset.left to incorrectly be 0
      offset.top  -= parseFloat( $(elem).css('margin-top') ) || 0
      offset.left -= parseFloat( $(elem).css('margin-left') ) || 0

      // Add offsetParent borders
      parentOffset.top  += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0
      parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0

      // Subtract the two offsets
      return {
        top:  offset.top  - parentOffset.top,
        left: offset.left - parentOffset.left
      }
    },
    offsetParent: function() {
      return this.map(function(){
        var parent = this.offsetParent || document.body
        while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
          parent = parent.offsetParent
        return parent
      })
    }
  }

  // for now
  $.fn.detach = $.fn.remove

  // Generate the `width` and `height` functions
  ;['width', 'height'].forEach(function(dimension){
    var dimensionProperty =
      dimension.replace(/./, function(m){ return m[0].toUpperCase() })

    $.fn[dimension] = function(value){
      var offset, el = this[0]
      if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] :
        isDocument(el) ? el.documentElement['scroll' + dimensionProperty] :
        (offset = this.offset()) && offset[dimension]
      else return this.each(function(idx){
        el = $(this)
        el.css(dimension, funcArg(this, value, idx, el[dimension]()))
      })
    }
  })

  function traverseNode(node, fun) {
    fun(node)
    for (var i = 0, len = node.childNodes.length; i < len; i++)
      traverseNode(node.childNodes[i], fun)
  }

  // Generate the `after`, `prepend`, `before`, `append`,
  // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods.
  adjacencyOperators.forEach(function(operator, operatorIndex) {
    var inside = operatorIndex % 2 //=> prepend, append

    $.fn[operator] = function(){
      // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
      var argType, nodes = $.map(arguments, function(arg) {
            argType = type(arg)
            return argType == "object" || argType == "array" || arg == null ?
              arg : zepto.fragment(arg)
          }),
          parent, copyByClone = this.length > 1
      if (nodes.length < 1) return this

      return this.each(function(_, target){
        parent = inside ? target : target.parentNode

        // convert all methods to a "before" operation
        target = operatorIndex == 0 ? target.nextSibling :
                 operatorIndex == 1 ? target.firstChild :
                 operatorIndex == 2 ? target :
                 null

        var parentInDocument = $.contains(document.documentElement, parent)

        nodes.forEach(function(node){
          if (copyByClone) node = node.cloneNode(true)
          else if (!parent) return $(node).remove()

          parent.insertBefore(node, target)
          if (parentInDocument) traverseNode(node, function(el){
            if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
               (!el.type || el.type === 'text/javascript') && !el.src)
              window['eval'].call(window, el.innerHTML)
          })
        })
      })
    }

    // after    => insertAfter
    // prepend  => prependTo
    // before   => insertBefore
    // append   => appendTo
    $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
      $(html)[operator](this)
      return this
    }
  })

  zepto.Z.prototype = Z.prototype = $.fn

  // Export internal API functions in the `$.zepto` namespace
  zepto.uniq = uniq
  zepto.deserializeValue = deserializeValue
  $.zepto = zepto

  return $
})()

window.Zepto = Zepto
window.$ === undefined && (window.$ = Zepto)

;(function($){
  var _zid = 1, undefined,
      slice = Array.prototype.slice,
      isFunction = $.isFunction,
      isString = function(obj){ return typeof obj == 'string' },
      handlers = {},
      specialEvents={},
      focusinSupported = 'onfocusin' in window,
      focus = { focus: 'focusin', blur: 'focusout' },
      hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }

  specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'

  function zid(element) {
    return element._zid || (element._zid = _zid++)
  }
  function findHandlers(element, event, fn, selector) {
    event = parse(event)
    if (event.ns) var matcher = matcherFor(event.ns)
    return (handlers[zid(element)] || []).filter(function(handler) {
      return handler
        && (!event.e  || handler.e == event.e)
        && (!event.ns || matcher.test(handler.ns))
        && (!fn       || zid(handler.fn) === zid(fn))
        && (!selector || handler.sel == selector)
    })
  }
  function parse(event) {
    var parts = ('' + event).split('.')
    return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
  }
  function matcherFor(ns) {
    return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
  }

  function eventCapture(handler, captureSetting) {
    return handler.del &&
      (!focusinSupported && (handler.e in focus)) ||
      !!captureSetting
  }

  function realEvent(type) {
    return hover[type] || (focusinSupported && focus[type]) || type
  }

  function add(element, events, fn, data, selector, delegator, capture){
    var id = zid(element), set = (handlers[id] || (handlers[id] = []))
    events.split(/\s/).forEach(function(event){
      if (event == 'ready') return $(document).ready(fn)
      var handler   = parse(event)
      handler.fn    = fn
      handler.sel   = selector
      // emulate mouseenter, mouseleave
      if (handler.e in hover) fn = function(e){
        var related = e.relatedTarget
        if (!related || (related !== this && !$.contains(this, related)))
          return handler.fn.apply(this, arguments)
      }
      handler.del   = delegator
      var callback  = delegator || fn
      handler.proxy = function(e){
        e = compatible(e)
        if (e.isImmediatePropagationStopped()) return
        e.data = data
        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
        if (result === false) e.preventDefault(), e.stopPropagation()
        return result
      }
      handler.i = set.length
      set.push(handler)
      if ('addEventListener' in element)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  }
  function remove(element, events, fn, selector, capture){
    var id = zid(element)
    ;(events || '').split(/\s/).forEach(function(event){
      findHandlers(element, event, fn, selector).forEach(function(handler){
        delete handlers[id][handler.i]
      if ('removeEventListener' in element)
        element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
      })
    })
  }

  $.event = { add: add, remove: remove }

  $.proxy = function(fn, context) {
    var args = (2 in arguments) && slice.call(arguments, 2)
    if (isFunction(fn)) {
      var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
      proxyFn._zid = zid(fn)
      return proxyFn
    } else if (isString(context)) {
      if (args) {
        args.unshift(fn[context], fn)
        return $.proxy.apply(null, args)
      } else {
        return $.proxy(fn[context], fn)
      }
    } else {
      throw new TypeError("expected function")
    }
  }

  $.fn.bind = function(event, data, callback){
    return this.on(event, data, callback)
  }
  $.fn.unbind = function(event, callback){
    return this.off(event, callback)
  }
  $.fn.one = function(event, selector, data, callback){
    return this.on(event, selector, data, callback, 1)
  }

  var returnTrue = function(){return true},
      returnFalse = function(){return false},
      ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/,
      eventMethods = {
        preventDefault: 'isDefaultPrevented',
        stopImmediatePropagation: 'isImmediatePropagationStopped',
        stopPropagation: 'isPropagationStopped'
      }

  function compatible(event, source) {
    if (source || !event.isDefaultPrevented) {
      source || (source = event)

      $.each(eventMethods, function(name, predicate) {
        var sourceMethod = source[name]
        event[name] = function(){
          this[predicate] = returnTrue
          return sourceMethod && sourceMethod.apply(source, arguments)
        }
        event[predicate] = returnFalse
      })

      if (source.defaultPrevented !== undefined ? source.defaultPrevented :
          'returnValue' in source ? source.returnValue === false :
          source.getPreventDefault && source.getPreventDefault())
        event.isDefaultPrevented = returnTrue
    }
    return event
  }

  function createProxy(event) {
    var key, proxy = { originalEvent: event }
    for (key in event)
      if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

    return compatible(proxy, event)
  }

  $.fn.delegate = function(selector, event, callback){
    return this.on(event, selector, callback)
  }
  $.fn.undelegate = function(selector, event, callback){
    return this.off(event, selector, callback)
  }

  $.fn.live = function(event, callback){
    $(document.body).delegate(this.selector, event, callback)
    return this
  }
  $.fn.die = function(event, callback){
    $(document.body).undelegate(this.selector, event, callback)
    return this
  }

  $.fn.on = function(event, selector, data, callback, one){
    var autoRemove, delegator, $this = this
    if (event && !isString(event)) {
      $.each(event, function(type, fn){
        $this.on(type, selector, data, fn, one)
      })
      return $this
    }

    if (!isString(selector) && !isFunction(callback) && callback !== false)
      callback = data, data = selector, selector = undefined
    if (callback === undefined || data === false)
      callback = data, data = undefined

    if (callback === false) callback = returnFalse

    return $this.each(function(_, element){
      if (one) autoRemove = function(e){
        remove(element, e.type, callback)
        return callback.apply(this, arguments)
      }

      if (selector) delegator = function(e){
        var evt, match = $(e.target).closest(selector, element).get(0)
        if (match && match !== element) {
          evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
          return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
        }
      }

      add(element, event, callback, data, selector, delegator || autoRemove)
    })
  }
  $.fn.off = function(event, selector, callback){
    var $this = this
    if (event && !isString(event)) {
      $.each(event, function(type, fn){
        $this.off(type, selector, fn)
      })
      return $this
    }

    if (!isString(selector) && !isFunction(callback) && callback !== false)
      callback = selector, selector = undefined

    if (callback === false) callback = returnFalse

    return $this.each(function(){
      remove(this, event, callback, selector)
    })
  }

  $.fn.trigger = function(event, args){
    event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
    event._args = args
    return this.each(function(){
      // handle focus(), blur() by calling them directly
      if (event.type in focus && typeof this[event.type] == "function") this[event.type]()
      // items in the collection might not be DOM elements
      else if ('dispatchEvent' in this) this.dispatchEvent(event)
      else $(this).triggerHandler(event, args)
    })
  }

  // triggers event handlers on current element just as if an event occurred,
  // doesn't trigger an actual event, doesn't bubble
  $.fn.triggerHandler = function(event, args){
    var e, result
    this.each(function(i, element){
      e = createProxy(isString(event) ? $.Event(event) : event)
      e._args = args
      e.target = element
      $.each(findHandlers(element, event.type || event), function(i, handler){
        result = handler.proxy(e)
        if (e.isImmediatePropagationStopped()) return false
      })
    })
    return result
  }

  // shortcut methods for `.bind(event, fn)` for each event type
  ;('focusin focusout focus blur load resize scroll unload click dblclick '+
  'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+
  'change select keydown keypress keyup error').split(' ').forEach(function(event) {
    $.fn[event] = function(callback) {
      return (0 in arguments) ?
        this.bind(event, callback) :
        this.trigger(event)
    }
  })

  $.Event = function(type, props) {
    if (!isString(type)) props = type, type = props.type
    var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true
    if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
    event.initEvent(type, bubbles, true)
    return compatible(event)
  }

})(Zepto)

;(function($){
  var jsonpID = 0,
      document = window.document,
      key,
      name,
      rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
      scriptTypeRE = /^(?:text|application)\/javascript/i,
      xmlTypeRE = /^(?:text|application)\/xml/i,
      jsonType = 'application/json',
      htmlType = 'text/html',
      blankRE = /^\s*$/,
      originAnchor = document.createElement('a')

  originAnchor.href = window.location.href

  // trigger a custom event and return false if it was cancelled
  function triggerAndReturn(context, eventName, data) {
    var event = $.Event(eventName)
    $(context).trigger(event, data)
    return !event.isDefaultPrevented()
  }

  // trigger an Ajax "global" event
  function triggerGlobal(settings, context, eventName, data) {
    if (settings.global) return triggerAndReturn(context || document, eventName, data)
  }

  // Number of active Ajax requests
  $.active = 0

  function ajaxStart(settings) {
    if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
  }
  function ajaxStop(settings) {
    if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
  }

  // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
  function ajaxBeforeSend(xhr, settings) {
    var context = settings.context
    if (settings.beforeSend.call(context, xhr, settings) === false ||
        triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
      return false

    triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
  }
  function ajaxSuccess(data, xhr, settings, deferred) {
    var context = settings.context, status = 'success'
    settings.success.call(context, data, status, xhr)
    if (deferred) deferred.resolveWith(context, [data, status, xhr])
    triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
    ajaxComplete(status, xhr, settings)
  }
  // type: "timeout", "error", "abort", "parsererror"
  function ajaxError(error, type, xhr, settings, deferred) {
    var context = settings.context
    settings.error.call(context, xhr, type, error)
    if (deferred) deferred.rejectWith(context, [xhr, type, error])
    triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
    ajaxComplete(type, xhr, settings)
  }
  // status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
  function ajaxComplete(status, xhr, settings) {
    var context = settings.context
    settings.complete.call(context, xhr, status)
    triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
    ajaxStop(settings)
  }

  // Empty function, used as default callback
  function empty() {}

  $.ajaxJSONP = function(options, deferred){
    if (!('type' in options)) return $.ajax(options)

    var _callbackName = options.jsonpCallback,
      callbackName = ($.isFunction(_callbackName) ?
        _callbackName() : _callbackName) || ('jsonp' + (++jsonpID)),
      script = document.createElement('script'),
      originalCallback = window[callbackName],
      responseData,
      abort = function(errorType) {
        $(script).triggerHandler('error', errorType || 'abort')
      },
      xhr = { abort: abort }, abortTimeout

    if (deferred) deferred.promise(xhr)

    $(script).on('load error', function(e, errorType){
      clearTimeout(abortTimeout)
      $(script).off().remove()

      if (e.type == 'error' || !responseData) {
        ajaxError(null, errorType || 'error', xhr, options, deferred)
      } else {
        ajaxSuccess(responseData[0], xhr, options, deferred)
      }

      window[callbackName] = originalCallback
      if (responseData && $.isFunction(originalCallback))
        originalCallback(responseData[0])

      originalCallback = responseData = undefined
    })

    if (ajaxBeforeSend(xhr, options) === false) {
      abort('abort')
      return xhr
    }

    window[callbackName] = function(){
      responseData = arguments
    }

    script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
    document.head.appendChild(script)

    if (options.timeout > 0) abortTimeout = setTimeout(function(){
      abort('timeout')
    }, options.timeout)

    return xhr
  }

  $.ajaxSettings = {
    // Default type of request
    type: 'GET',
    // Callback that is executed before request
    beforeSend: empty,
    // Callback that is executed if the request succeeds
    success: empty,
    // Callback that is executed the the server drops error
    error: empty,
    // Callback that is executed on request complete (both: error and success)
    complete: empty,
    // The context for the callbacks
    context: null,
    // Whether to trigger "global" Ajax events
    global: true,
    // Transport
    xhr: function () {
      return new window.XMLHttpRequest()
    },
    // MIME types mapping
    // IIS returns Javascript as "application/x-javascript"
    accepts: {
      script: 'text/javascript, application/javascript, application/x-javascript',
      json:   jsonType,
      xml:    'application/xml, text/xml',
      html:   htmlType,
      text:   'text/plain'
    },
    // Whether the request is to another domain
    crossDomain: false,
    // Default timeout
    timeout: 0,
    // Whether data should be serialized to string
    processData: true,
    // Whether the browser should be allowed to cache GET responses
    cache: true
  }

  function mimeToDataType(mime) {
    if (mime) mime = mime.split(';', 2)[0]
    return mime && ( mime == htmlType ? 'html' :
      mime == jsonType ? 'json' :
      scriptTypeRE.test(mime) ? 'script' :
      xmlTypeRE.test(mime) && 'xml' ) || 'text'
  }

  function appendQuery(url, query) {
    if (query == '') return url
    return (url + '&' + query).replace(/[&?]{1,2}/, '?')
  }

  // serialize payload and append it to the URL for GET requests
  function serializeData(options) {
    if (options.processData && options.data && $.type(options.data) != "string")
      options.data = $.param(options.data, options.traditional)
    if (options.data && (!options.type || options.type.toUpperCase() == 'GET'))
      options.url = appendQuery(options.url, options.data), options.data = undefined
  }

  $.ajax = function(options){
    var settings = $.extend({}, options || {}),
        deferred = $.Deferred && $.Deferred(),
        urlAnchor, hashIndex
    for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]

    ajaxStart(settings)

    if (!settings.crossDomain) {
      urlAnchor = document.createElement('a')
      urlAnchor.href = settings.url
      // cleans up URL for .href (IE only), see https://github.com/madrobby/zepto/pull/1049
      urlAnchor.href = urlAnchor.href
      settings.crossDomain = (originAnchor.protocol + '//' + originAnchor.host) !== (urlAnchor.protocol + '//' + urlAnchor.host)
    }

    if (!settings.url) settings.url = window.location.toString()
    if ((hashIndex = settings.url.indexOf('#')) > -1) settings.url = settings.url.slice(0, hashIndex)
    serializeData(settings)

    var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
    if (hasPlaceholder) dataType = 'jsonp'

    if (settings.cache === false || (
         (!options || options.cache !== true) &&
         ('script' == dataType || 'jsonp' == dataType)
        ))
      settings.url = appendQuery(settings.url, '_=' + Date.now())

    if ('jsonp' == dataType) {
      if (!hasPlaceholder)
        settings.url = appendQuery(settings.url,
          settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
      return $.ajaxJSONP(settings, deferred)
    }

    var mime = settings.accepts[dataType],
        headers = { },
        setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
        protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
        xhr = settings.xhr(),
        nativeSetHeader = xhr.setRequestHeader,
        abortTimeout

    if (deferred) deferred.promise(xhr)

    if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
    setHeader('Accept', mime || '*/*')
    if (mime = settings.mimeType || mime) {
      if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
      xhr.overrideMimeType && xhr.overrideMimeType(mime)
    }
    if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
      setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')

    if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
    xhr.setRequestHeader = setHeader

    xhr.onreadystatechange = function(){
      if (xhr.readyState == 4) {
        xhr.onreadystatechange = empty
        clearTimeout(abortTimeout)
        var result, error = false
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
          dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
          result = xhr.responseText

          try {
            // http://perfectionkills.com/global-eval-what-are-the-options/
            if (dataType == 'script')    (1,eval)(result)
            else if (dataType == 'xml')  result = xhr.responseXML
            else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result)
          } catch (e) { error = e }

          if (error) ajaxError(error, 'parsererror', xhr, settings, deferred)
          else ajaxSuccess(result, xhr, settings, deferred)
        } else {
          ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
        }
      }
    }

    if (ajaxBeforeSend(xhr, settings) === false) {
      xhr.abort()
      ajaxError(null, 'abort', xhr, settings, deferred)
      return xhr
    }

    if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]

    var async = 'async' in settings ? settings.async : true
    xhr.open(settings.type, settings.url, async, settings.username, settings.password)

    for (name in headers) nativeSetHeader.apply(xhr, headers[name])

    if (settings.timeout > 0) abortTimeout = setTimeout(function(){
        xhr.onreadystatechange = empty
        xhr.abort()
        ajaxError(null, 'timeout', xhr, settings, deferred)
      }, settings.timeout)

    // avoid sending empty string (#319)
    xhr.send(settings.data ? settings.data : null)
    return xhr
  }

  // handle optional data/success arguments
  function parseArguments(url, data, success, dataType) {
    if ($.isFunction(data)) dataType = success, success = data, data = undefined
    if (!$.isFunction(success)) dataType = success, success = undefined
    return {
      url: url
    , data: data
    , success: success
    , dataType: dataType
    }
  }

  $.get = function(/* url, data, success, dataType */){
    return $.ajax(parseArguments.apply(null, arguments))
  }

  $.post = function(/* url, data, success, dataType */){
    var options = parseArguments.apply(null, arguments)
    options.type = 'POST'
    return $.ajax(options)
  }

  $.getJSON = function(/* url, data, success */){
    var options = parseArguments.apply(null, arguments)
    options.dataType = 'json'
    return $.ajax(options)
  }

  $.fn.load = function(url, data, success){
    if (!this.length) return this
    var self = this, parts = url.split(/\s/), selector,
        options = parseArguments(url, data, success),
        callback = options.success
    if (parts.length > 1) options.url = parts[0], selector = parts[1]
    options.success = function(response){
      self.html(selector ?
        $('<div>').html(response.replace(rscript, "")).find(selector)
        : response)
      callback && callback.apply(self, arguments)
    }
    $.ajax(options)
    return this
  }

  var escape = encodeURIComponent

  function serialize(params, obj, traditional, scope){
    var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
    $.each(obj, function(key, value) {
      type = $.type(value)
      if (scope) key = traditional ? scope :
        scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
      // handle data in serializeArray() format
      if (!scope && array) params.add(value.name, value.value)
      // recurse into nested objects
      else if (type == "array" || (!traditional && type == "object"))
        serialize(params, value, traditional, key)
      else params.add(key, value)
    })
  }

  $.param = function(obj, traditional){
    var params = []
    params.add = function(key, value) {
      if ($.isFunction(value)) value = value()
      if (value == null) value = ""
      this.push(escape(key) + '=' + escape(value))
    }
    serialize(params, obj, traditional)
    return params.join('&').replace(/%20/g, '+')
  }
})(Zepto)

;(function($){
  var zepto = $.zepto, oldQsa = zepto.qsa, oldMatches = zepto.matches

  function visible(elem){
    elem = $(elem)
    return !!(elem.width() || elem.height()) && elem.css("display") !== "none"
  }

  // Implements a subset from:
  // http://api.jquery.com/category/selectors/jquery-selector-extensions/
  //
  // Each filter function receives the current index, all nodes in the
  // considered set, and a value if there were parentheses. The value
  // of `this` is the node currently being considered. The function returns the
  // resulting node(s), null, or undefined.
  //
  // Complex selectors are not supported:
  //   li:has(label:contains("foo")) + li:has(label:contains("bar"))
  //   ul.inner:first > li
  var filters = $.expr[':'] = {
    visible:  function(){ if (visible(this)) return this },
    hidden:   function(){ if (!visible(this)) return this },
    selected: function(){ if (this.selected) return this },
    checked:  function(){ if (this.checked) return this },
    parent:   function(){ return this.parentNode },
    first:    function(idx){ if (idx === 0) return this },
    last:     function(idx, nodes){ if (idx === nodes.length - 1) return this },
    eq:       function(idx, _, value){ if (idx === value) return this },
    contains: function(idx, _, text){ if ($(this).text().indexOf(text) > -1) return this },
    has:      function(idx, _, sel){ if (zepto.qsa(this, sel).length) return this }
  }

  var filterRe = new RegExp('(.*):(\\w+)(?:\\(([^)]+)\\))?$\\s*'),
      childRe  = /^\s*>/,
      classTag = 'Zepto' + (+new Date())

  function process(sel, fn) {
    // quote the hash in `a[href^=#]` expression
    sel = sel.replace(/=#\]/g, '="#"]')
    var filter, arg, match = filterRe.exec(sel)
    if (match && match[2] in filters) {
      filter = filters[match[2]], arg = match[3]
      sel = match[1]
      if (arg) {
        var num = Number(arg)
        if (isNaN(num)) arg = arg.replace(/^["']|["']$/g, '')
        else arg = num
      }
    }
    return fn(sel, filter, arg)
  }

  zepto.qsa = function(node, selector) {
    return process(selector, function(sel, filter, arg){
      try {
        var taggedParent
        if (!sel && filter) sel = '*'
        else if (childRe.test(sel))
          // support "> *" child queries by tagging the parent node with a
          // unique class and prepending that classname onto the selector
          taggedParent = $(node).addClass(classTag), sel = '.'+classTag+' '+sel

        var nodes = oldQsa(node, sel)
      } catch(e) {
        console.error('error performing selector: %o', selector)
        throw e
      } finally {
        if (taggedParent) taggedParent.removeClass(classTag)
      }
      return !filter ? nodes :
        zepto.uniq($.map(nodes, function(n, i){ return filter.call(n, i, nodes, arg) }))
    })
  }

  zepto.matches = function(node, selector){
    return process(selector, function(sel, filter, arg){
      return (!sel || oldMatches(node, sel)) &&
        (!filter || filter.call(node, null, arg) === node)
    })
  }
})(Zepto)

;(function($){
  var slice = Array.prototype.slice

  function Deferred(func) {
    var tuples = [
          // action, add listener, listener list, final state
          [ "resolve", "done", $.Callbacks({once:1, memory:1}), "resolved" ],
          [ "reject", "fail", $.Callbacks({once:1, memory:1}), "rejected" ],
          [ "notify", "progress", $.Callbacks({memory:1}) ]
        ],
        state = "pending",
        promise = {
          state: function() {
            return state
          },
          always: function() {
            deferred.done(arguments).fail(arguments)
            return this
          },
          then: function(/* fnDone [, fnFailed [, fnProgress]] */) {
            var fns = arguments
            return Deferred(function(defer){
              $.each(tuples, function(i, tuple){
                var fn = $.isFunction(fns[i]) && fns[i]
                deferred[tuple[1]](function(){
                  var returned = fn && fn.apply(this, arguments)
                  if (returned && $.isFunction(returned.promise)) {
                    returned.promise()
                      .done(defer.resolve)
                      .fail(defer.reject)
                      .progress(defer.notify)
                  } else {
                    var context = this === promise ? defer.promise() : this,
                        values = fn ? [returned] : arguments
                    defer[tuple[0] + "With"](context, values)
                  }
                })
              })
              fns = null
            }).promise()
          },

          promise: function(obj) {
            return obj != null ? $.extend( obj, promise ) : promise
          }
        },
        deferred = {}

    $.each(tuples, function(i, tuple){
      var list = tuple[2],
          stateString = tuple[3]

      promise[tuple[1]] = list.add

      if (stateString) {
        list.add(function(){
          state = stateString
        }, tuples[i^1][2].disable, tuples[2][2].lock)
      }

      deferred[tuple[0]] = function(){
        deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments)
        return this
      }
      deferred[tuple[0] + "With"] = list.fireWith
    })

    promise.promise(deferred)
    if (func) func.call(deferred, deferred)
    return deferred
  }

  $.when = function(sub) {
    var resolveValues = slice.call(arguments),
        len = resolveValues.length,
        i = 0,
        remain = len !== 1 || (sub && $.isFunction(sub.promise)) ? len : 0,
        deferred = remain === 1 ? sub : Deferred(),
        progressValues, progressContexts, resolveContexts,
        updateFn = function(i, ctx, val){
          return function(value){
            ctx[i] = this
            val[i] = arguments.length > 1 ? slice.call(arguments) : value
            if (val === progressValues) {
              deferred.notifyWith(ctx, val)
            } else if (!(--remain)) {
              deferred.resolveWith(ctx, val)
            }
          }
        }

    if (len > 1) {
      progressValues = new Array(len)
      progressContexts = new Array(len)
      resolveContexts = new Array(len)
      for ( ; i < len; ++i ) {
        if (resolveValues[i] && $.isFunction(resolveValues[i].promise)) {
          resolveValues[i].promise()
            .done(updateFn(i, resolveContexts, resolveValues))
            .fail(deferred.reject)
            .progress(updateFn(i, progressContexts, progressValues))
        } else {
          --remain
        }
      }
    }
    if (!remain) deferred.resolveWith(resolveContexts, resolveValues)
    return deferred.promise()
  }

  $.Deferred = Deferred
})(Zepto)

;(function($){
  // Create a collection of callbacks to be fired in a sequence, with configurable behaviour
  // Option flags:
  //   - once: Callbacks fired at most one time.
  //   - memory: Remember the most recent context and arguments
  //   - stopOnFalse: Cease iterating over callback list
  //   - unique: Permit adding at most one instance of the same callback
  $.Callbacks = function(options) {
    options = $.extend({}, options)

    var memory, // Last fire value (for non-forgettable lists)
        fired,  // Flag to know if list was already fired
        firing, // Flag to know if list is currently firing
        firingStart, // First callback to fire (used internally by add and fireWith)
        firingLength, // End of the loop when firing
        firingIndex, // Index of currently firing callback (modified by remove if needed)
        list = [], // Actual callback list
        stack = !options.once && [], // Stack of fire calls for repeatable lists
        fire = function(data) {
          memory = options.memory && data
          fired = true
          firingIndex = firingStart || 0
          firingStart = 0
          firingLength = list.length
          firing = true
          for ( ; list && firingIndex < firingLength ; ++firingIndex ) {
            if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) {
              memory = false
              break
            }
          }
          firing = false
          if (list) {
            if (stack) stack.length && fire(stack.shift())
            else if (memory) list.length = 0
            else Callbacks.disable()
          }
        },

        Callbacks = {
          add: function() {
            if (list) {
              var start = list.length,
                  add = function(args) {
                    $.each(args, function(_, arg){
                      if (typeof arg === "function") {
                        if (!options.unique || !Callbacks.has(arg)) list.push(arg)
                      }
                      else if (arg && arg.length && typeof arg !== 'string') add(arg)
                    })
                  }
              add(arguments)
              if (firing) firingLength = list.length
              else if (memory) {
                firingStart = start
                fire(memory)
              }
            }
            return this
          },
          remove: function() {
            if (list) {
              $.each(arguments, function(_, arg){
                var index
                while ((index = $.inArray(arg, list, index)) > -1) {
                  list.splice(index, 1)
                  // Handle firing indexes
                  if (firing) {
                    if (index <= firingLength) --firingLength
                    if (index <= firingIndex) --firingIndex
                  }
                }
              })
            }
            return this
          },
          has: function(fn) {
            return !!(list && (fn ? $.inArray(fn, list) > -1 : list.length))
          },
          empty: function() {
            firingLength = list.length = 0
            return this
          },
          disable: function() {
            list = stack = memory = undefined
            return this
          },
          disabled: function() {
            return !list
          },
          lock: function() {
            stack = undefined;
            if (!memory) Callbacks.disable()
            return this
          },
          locked: function() {
            return !stack
          },
          fireWith: function(context, args) {
            if (list && (!fired || stack)) {
              args = args || []
              args = [context, args.slice ? args.slice() : args]
              if (firing) stack.push(args)
              else fire(args)
            }
            return this
          },
          fire: function() {
            return Callbacks.fireWith(this, arguments)
          },
          fired: function() {
            return !!fired
          }
        }

    return Callbacks
  }
})(Zepto)
;
$.expr[":"].input = function() {
	if (/^(?:INPUT|SELECT|TEXTAREA)$/.test(this.nodeName)) {
		return this;
	}
};
$.ajaxSettings.dataType = "html";
$.ajaxSettings.accepts.html = "text/html; charset=windows-31j";

var Env = (function() {
	var IS_CHROME = typeof chrome === 'object' && chrome.extension;
	return {
		IS_CHROME: IS_CHROME,
		IS_GM: typeof GM_setValue === "function",
		IS_FIREFOX: typeof InstallTrigger !== 'undefined',
		IS_LOCAL: location.protocol === "file:",
		storage: IS_CHROME && chrome.storage.local,
	};
})();

var ConfigMethods = {
	addID: function(type, id) {
		var target = "vanished" + type + "IDs";
		Config.get(target, function(ids) {
			ids = Array.isArray(ids) ? ids : [];
			if (ids.indexOf(id) === -1) {
				ids.push(id);
				ids.sort(function(l, r) {
					return +r - l;
				});
				this.vanishedMessageIDs = ids;
				Config.set(target, ids);
			}
		}.bind(this));
	},
	removeID: function(type, id) {
		var target = "vanished" + type + "IDs";
		Config.get(target, function(ids) {
			ids = Array.isArray(ids) ? ids : [];
			var index = ids.indexOf(id);
			if (index !== -1) {
				ids.splice(index, 1);
				this.vanishedMessageIDs = ids;
				if (ids.length) {
					Config.set(target, ids);
				} else {
					Config.remove(target);
				}
			}
		}.bind(this));
	},
	clearIDs: function(type) {
		var target = "vanished" + type + "IDs";
		Config.remove(target);
		this[target] = [];
	},

	/** @param {String} id */
	addVanishedMessage: function(id) {
		this.addID("Message", id);
	},
	removeVanishedMessage: function(id) {
		this.removeID("Message", id);
	},
	clearVanishedMessageIDs: function() {
		this.clearIDs("Message");
	},

	/** @param {String} id */
	addVanishedThread: function(id) {
		this.addID("Thread", id);
	},
	removeVanishedThread: function(id) {
		this.removeID("Thread", id);
	},
	clearVanishedThreadIDs: function() {
		this.clearIDs("Thread");
	},
	clearVanish: function() {
		this.clearVanishedMessageIDs();
		this.clearVanishedThreadIDs();
	},
	clear: function() {
		Config.clear();
		$.extend(true, this, Config.prototype, ConfigMethods);
	},

	isTreeView: function() {
		return this.viewMode === "t";
	}
};

function Config() {
	Config.load(this);
}

Config.prototype = {
	treeMode: "tree-mode-ascii",
	thumbnail: true,
	thumbnailPopup: true,
	popupAny: false,
	popupMaxWidth: "",
	popupMaxHeight: "",
	popupMode: "center",
	popupBestFit: true,
	threadOrder: "ascending",
	NGHandle: "",
	NGWord: "",
	useNG: true,
	NGCheckMode: false,
	spacingBetweenMessages: Env.IS_LOCAL,
	useVanishThread: true,
	vanishedThreadIDs: [], //扱い注意
	utterlyVanishNGThread: false,
	useVanishMessage: Env.IS_LOCAL,
	vanishedMessageIDs: [],
	vanishMessageAggressive: false,
	utterlyVanishMessage: false,
	utterlyVanishNGStack: false,
	deleteOriginal: !Env.IS_LOCAL,
	zero: true,
	accesskeyReload: "R",
	accesskeyV: "",
	fc2: true,
	keyboardNavigation: false,
	keyboardNavigationOffsetTop: "200",
	viewMode: "t",
	css: "",
	linkAnimation: true,
	shouki: true,
	closeResWindow: false,
	maxLine: Env.IS_LOCAL ? "4" : "",
	openLinkInNewTab: Env.IS_LOCAL,
	characterEntity: true,
};
Config.load = function(config) {
	var i, keys, key, value;
	if (Env.IS_GM) {
		keys = Object.keys(Config.prototype);
		i = keys.length;
		while (i--) {
			key = keys[i];
			value = GM_getValue(key);
			if (value != null) {
				config[key] = JSON.parse(value);
			}
		}
	} else {
		i = localStorage.length;
		while (i--) {
			key = localStorage.key(i);
			if (Config.prototype.hasOwnProperty(key)) {
				value = localStorage.getItem(key);
				if (value !== null) {
					config[key] = JSON.parse(value);
				}
			}
		}
	}
};
Config.mixin = function(data) {
	return $.extend(data, ConfigMethods);
};
Config.loadFromLocal = function() {
	return Config.mixin(new Config());
};
Config.loadFromChromeStorage = function() {
	return new Promise(function(resolve) {
		function doResolve(data) {
			resolve(Config.mixin(data));
		}
		Env.storage.get(Config.prototype, doResolve);
	});
};
Config.remove = function(key) {
	if (Env.IS_GM) {
		GM_deleteValue(key);
	} else if (Env.IS_CHROME) {
		Env.storage.remove(key);
	} else {
		localStorage.removeItem(key);
	}
};
Config.set = function(key, value) {
	var json = JSON.stringify(value);
	if (Env.IS_GM) {
		GM_setValue(key, json);
	} else if (Env.IS_CHROME) {
		var item = {};
		item[key] = value;
		Env.storage.set(item);
	} else {
		localStorage.setItem(key, json);
	}
};
Config.setAll = function(items) {
	if (Env.IS_CHROME) {
		Env.storage.set(items);
	} else {
		for (var key in items) {
			Config.set(key, items[key]);
		}
	}
};
Config.clear = function() {
	var i, key;
	if (Env.IS_GM) {
		Object.keys(Config.prototype).forEach(GM_deleteValue);
	} else if (Env.IS_CHROME) {
		Env.storage.clear();
	} else {
		i = localStorage.length;
		while (i--) {
			key = localStorage.key(i);
			if (Config.prototype.hasOwnProperty(key)) {
				localStorage.removeItem(key);
			}
		}
	}
};
Config.get = function(key, fun) {
	if (Env.IS_GM) {
		fun(JSON.parse(GM_getValue(key, "null")));
	} else if (Env.IS_CHROME) {
		Env.storage.get(key, function(item) {
			fun(item[key]);
		});
	} else {
		fun(JSON.parse(localStorage.getItem(key)));
	}
};
if (!Env.IS_GM && window.chrome && !window.jasmine) {
	Config.promise = Config.loadFromChromeStorage();
}

function ConfigController(item) {
	this.item = item;
	var el = document.createElement("form");
	el.id = "config";
	this.$el = $(el);

	var events = [
		"save",
		"clear",
		"close",
		"clearVanishThread",
		"clearVanishMessage",
		"addToNGWord",
	];
	for (var i = events.length - 1; i >= 0; i--) {
		var event = events[i];
		this.$el.on("click", "#" + event, this[event].bind(this));
	}

	this.$el.on('keyup', '#quote-input', this.quotemeta.bind(this));

	this.render();
	return el;
}
ConfigController.prototype = {
	$: function(selector) {
		return $(selector, this.$el);
	},
	render: function() {
		this.$el.append(this.template());
		if (Env.IS_CHROME) {
			this.$("#close").remove();
		}
		this.restore();
	},
	template: function() {
		return '<style type="text/css">\
			<!--\
				li {\
					list-style-type: none;\
				}\
				#configInfo {\
					font-weight: bold;\
					font-style: italic;\
				}\
				legend + ul {\
					margin: 0 0 0 0;\
				}\
			-->\
			</style>\
			<fieldset>\
				<legend>設定</legend>\
				<fieldset>\
					<legend>表示</legend>\
					<ul>\
						<li><label><input type="radio" name="viewMode" value="t">ツリー表示</label></li>\
						<li><label><input type="radio" name="viewMode" value="s">スタック表示</label> <em> 対応していない機能もあります</em></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>共通</legend>\
					<ul>\
						<li><label><input type="checkbox" name="zero">常に0件リロード</label><em>(チェックを外しても「表示件数」は0のままなので手動で直してね)</em></li>\
						<li><label>未読リロードに使うアクセスキー<input type="text" name="accesskeyReload" size="1"></label></li>\
						<li><label>内容欄へのアクセスキー<input type="text" name="accesskeyV" size="1"></label></li>\
						<li><label><input type="checkbox" name="fc2">fc2とamebaを直接開けるようにする(リファラーを送らない)</label><em>opera12以外</em></li>\
						<li><label><input type="checkbox" name="keyboardNavigation">jkで移動、rでレス窓開く</label><em><a href="https://greasyfork.org/scripts/1971-tree-view-for-qwerty#keyboardNavigation">chrome以外の人は説明を読む</a></em></li>\
						<ul>\
							<li><label>上から<input type="text" name="keyboardNavigationOffsetTop" size="4">pxの位置に合わせる</label></li>\
						</ul>\
						<li><label><input type="checkbox" name="closeResWindow">書き込み完了した窓を閉じる</label> <em><a href="https://greasyfork.org/scripts/1971-tree-view-for-qwerty#close-tab-in-firefox">firefoxは説明を読むこと</a></em><li>\
						<li><label><input type="checkbox" name="openLinkInNewTab">target属性の付いたリンクを常に新しいタブで開く</label></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>ツリーのみ</legend>\
						<ul style="display:inline-block">\
							<li><label><input type="checkbox" name="deleteOriginal">ツリー表示のとき、元の投稿を非表示にする</label></li>\
							<li>スレッドの表示順\
								<ul>\
									<li><label><input type="radio" name="threadOrder" value="ascending">古→新</label></li>\
									<li><label><input type="radio" name="threadOrder" value="descending">新→古</label></li>\
								</ul>\
							</li>\
							<li>ツリーの表示に使うのは\
								<ul>\
									<li><label><input type="radio" name="treeMode" value="tree-mode-css">CSS</label></li>\
									<li><label><input type="radio" name="treeMode" value="tree-mode-ascii">文字</label></li>\
								</ul>\
							</li>\
							<li><label><input type="checkbox" name="spacingBetweenMessages">記事の間隔を開ける</label></li>\
							<li><label><input type="text" name="maxLine" size="2">行以上は省略する</label></li>\
							<li><label><input type="checkbox" name="characterEntity">数値文字参照を展開</label> <em>(&#数字;が置き換わる)</em></li>\
						</ul>\
					</ul>\
				<fieldset style="display:inline-block">\
					<legend>非表示設定</legend>\
					<ul>\
						<li><label><input type="checkbox" name="useVanishThread">スレッド非表示機能を使う</label><li>\
						<ul>\
							<li><span id="vanishedThreadIDs"></span>個のスレッドを非表示中<input type="button" value="クリア" id="clearVanishThread"></li>\
							<li><label><input type="checkbox" name="utterlyVanishNGThread">完全に非表示</label></li>\
						</ul>\
					</ul>\
					<ul>\
						<li><label><input type="checkbox" name="useVanishMessage">投稿非表示機能を使う</label> <em>使う前に<a href="https://greasyfork.org/scripts/1971-tree-view-for-qwerty#vanish">投稿非表示機能の注意点</a>を読むこと。</em><li>\
						<ul>\
							<li><span id="vanishedMessageIDs"></span>個の投稿を非表示中<input type="button" value="クリア" id="clearVanishMessage"></li>\
							<li><label><input type="checkbox" name="utterlyVanishMessage">完全に非表示</label></li>\
							<li><label><input type="checkbox" name="vanishMessageAggressive">パラノイア</label></li>\
						<ul>\
					<ul>\
				</fieldset>\
				</fieldset>\
				</ul>\
				<fieldset>\
					<legend>画像</legend>\
					<ul>\
						<li>\
							<label><input type="checkbox" name="thumbnail">小町と退避の画像のサムネイルを表示</label>\
							<ul>\
								<li>\
									<label><input type="checkbox" name="thumbnailPopup">ポップアップ表示</label>\
									<ul>\
										<li><label>最大幅:<input type="text" name="popupMaxWidth" size="5">px </label><label>最大高:<input type="text" name="popupMaxHeight" size="5">px <em>空欄で原寸表示</em></label></li>\
										<li><label><input type="radio" name="popupMode" value="followMouse">マウスを追う</label></li>\
										<li><label><input type="radio" name="popupMode" value="notFollowMouse">マウスを追わず、マウスの側に表示</label></li>\
										<li><label><input type="radio" name="popupMode" value="center">マウスを追わず、画面中央に表示</label></li>\
										<ul>\
											<li><label><input type="checkbox" name="popupBestFit">最大幅高未設定時にポップアップを画面サイズに合わせる</label><em>「画面中央に表示」の時のみ</em></li>\
										</ul>\
									</ul>\
								</li>\
								<li><label><input type="checkbox" name="linkAnimation">描画アニメがある場合にリンクする</label></li>\
								<li><label><input type="checkbox" name="shouki">詳希(;゚Д゚)</label></li>\
							</ul>\
						</li>\
						<li><label><input type="checkbox" name="popupAny">小町と退避以外の画像もポップアップ</label></li>\
					</ul>\
				</fieldset>\
				<fieldset>\
					<legend>NGワード</legend>\
					<ul>\
						<li><label><input type="checkbox" name="useNG">NGワードを使う</label>\
						<p>指定には正規表現を使う。以下簡易説明。複数指定するには|(縦棒)で"区切る"(先頭や末尾につけてはいけない)。()?*+[]{}^$.の前には\\を付ける。</p>\
						<li><table>\
							<tr>\
								<td><label for="NGHandle">ハンドル</label>\
								<td><input id="NGHandle" type="text" name="NGHandle" size="30"><em>投稿者とメールと題名</em>\
							<tr>\
								<td><label for="NGWord">本文</label>\
								<td><input id="NGWord" type="text" name="NGWord" size="30">\
							<tr><td><td><input id="quote-input" type="text" size="15" value=""> よく分からん人はここにNGワードを一つづつ入力して追加ボタンだ\
							<tr><td><td><input id="quote-output" type="text" size="15" readonly><input type="button" id="addToNGWord" value="本文に追加">\
						</table>\
						<li><label><input type="checkbox" name="NGCheckMode">NGワードを含む投稿を畳まない</label>\
						<li><label><input type="checkbox" name="utterlyVanishNGStack">スタック表示時に完全非表示</label>\
					</ul>\
				</fieldset>\
				<p>\
					<label>追加CSS<br><textarea name="css" cols="70" rows="5"></textarea></label>\
				</p>\
				<p>\
					<input type="submit" id="save" accesskey="s" value="保存(s)">\
					<input type="button" id="clear" style="float:right" value="デフォルトに戻す">\
					<input type="button" id="close" accesskey="c" value="閉じる(c)">\
					<span id="configInfo"></span>\
				</p>\
			</fieldset>';
	},
	quotemeta: function() {
		var output = this.$('#quote-output');
		var input = this.$('#quote-input');
		output.val(ConfigController.quotemeta(input.val()));
	},
	addToNGWord: function() {
		var output = this.$('#quote-output').val();
		if (!output.length) {
			return;
		}
		var word = this.$('#NGWord').val();
		if (word.length) {
			output = word + '|' + output;
		}
		this.$('#NGWord').val(output);
		this.$('#quote-output, #quote-input').val('');
	},
	save: function() {
		var items = {}, config = this.item;
		this.$(":input").each(function() {
			if (!this.name) {
				return;
			}
			var k = this.name, v = null;
			switch (this.type) {
				case "radio":
					if (this.checked) {
						v = this.value;
					}
					break;
				case "text":
				case "textarea":
					v = this.value;
					break;
				case "checkbox":
					v = this.checked;
					break;
			}
			if (v !== null) {
				config[k] = v;
				items[k] = v;
			}
		});
		Config.setAll(items);

		this.info("保存しました。");
		return false;
	},

	clear: function() {
		this.item.clear();
		this.restore();
		this.info("デフォルトに戻しました。");
	},

	close: function() {
		this.$el.remove();
		$(window).scrollTop(0);
	},

	clearVanishThread: function() {
		this.item.clearVanishedThreadIDs();
		this.$("#vanishedThreadIDs").text(0);
		this.info("非表示に設定されていたスレッドを解除しました。");
	},

	clearVanishMessage: function() {
		this.item.clearVanishedMessageIDs();
		this.$("#vanishedMessageIDs").text(0);
		this.info("非表示に設定されていた投稿を解除しました。");
	},

	info: function(text) {
		clearTimeout(this.id);
		var $info = this.$("#configInfo");
		$info.text(text);
		this.id = setTimeout(function() {
			$info.empty();
		}, 5000);
	},

	restore: function restore() {
		var config = this.item;
		this.$("#vanishedThreadIDs").text(config.vanishedThreadIDs.length);
		this.$("#vanishedMessageIDs").text(config.vanishedMessageIDs.length);

		this.$el.find(":input").each(function() {
			if (!this.name) {
				return;
			}
			switch (this.type) {
				case "radio":
					this.checked = config[this.name] === this.value;
					break;
				case "text":
				case "textarea":
					$(this).val(config[this.name]);
					break;
				case "checkbox":
					this.checked = config[this.name];
					break;
			}
		});
	}

};
ConfigController.quotemeta = function(str) {
	return (str + '').replace(/([()[\]{}|*+.^$?\\])/g, "\\$1");
};
;
if (!String.prototype.startsWith) {
	String.prototype.startsWith = function(start) {
		return this.lastIndexOf(start, 0) === 0;
	};
}
if (!String.prototype.endsWith) {
	Object.defineProperty(String.prototype, 'endsWith', {
		value: function (searchString, position) {
			var subjectString = this.toString();
			if (position === undefined || position > subjectString.length) {
				position = subjectString.length;
			}
			position -= searchString.length;
			var lastIndex = subjectString.indexOf(searchString, position);
			return lastIndex !== -1 && lastIndex === position;
		}
	});
}
if (!String.prototype.includes) {
	String.prototype.includes = function() {'use strict';
		return String.prototype.indexOf.apply(this, arguments) !== -1;
	};
}
if (!String.prototype.trimRight) {
	String.prototype.trimRight = function() {
		return this.replace(/\s+$/, "");
	};
}
if (!Date.prototype.toLocaleFormat) {
	Date.prototype.toLocaleFormat = function(format) {
		return format
			.replace(/%Y/, this.getFullYear())
			.replace(/%m/, ("0" + (this.getMonth() + 1)).slice(-2))
			.replace(/%d/, ("0" + this.getDate()).slice(-2));
	};
}

function identity(x) {
	return x;
}

function compose(/* f1, f2, ..., fn */) {
	var fns = arguments,
	length = arguments.length;
	return function() {
		var args = arguments, i = length;
		while (--i >= 0) {
			args = [fns[i].apply(this, args)];
		}
		return args[0];
	};
}

function curry2(fn) {
	return function(first) {
		return function(second) {
			return fn(first, second);
		};
	};
}

function curry3(fn) {
	return function(first) {
		return function(second) {
			return function(third) {
				return fn(first, second, third);
			};
		};
	};
}

function actions(acts, done) {
	return function(seed) {
		var initialState = { values: [], state: seed };

		var state = acts.reduce(function(state, action) {
			var result = action(state.state);
			var values = state.values.concat(result.value);
			return { values: values, state: result.state };
		}, initialState);

		var values = state.values.filter(function (value) {
			return value !== undefined;
		});

		return done(values, state.state);
	};
}

function doWhen(condition, action) {
	return condition ? action() : undefined;
}

function memoize(fn) {
	var cache = {};
	return function(arg) {
		if (!cache.hasOwnProperty(arg)) {
			cache[arg] = fn(arg);
		}
		return cache[arg];
	};
}

function Post(id) {
	this.id = id;

	this.parent = null; // {Post}
	this.child = null; // {Post}
	this.next = null; // {Post}
}
Post.fetchFromRemote = function(q, f) {
	var now = Date.now();
	var ONE_DAY = 24 * 60 * 60 * 1000;
	var beforeDates = [];
	var afterDates = [];
	var target = afterDates;
	var tmp = location.search.match(/ff=(\d{4})(\d{2})(\d{2})\.dat$/);
	var beforecontainers = [];
	var aftercontainers = [];
	var y = +tmp[1];
	var m = +tmp[2] - 1;
	var d = +tmp[3];

	function getDate(number) {
		return new Date(now - number * ONE_DAY);
	}

	function isReferenceDate(date) {
		return date.getDate() === d && date.getMonth() === m && date.getFullYear() === y;
	}

	function getLog(date) {
		var url = location.href.replace(/ff=\d+\.dat(?=$|#)/, date.toLocaleFormat('ff=%Y%m%d.dat'));
		return $.get(url).then(wrapWithDiv).then(function(container) {
			return {container: container, date: date};
		});
	}

	function hasOP(container) {
		return container.querySelector('a[name="' + q.s + '"]');
	}

	for (var i = 0; i < 7; i++) {
		var date = getDate(i);

		if (isReferenceDate(date)) {
			target = beforeDates;
		} else {
			target.push(date);
		}
	}

	var after = afterDates.map(getLog).reduce(function(sequence, logPromise) {
		return sequence.then(function() {
			return logPromise;
		}).then(function(data) {
			aftercontainers.push(data);
		});
	}, $.Deferred().resolve());

	var before = beforeDates.reduce(function(sequence, date) {
		return sequence.then(function(done) {
			if (done) {
				return done;
			}
			return getLog(date).then(function(data) {
				beforecontainers.push(data);
				return hasOP(data.container);
			});
		});
	}, $.Deferred().resolve(hasOP(f)));

	return $.when(before, after).then(function() {
		return {befores: beforecontainers, afters: aftercontainers};
	}).promise();
};
Post.makePosts = function(context) {
	var posts = [];
	var as = context.querySelectorAll("a[name]");
	var font = DOM.nextElement("FONT");
	var b = DOM.nextElement("B");
	var blockquote = DOM.nextElement("BLOCKQUOTE");

	for (var i = 0, len = as.length; i < len; i++) {
		var a = as[i];
		var post = new Post(a.name);
		posts.push(post);

		var header = font(a);

		post.title = header.firstChild.innerHTML;
		var named = b(header);
		post.name = named.innerHTML;

		var info = font(named);
		post.date = info.firstChild.nodeValue.trim().slice(4);//「投稿日:」削除
		post.resUrl = info.firstElementChild.href;
		post.threadUrl = info.lastElementChild.href;
		post.threadId = /&s=([^&]+)/.exec(post.threadUrl)[1];
		if (info.childElementCount === 3) {
			post.posterUrl = info.firstElementChild.nextElementSibling.href;
		} else {
			post.posterUrl = null;
		}

		var text = blockquote(info).firstElementChild.innerHTML;

		text = text.replace(/<\/?font[^>]*>/ig, "")
			.replace(/\r\n?/g, "\n")
			.slice(0, -1);

		if (text.includes("&lt;A")) {
			text = text.replace(
				//        "       </A>
				//firefox %22    %3C\/A%3E
				//chrome  &quot; &lt;\/A&gt;
				//opera   &quot; <\/A>
				/&lt;A href="<a href="(.*)(?:%22|&quot;)" target="link">\1"<\/a> target="link"&gt;<a href="\1(?:%3C\/A%3E|&lt;\/A&gt;|<\/A>)" target="link">\1&lt;\/A&gt;<\/a>/g,
				'<a href="$1" target="link">$1</a>'
			);
		}

		post.text = text;

		var reference = /\n\n<a href="(?:h[^"]+&amp;s=|#)(\d+)[^"]*">参考:([^<]+)<\/a>$/.exec(text);
		if (reference) {
			post.parentId = reference[1];
			post.parentDate = reference[2];
			text = text.slice(0, reference.index);
		} else {
			post.parentId = null;
			post.parentDate = null;
		}

		var url = /\n\n<[^<]+<\/a>$/.exec(text);
		if (url) {
			text = text.slice(0, url.index);
		}
		if (!text.includes("<") && text.includes(":")) {
			post.text = Post.relinkify(text) +
				(url ? url[0] : "") + (reference ? reference[0] : "");
		}
	}
	if (posts.length >= 2 && (+posts[0].id) < (+posts[1].id)) {
		posts.reverse();
	}

	return posts;
};
Post.byID = function(l, r) {
	return +l.id - +r.id;
};
Post.relinkify1stMatching = function(_, p1) {
	return Post.relinkify(p1);
};
Post.relinkify = function(url) {
	return url.replace(/(?:https?|ftp|gopher|telnet|whois|news):\/\/[\x21-\x7e]+/ig, '<a href="$&" target="link">$&</a>');
};
Post.prototype = {
	id: "", // {string} /^\d+$/
	title: " ", // {string}
	name: " ", // {string}
	date: "", // {string}
	resUrl: "", // {string}
	threadUrl: "", // {string}
	threadId: "", // {string}
	posterUrl: "", // {string}
	parentId: null, // {(string|null|undefined}} null: 親なし, undefined: 不明, everything else: ID 今は数字のみ受け付ける
	parentDate: "", // {string}
	text: "", // {string}

	showAll: false, // {boolean}
	rejectLevel: 0, // {number}
	isRead: false, // {boolean}

	isOP: function() {
		return this.id === this.threadId;
	},
	getText: function() {
		if (this.parentId) {
			return this.text.slice(0, this.text.lastIndexOf("\n\n"));//参考と空行を除去
		}

		return this.text;
	},
	getQuotedText: function() {
		var lines = this.text
			.replace(/&gt; &gt;.*\n/g, "")
			//target属性がないのは参考リンクのみ
			.replace(/<a href="[^"]+">参考:.*<\/a>/i, "")
			.replace(
				/<a href="[^"]+" target="link">([^<]+)<\/a>/ig,       //<A href=¥S+ target=¥"link¥">(¥S+)<¥/A>
				Post.relinkify1stMatching
			)
			.replace(/\n/g, "\n&gt; ");
		lines = ("&gt; " + lines + "\n")
			.replace(/\n&gt;[ \n\r\f\t]+\n/g, "\n")
			.replace(/\n&gt;[ \n\r\f\t]+\n$/, "\n");
		return lines;
	},
	makeParentText: function() {
		var text = this.text
			.replace(/^&gt; (.*\n?)|^.*\n?/mg, "$1")
			.replace(/\n$/, "")
			.replace(/^[ \n\r\f\t]*$/mg, "$&\n$&");

		//TODO 引用と本文の間に一行開ける
		//text = text.replace(/((?:&gt; .*\n)+)(.+)/, "$1\n$2"); //replace(/^(?!&gt; )/m, "\n$&");

		return text;// + "\n\n";
	},
	hasQuote: function() {
		return (/^&gt; /m).test(this.text);
	},
	mayHaveParent: function() {
		return this.isRead && !this.isOP() && this.hasQuote();
	}
};

var ImaginaryPostProperties = {
	text: {
		get: function() {
			var text, child = this.child;
			if (child.next) {
				var counter = {}, max = 0, candidate;
				do {
					candidate = child.makeParentText();
					counter[candidate] = ++counter[candidate] || 1;
					if (child.getText().replace(/^&gt; .*/mg, "").trim() !== "") {
						counter[candidate] += 2;
					}
				} while ((child = child.next));

				for (candidate in counter) {
					var number = counter[candidate];
					if (max < number) {
						max = +number;
						text = candidate;
					}
				}
			} else {
				text = child.makeParentText();
			}
			return Object.defineProperty(this, "text", {value: text}).text;
		}
	},
	getText: {
		value: function() {
			return this.text;
		}
	},
	isRead: {
		value: true
	}
};

function MergedPost(id, child) {
	this.id = id;
	this.name = child.title.replace(/^>/, "");
	this.date = child.parentDate;
	this.resUrl = child.resUrl.replace(/(\?|&)s=\d+/, "$1s=" + id);
	this.threadUrl = child.threadUrl;
	this.threadId = child.threadId;
	this.parentId = this.isOP() ? null : undefined;
	this.child = child;
	this.next = null;
	this.parent = null;
}
extend(MergedPost, Post, ImaginaryPostProperties);

function GhostPost(id, child) {
	this.id = id;
	this.child = child;
	child.parent = this;
	this.threadId = child.threadId;
	this.threadUrl = child.threadUrl;
	this.resUrlTemplate = child.child.resUrl;
}
extend(GhostPost, Post, $.extend({}, ImaginaryPostProperties, {
	resUrl: {
		get: function() {
			if (this.id) {
				return this.resUrlTemplate.replace(/(\?|&)s=\d+/, "$1s=" + this.id);
			}
		}
	}
}));
GhostPost.prototype.date = "?";

function Thread(id) {
	this.posts = [];
	this.id = id;
	this.postCount = 0;
}
Thread.cache = {};
Thread.getRoots = function(id) {
	return Thread.cache[id];
};
Thread.connect = function(allPosts) {
	var lastChild = Object.create(null);

	return function connect(roots, post) {
		allPosts[post.id] = post;
		var parentId = post.parentId;
		if (parentId) {
			var parent = allPosts[parentId];
			if (parent) {
				var child = lastChild[parentId];
				if (child) {
					child.next = post;
				} else {
					parent.child = post;
				}
			} else {
				parent = new MergedPost(parentId, post);
				allPosts[parentId] = parent;
				roots.push(parent);
			}
			post.parent = parent;
			lastChild[parentId] = post;
		} else {
			roots.push(post);
		}
		return roots;
	};
};
Thread.prototype = {
	getRoots: function(config, postParent, threshold) {
		var allPosts = Object.create(null);
		var id = this.id;

		var roots = this.posts.reduceRight(Thread.connect(allPosts), []);

		roots.sort(Post.byID);
		roots = roots.reduce(function(roots, post) {
			if (post.mayHaveParent()) {
				var parentID = postParent.find(post.id, id);
				var parent = allPosts[parentID];
				if (parent) {
					post.parentId = parentID;
					post.parent = parent;
					post.next = parent.child;
					parent.child = post;
				} else if (parentID !== null) {
					post.parentId = parentID;
					var ghost = new GhostPost(parentID, post);
					if (parentID) {
						allPosts[parentID] = ghost;
					}
					roots.push(ghost);
				} else {
					roots.push(post);
				}
			} else {
				roots.push(post);
			}
			return roots;
		}, []);

		this.postCount = this.posts.length;

		if (id <= threshold) {
			roots = this.processVanish(config, postParent, roots);
		}

		Thread.cache[id] = roots;
		return roots;
	},
	processVanish: function(config, postParent, roots) {
		var ids = config.vanishedMessageIDs;
		var utterlyVanishMessage = config.utterlyVanishMessage;
		var newRoots = [];
		var postCount = this.postCount;
		function setRejectLevel(post, generation, isRoot) {
			var id = post.id;
			var child = post.child;
			var next = post.next;
			var rejectLevel = 0;
			if (ids.indexOf(id) > -1) {
				rejectLevel = 3;
			} else if (generation > 0) {
				rejectLevel = generation;
			} else if (isRoot) {
				var parentID = postParent.find(id);
				if (parentID) {
					if (ids.indexOf(parentID) > -1) {
						rejectLevel = 2;
					} else {
						var grandParentID = postParent.find(parentID);
						if (grandParentID && ids.indexOf(grandParentID) > -1) {
							rejectLevel = 1;
						}
					}
				}
			}
			if (rejectLevel) {
				post.rejectLevel = rejectLevel;
				if (utterlyVanishMessage && !post.isRead) {
					postCount--;
				}
			}
			if (child) {
				child = setRejectLevel(child, rejectLevel - 1, false);
			}
			if (next) {
				next = setRejectLevel(next, generation, false);
			}

			if (utterlyVanishMessage) {
				if (!child && post.isRead) {
					return next;
				}
				post.child = child;
				post.next = next;
				if (isRoot && rejectLevel === 0) {
					newRoots.push(post);
				} else if (rejectLevel === 1 && child) {
					newRoots.push(child);
				}
				return rejectLevel === 3 ? next : post;
			}
		}

		for (var i = roots.length - 1; i >= 0; i--) {
			setRejectLevel(roots[i], 0, true);
		}

		if (utterlyVanishMessage) {
			roots = newRoots.sort(Post.byID);
		}

		this.postCount = postCount;

		return roots;
	},
	getDate: function() {
		return this.posts[0].date;
	},
	getNumber: function() {
		return this.postCount;
	},
	getID: function() {
		return this.id;
	},
	getURL: function() {
		return this.posts[0].threadUrl;
	},
};

var Posts = {
	checkCharacterEntity: function(data) {
		data.state.hasCharacterEntity = /&amp;#(?:\d+|x[\da-fA-F]+);/.test(data.value);
		return data;
	},
	characterEntity: function(config, post, data) {
		var state = data.state;

		state.expandCharacterEntity = state.hasCharacterEntity && (post.hasOwnProperty("characterEntity") ? post.characterEntity : config.characterEntity);
		if (state.expandCharacterEntity) {
			var iter = document.createNodeIterator(data.value, NodeFilter.SHOW_TEXT, null, false);  //operaは省略可能な第3,4引数も渡さないとエラーを吐く
			var node;
			while ((node = iter.nextNode())) {
				node.data = node.data.replace(/&#(\d+|x[0-9a-fA-F]+);/g, Posts.replaceCharacterEntity);
			}
		}

		return data;
	},
	replaceCharacterEntity: function(str, p1) {
		return String.fromCharCode(p1[0] === "x" ? parseInt(p1.slice(1), 16) : p1);
	},
	makeText: function(data) {
		//終わりの空行引用は消してレスする人がいる
		//引用の各行に空白を追加する人がいる
		var post = data.value;
		var text = post.getText();
		var parent = post.parent ? post.parent.getQuotedText() : "";

		if (post.showAll) {
			text = Posts.markQuote(text, parent);
		} else {
			if (text.startsWith(parent)) {
				text = text.slice(parent.length);
			} else {
				//整形して
				parent = Posts.trimRights(parent);
				text = Posts.trimRights(text);

				//もう一度
				if (text.startsWith(parent)) {
					text = text.slice(parent.length);
				} else {
					//深海式レスのチェック
					var parent2 = parent.split("\n").filter(function(line) {
						return !line.startsWith("&gt; &gt; ");
					}).join("\n");
					if (text.startsWith(parent2)) {
						text = text.slice(parent2.length);
					} else {
						text = Posts.markQuote(text, parent);
					}
				}
			}

			//全角空白も\sになる
			//空白のみの投稿が空投稿になる
			text = text.trimRight().replace(/^\s*\n/, "");

			if (text.length === 0) {
				text = '<span class="note">(空投稿)</span>';
			}
		}

		data.value = text;

		return data;
	},
	checkThumbnails: function(data) {
		data.state.mayHaveThumbnails = data.value.includes('<a');

		return data;
	},
	putThumbnails: function(config) {
		if (!config.thumbnail) {
			return identity;
		}

		var thumbnail = new Thumbnail(config);
		return function(data) {
			if (data.state.mayHaveThumbnails) {
				thumbnail.register(data.value);
			}

			return data;
		};
	},
	markNG: function(config) {
		function mark(re, reg, data) {
			if (re.test(data.value)) {
				data.value = data.value.replace(reg, "<strong class='NGWordHighlight'>$&</strong>");
				data.state.isNG = true;
			}

			return data;
		}

		var ng = new NG(config);
		var text = identity;
		var header = identity;
		var mark_ = curry3(mark);

		if (ng.NGWordRE) {
			text = mark_(ng.NGWordRE)(ng.NGWordREg);
		}
		if (ng.NGHandleRE) {
			header = mark_(ng.NGHandleRE)(ng.NGHandleREg);
		}

		return {text: text, header: header};
	},
	markQuote: function(text, parent) {
		var parentLines = parent.split("\n");
		parentLines.pop();
		var lines = text.split("\n");
		var i = Math.min(parentLines.length, lines.length);

		while (i--) {
			lines[i] = '<span class="quote' +
				(parentLines[i] === lines[i] ? '' : ' modified') +
				'">' + lines[i] + '</span>';
		}

		return lines.join("\n");
	},
	trimRights: function(string) {
		return string.replace(/^.+$/gm, function(str) {
			return str.trimRight();
		});
	},
	truncate: function(config, post, data) {
		if (!config.maxLine || post.showAll) {
			return data;
		}

		var text = data.value;
		var maxLine = +config.maxLine;
		var lines = text.split("\n");
		var length = lines.length;

		if (length > maxLine) {
			var truncation = post.hasOwnProperty("truncation") ? post.truncation : true;
			var label;

			if (truncation) {
				lines[maxLine] = '<span class="truncation">' + lines[maxLine];
				text = lines.join("\n") + "\n</span>";
				label = '以下' + (length - maxLine) + '行省略';
			} else {
				text += '\n';
				label = '省略する';
			}

			text += '(<a href="javascript:void(0)" class="toggleTruncation note">' + label + '</a>)';
		}

		data.value = text;

		return data;
	},
	prependExtension: function() {
		return {
			text: identity,
			header: identity,
		};
	},
	createDText: function(post, data) {
		var dText = document.createElement("span");
		dText.className = "text" + (post.isRead ? " read" : "");
		dText.innerHTML = data.value;

		data.value = dText;

		return data;
	},
	createMessage: function(post) {
		var el = document.createElement("span");

		el.id = post.id;
		el.post = post;

		return el;
	},
	setMargin: function(config, post, depth, el) {
		if (config.spacingBetweenMessages) {
			el.classList.add("spacing");
		}

		if (!depth) {
			depth = 1;
			var parent = post;
			while ((parent = parent.parent)) {
				depth++;
			}
		}

		el.style.marginLeft = depth + 'em';
	},
	message: function(el, dHeader, dText) {
		el.classList.add("message");
		el.appendChild(dHeader);
		el.appendChild(dText);

		return el;
	},
	button: function(el, rejectLevel, extension, state) {
		var reasons = [];

		if (rejectLevel) {
			reasons.push([null, "孫", "子", "個"][rejectLevel]);
			el.classList.add("reject");
		}

		if (state.isNG) {
			reasons.push("NG");
		}

		el.classList.add("showMessage");
		el.innerHTML = extension('<a class="showMessageButton" href="#">' + reasons.join(",") + '</a>');

		return el;
	},
	checkNGHeader: function(ng, handle) {
		return function(state) {
			return ng({value: handle, state: state});
		};
	},
	hide: function(config, post) {
		var notCheckMode = !config.NGCheckMode;
		return function(state) {
			state.hide = (state.isNG && notCheckMode) || post.rejectLevel;
			return {value: undefined, state: state};
		};
	},
	header: function(config, post, extension, name, title, state) {
		var resUrl = post.resUrl ? 'href="' + post.resUrl + '" ' : '';
		var vanish;
		if (post.rejectLevel === 3) {
			vanish = ' <a href="javascript:void(0)" class="cancelVanishedMessage">非表示を解除</a>';
		} else if (config.useVanishMessage) {
			vanish = ' <a href="javascript:void(0)" class="toggleMessage">消</a>';
		} else {
			vanish = "";
		}
		var dHeader = document.createElement("div");
		var header = '<a ' + resUrl + 'class="res" target="link">■</a>'
			+ '<span class="message-info">'
			+ ((title === '> ' || title === ' ') && name === ' '
				? ""
				: '<strong>' + title + '</strong> : <strong>' + name + '</strong> #'
			)
			+ post.date + '</span>'
			+ (resUrl && ' <a ' + resUrl + ' target="link">■</a>')
			+ vanish
			+ (state.hide ? ' <a href="#" class="fold">畳む</a>' : "")
			+ (post.posterUrl ? ' <a href="' + post.posterUrl + '" target="link">★</a>' : '')
			+ (state.hasCharacterEntity ? ' <a href="#" class="characterEntity' + (state.expandCharacterEntity ? ' characterEntityOn' : '' ) + '">文字参照</a>' : "")
			+ ' <a href="'
			+ post.threadUrl
			+ '" target="link">◆</a>';

		dHeader.className = "message-header";
		dHeader.innerHTML = extension(header);

		return { value: dHeader, state: state };
	}
};

function AbstractPosts() {}
AbstractPosts.prototype = {
	init: function(item, el) {
		this.el = el || document.createElement("span");
		this.el.className = "messages";
		this.item = item;
	},
	getContainer: function() {
		return this.el;
	},
	render: function(config) {
		if (this.pre) {
			this.pre();
		}

		var roots = this.item;
		var maker = this.messageMaker(this.mode, config);

		for (var i = 0, length = roots.length; i < length; i++) {
			this.doShowPosts(config, maker, roots[i], 1);
		}
		return this.el;
	},
	doShowPosts: function(config, maker, post, depth) {
		var dm = maker(post, depth);
		var dc = this.getContainer(post, depth);
		dc.appendChild(dm);
		if (config.spacingBetweenMessages && dm.extension) {
			dc.insertAdjacentHTML("beforeend", dm.extension);
			dm.extension = null;
		}

		if (post.child) {
			this.doShowPosts(config, maker, post.child, depth + 1);
		}
		if (post.next) {
			this.doShowPosts(config, maker, post.next, depth);
		}
	},
	messageMaker: function(mode, config) {
		var isCSS = mode === "tree-mode-css";
		var ng = Posts.markNG(config);
		var ngCheckHandler = curry2(Posts.checkNGHeader)(ng.header);
		var putThumbnails = Posts.putThumbnails(config);
		var characterEntity = curry3(Posts.characterEntity)(config);
		var createDText = curry2(Posts.createDText);
		var truncate = curry3(Posts.truncate)(config);
		var prependExtension = isCSS ? Posts.prependExtension : curry2(this.prependExtension)(config);
		var setMargin = isCSS ? Posts.setMargin : identity;

		return function(post, depth) {
			var extension = prependExtension(post);
			var text = compose(
				putThumbnails,
				characterEntity(post),
				createDText(post),
				extension.text,
				truncate(post),
				ng.text,
				Posts.checkCharacterEntity,
				Posts.checkThumbnails,
				Posts.makeText
			);

			var mText = function(post) {
				return function(state) {
					return text({value: post, state: state});
				};
			};

			var mHeader = actions(
				[
					ngCheckHandler(post.name),
					ngCheckHandler(post.title),
					Posts.hide(config, post),
				], function(values, state) {
					return Posts.header(config, post, extension.header, values[0], values[1], state);
				}
			);

			var mMessage = actions([ mText(post), mHeader ], function(values, state) {
				var dMessage = Posts.createMessage(post);
				setMargin(config, post, depth, dMessage);

				if (state.extension) {
					dMessage.extension = state.extension;
				}
				if (state.hide && !post.show && !post.showAll) {
					return Posts.button(dMessage, post.rejectLevel, extension.header, state);
				} else {
					return Posts.message(dMessage, values[1], values[0]);
				}
			});

			return mMessage({});
		};
	},
};

function CSSView() {
	this.mode = "tree-mode-css";
	this.containers = null;
	this.pre = function() {
		this.containers = [{dcontainer: this.el}];
	};
	this.getContainer = function(post, depth) {
		var containers = this.containers;
		var container = containers[containers.length - 1];

		if ("lastChildID" in container && container.lastChildID === post.id) {
			containers.pop();
			container = containers[containers.length - 1];
		}

		var child = post.child;
		if (child && child.next) {
			var lastChild = child;
			do {
				lastChild = lastChild.next;
			} while (lastChild.next);

			var dout = DOM('<span class="border outer" style="left:' +
				(+depth + 0.5) + 'em"><span class="border inner" style="left:' +
				(-depth - 0.5) + 'em"></span></span>');
			container.dcontainer.appendChild(dout);
			container = {lastChildID: lastChild.id, dcontainer: dout.firstChild};
			containers.push(container);
		}

		return container.dcontainer;
	};
}
extend(CSSView, AbstractPosts);

function ASCIIView() {
	this.mode = "tree-mode-ascii";

	function wrapTree(tag, tree) {
		return '<' + tag + ' class="a-tree">' + tree + '</' + tag + '>';
	}

	function getExtension(config, post) {
		var forHeader, forText, init;
		var utterlyVanishMessage = config.utterlyVanishMessage;
		var hasNext = post.next;
		var tree = [];
		var parent = post;

		while ((parent = parent.parent)) {
			if (utterlyVanishMessage && parent.rejectLevel) {
				break;
			}
			tree.push(parent.next ? "|" : " ");
		}
		init = tree.reverse().join("");

		if (post.isOP()) {
			forHeader = " ";
		} else {
			forHeader = init + (hasNext ? '├' : '└');
		}
		forText = init + (hasNext ? '|' : ' ') + (post.child ? '|' : ' ');

		return {header: forHeader, text: forText};
	}

	this.prependExtension = function(config, post) {
		var extension = getExtension(config, post);

		return {
			text: function(data) {
				data.value = data.value.replace(/^/gm, wrapTree("span", extension.text));
				data.state.extension = wrapTree("div", extension.text);

				return data;
			},
			header: function(header) {
				return extension.header + header;
			}
		};
	};
}
extend(ASCIIView, AbstractPosts);

var View = {
	"tree-mode-css": CSSView,
	"tree-mode-ascii": ASCIIView
};

function Threads() {
	var el = document.createElement("div");
	el.id = "content";
	return el;
}
Threads.addEventListeners = function(config, el, postParent) {
	var $el = $(el);

	function getTreeMode($node) {
		return $node.closest(".tree-mode-css").length ? "tree-mode-css" : "tree-mode-ascii";
	}

	function replace(e) {
		var $message = $(this).closest(".message, .showMessage");
		var post = $message.prop("post");
		var mode = getTreeMode($message);
		var view = new View[mode]();
		var maker = view.messageMaker(mode, config);
		e.data.callback(post);
		$message.replaceWith(maker(post));
		return false;
	}

	function addEventToReplaceWithNewOne(event, selector, callback) {
		$el.on(event, selector, {callback: callback}, replace);
	}
	var click = addEventToReplaceWithNewOne.bind(this, "click");

	click(".characterEntity", function(post) {
		post.characterEntity = !(post.hasOwnProperty("characterEntity") ? post.characterEntity : config.characterEntity);
	});

	click(".showMessageButton", function(post) {
		post.show = true;
	});

	click(".cancelVanishedMessage", function(post) {
		config.removeVanishedMessage(post.id);
		delete post.rejectLevel;
	});

	click(".fold", function(post) {
		post.show = false;
	});

	$el.on("mousedown", ".message", function(e) {
		$.Deferred(function(dfd) {
			var reject = dfd.reject.bind(dfd);
			$el.one("mouseup", reject);
			$el.one("mousemove", reject);
			setTimeout(function() {
				e.data = {
					callback: function(post) {
						post.showAll = !post.showAll;
					}
				};
				dfd.resolve(e);
			}, 500);
		}).then(replace.bind(this));
	});

	click(".toggleTruncation", function(post) {
		post.truncation = post.hasOwnProperty("truncation") ? !post.truncation : false;
	});

	$el.on("click", ".toggleMessage", function() {
		var $this = $(this);
		var $message = $this.closest(".message");
		var post = $message.prop("post");
		var id = post.id;
		var text, func, type;
		function get$(post) {
			return $("#" + post.id);
		}

		if ($this.hasClass("revert")) {
			text = "消";
			func = function(post, rejectLevel) {
				if (post.rejectLevel === rejectLevel) {
					post.rejectLevel = 0;
					get$(post).find("strong").remove();
				}
			};
			type = "remove";
		} else {
			if (post.isRead) {
				id = postParent.find(post.child.id, post.threadId, true);
				if (!id) {
					$this.replaceWith("最新1000件以内に存在しないため投稿番号が取得できませんでした。過去ログからなら消せるかもしれません");
					return;
				}
			}
			if (id.length > 100) {
				return;
			}

			text = "戻";
			func = function(post, rejectLevel) {
				var $post = get$(post);
				if (post.rejectLevel < rejectLevel) {
					post.rejectLevel = rejectLevel;
				}
				if (!$post.find("strong.note").length) {
					$post.find(".message-info").before('<strong class="note" style="color:red">この投稿も非表示になります</strong>');
				}
			};
			type = "add";
		}

		$message.find(".text").toggle();
		$this.text(text);
		post.rejectLevel = 3;
		(function prepareToBeVanished(post, rejectLevel) {
			if (post === null || rejectLevel === 0) {
				return;
			}
			func(post, rejectLevel);
			prepareToBeVanished(post.child, rejectLevel - 1);
			prepareToBeVanished(post.next, rejectLevel);
		})(post.child, 2);

		$this.toggleClass("revert");

		type += "VanishedMessage";

		config[type](id);
	});

	$el.on("click", ".vanish", function() {
		var $button = $(this);
		var $thread = $button.closest(".thread");
		var id = $thread.attr("id").slice(1);
		var type, text;

		if ($thread.hasClass("NGThread")) {
			type = "remove";
			text = "消";
		} else {
			type = "add";
			text = "戻";
		}
		type += "VanishedThread";
		$thread.find(".messages").toggle();
		config[type](id);
		$thread.toggleClass("NGThread");
		$button.text(text);
	});

	$el.on("click", ".toggle-tree", function toggleTree(e) {
		e.preventDefault();
		var button = this;
		setTimeout(function() {
			var $thread = $(button).closest(".thread");

			$thread.toggleClass("tree-mode-ascii tree-mode-css");
			var view = new View[getTreeMode($thread)]();
			var roots = Thread.getRoots($thread.attr("id").slice(1));
			view.init(roots);
			$thread.find(".messages").replaceWith(view.render(config));
		});
	});
};
Threads.showThreads = function(config, el, threads, postParent) {
	var mode = config.treeMode;
	var view = new View[mode]();
	var vanishedThreadIDs = config.vanishedThreadIDs;
	var threshold = config.useVanishMessage ? +config.vanishedMessageIDs[0] : Number.NaN;

	function showThread(thread) {
		var NG = "";
		var vanishButton = '消';
		if (config.useVanishThread && vanishedThreadIDs.indexOf(thread.getID()) > -1) {
			if (config.utterlyVanishNGThread) {
				return;
			}
			NG = " NGThread";
			vanishButton = "戻";
		}
		var roots = thread.getRoots(config, postParent, threshold);
		if (!thread.getNumber()) {
			return;
		}

		var html = '<pre id="t' + thread.getID() + '" class="thread ' + mode + NG + '">' +
			'<div class="thread-header">' +
			'<a href="' + thread.getURL() + '" target="link">◆</a>' +
			' 更新日:' + thread.getDate() + ' 記事数:' + thread.getNumber() +
			' <a href="javascript:void(0)" class="toggle-tree">●</a>' +
			(config.useVanishThread ? ' <a href="javascript:void(0)" class="vanish">' + vanishButton + '</a>' : '') +
			' <a href="' + thread.getURL() + '" target="link">◆</a>' +
			'</div><span class="messages"></span></pre>';
		var dthread = DOM(html);
		view.init(roots, dthread.lastChild);
		view.render(config);
		el.appendChild(dthread);
	}
	return loop(showThread, threads);
};

var PostParent = (function() {
	function constructor(config, isNormalMode, view, posts) {
		var data = null;

		function getStorage() {
			return config.useVanishMessage ? localStorage : sessionStorage;
		}

		function load() {
			data = JSON.parse(getStorage().getItem("postParent")) || {};
		}

		function save(data) {
			getStorage().setItem("postParent", JSON.stringify(data));
		}

		function update(posts) {
			var changed = false;
			if (!posts.length) {
				return;
			}
			if (!data) {
				load();
			}
			for (var i = 0, len = posts.length; i < len; i++) {
				var post = posts[i];
				var id = post.id;
				if (data.hasOwnProperty(id) && data[id] !== false) {
					continue;
				}

				var parentID = post.parentId;
				if (parentID && parentID.length > 20) {
					parentID = null;
				}
				data[id] = parentID;
				changed = true;
			}
			if (changed) {
				setTimeout(save, 0, data);
			}
		}

		function cleanUp() {
			if (!data) {
				return;
			}
			var ids = Object.keys(data);
			var length = ids.length;
			var upperlimit = 500;
			var lowerlimit = 300;
			if (config.useVanishMessage) {
				if (config.vanishMessageAggressive) {
					upperlimit = 3500;
					lowerlimit = 3300;
				} else {
					upperlimit = 1500;
					lowerlimit = 1300;
				}
			}
			if (length > upperlimit) {
				ids.sort(function(l, r) {
					return +r - +l;
				});
				var saveData = {};
				var i = lowerlimit;
				while (i--) {
					saveData[ids[i]] = data[ids[i]];
				}
				setTimeout(save, 0, saveData);
			}
		}
		function isNumber(number) {
			return /^\d+$/.test(number);
		}
		function fetchThread(childID, threadID) {
			//isNumber(childID)を先にしてるからchildID in dataのままでいい
			if (!(childID in data) && !Env.IS_LOCAL) {
				$.ajax({url: 'bbs.cgi?m=t&s=' + threadID, async: false}).then(wrapWithDiv).then(function(dcontainer) {
					var posts = Post.makePosts(dcontainer);
					update(posts);
					if (data[childID] === undefined) {
						update([{id: childID, parentId: false}]);
					}
				});
			}
		}

		var find = function(childID, threadID, force) {
			if (!isNumber(childID) || !isNumber(threadID)) {
				return undefined;
			}
			if (!data) {
				load();
			}

			if (config.vanishMessageAggressive && isNormalMode || force) {
				fetchThread(childID, threadID);
			}

			return data[childID];
		};

		function isDOMStorageAvailable() {
			try {
				return localStorage;
			} catch (e) {
				return null;
			}
		}

		if (isDOMStorageAvailable()) {
			update(posts);
			view.then(function() {
				setTimeout(cleanUp, 10 * 1000);
			});
		} else {
			find = doNothing;
		}

		return {
			find: find
		};
	}

	return {
		create: function(config, q, view, posts) {
			if (posts.length) {
				return constructor(config, !q.m, view, posts);
			} else {
				return doNothing;
			}
		}
	};
})();

function Thumbnail(config) {
	var whitelist = [
		"http://misao.on.arena.ne.jp/c/",
		"http://komachi.betanya.com/uploader/stored/",
		"http://www.google.co.jp/logos/"
	];
	var animationChecker = memoize(Thumbnail.checkAnimation);
	var id = null;

	function removeImageView() {
		$("#image-view").remove();
		$(".popup").unbind(".followMouse").removeClass("popup");
		$(document.body).unbind(".popup");
		clearTimeout(id);
		id = null;
	}

	function popupHandler(e) {
		//firefox:ポップアップを消した後、カーソルがサムネイルの上にある
		if (e.relatedTarget === null) {
			return;
		}
		//opera
		if (e.relatedTarget instanceof HTMLBodyElement) {
			return;
		}

		var $this = $(this);
		//chrome
		if ($this.hasClass("last") && $(e.relatedTarget).closest("#image-view").length && !$("#image-view").length) {
			return;
		}

		if ($this.hasClass("popup")) {
			return;
		}

		function getRatio(natural, max) {
			if (/^\d+$/.test(max) && natural > max) {
				return +max / natural;
			} else {
				return 1;
			}
		}

		var image = new Image();
		image.src = this.src || this.href;
		image.style.backgroundColor = "white";

		$(".last").removeClass("last");
		$this.addClass("popup last");

		var isFollowMouse = config.popupMode === "followMouse";
		var isBestFit = config.popupMode === "center" && config.popupBestFit;
		var viewport = document.compatMode === "BackCompat" ? document.body : document.documentElement;
		var windowHeight = viewport.clientHeight;
		var windowWidth = viewport.clientWidth;
		var $imageView = $("<figure>", {
			id: 'image-view',
			html: '<figcaption><span id="percentage"></span>%</figcaption>',
			"class": isFollowMouse ? "" : "popup",
		});

		if (isFollowMouse) {
			$this.bind("mousemove.followMouse", function(e) {
				$imageView.css({
					top: (e.pageY - 30) + "px",
					left: (e.pageX + 10) + "px"
				});
			});
		}

		$(document.body) // 予め確保しておくとfirefoxでイベントが発火しない GM2.3 2014/12/14
			.bind("click.popup", removeImageView)
			.bind("keydown.popup", function(e) {
				if (e.which === 27) { // ESC
					removeImageView();
				}
			}).bind("mouseout.popup", function(e) {
				if (!$(e.relatedTarget).closest(".popup").length) {
					removeImageView();
				}
			});

		if (!image.complete) {
			setTimeout(function() {
				if (!image.complete && !$this.next(".note").length) {
					$this.after('<span class="note">ダウンロード中</span>');
					$(image).on({
						"load": function() {
							$this.next(".note").remove();
						},
						"error": function() {
							$this.next(".note").text("error");
							$this.get(0).removeEventListener("mouseover", popupHandler, false);
						}
					});
				}
			}, 100);
		}

		function checkMetadata() {
			if (!image.complete && image.naturalWidth === 0 && image.naturalHeight === 0) {
				id = setTimeout(checkMetadata, 50);
			} else {
				popup();
			}
		}

		checkMetadata();

		function popup() {
			$imageView.append(image).appendTo(document.body);
			var width = image.width;
			var height = image.height;
			var marginHeight = $imageView.height() - height;
			var maxWidth = config.popupMaxWidth || (isBestFit ? windowWidth : width);
			var maxHeight = config.popupMaxHeight || (isBestFit ? windowHeight - marginHeight : height);
			var ratio = Math.min(getRatio(width, maxWidth), getRatio(height, maxHeight));
			var percentage = Math.floor(ratio * 100);
			var bgcolor = ratio < 0.5 ? "red" : ratio < 0.9 ? "blue" : "green";
			// 丸めないと画像が表示されないことがある
			var imageHeight = Math.floor(height * ratio) || 1;
			var imageWidth = Math.floor(width * ratio) || 1;
			var css = {
				backgroundColor: bgcolor
			};

			if (config.popupMode === "center") {
				css.top = window.pageYOffset + (windowHeight - imageHeight - marginHeight) / 2;
				css.left = window.pageXOffset + (windowWidth - imageWidth) / 2;
			} else {
				css.top = (e.pageY - 30) + "px";
				css.left = (e.pageX + (config.popupMode === "followMouse" ? 10 : -10)) + "px";
			}

			$(image).attr({
				height: imageHeight,
				width: imageWidth
			});
			$imageView.css(css);
			$("#percentage").text(percentage);

			$imageView.css("visibility", "visible");
		}
	}

	function whitelistHas(href) {
		for (var i = 0, len = whitelist.length; i < len; i++) {
			if (href.startsWith(whitelist[i])) {
				return true;
			}
		}
		return false;
	}

	function thumbnailLink(href) {
		if (!/\.(?:jpe?g|png|gif|bmp)$/i.test(href)) {
			return null;
		}

		if (whitelistHas(href)) {
			return realThumbnail(href);
		} else if (config.popupAny) {
			return '[<a href="' + href + '" target="link" class="thumbnail">■</a>]';
		}

		return null;
	}

	function realThumbnail(href) {
		var thumbnail;
		var src = href;

		if (!config.thumbnailPopup && href.startsWith('http://misao.on.arena.ne.jp/c/up/')) {
			src = src.replace(/up\//, "up/pixy_");
		}

		thumbnail = '<a href="' + href + '" target="link"><img class="thumbnail" src="' + src + '"></a>';

		if (config.linkAnimation) {
			var misao = /^http:\/\/misao\.on\.arena\.ne\.jp\/c\/up\/(misao\d+)\.(?:png|jpg)$/.exec(href);
			if (misao) {
				var misaoID = misao[1];
				var animationURL = 'http://misao.on.arena.ne.jp/c/upload.cgi?m=A&id=' + (/(?!0)\d+/).exec(misaoID)[0];
				thumbnail += '<span class="animation ' + misaoID + '">[<a href="' + animationURL + '" target="link">A</a><span class="unsure">?</span>]</span>';
				animationChecker(href).then(function(isAnimation) {
					setTimeout(function() {
						var $misao = $('.' + misaoID);
						if (isAnimation) {
							$misao.find(".unsure").remove();
						} else {
							$misao.remove();
						}
					});
				});
			}
		}

		if (config.shouki) {
			thumbnail += '[<a href="http://images.google.com/searchbyimage?image_url=' + href + '" target="link">詳</a>]';
		}

		return thumbnail;
	}

	this.register = function(container) {
		var as = container.querySelectorAll('a[target]');
		var has = false;
		var i;
		for (i = as.length - 1; i >= 0; i--) {
			var a = as[i];
			var href = a.href;
			var thumbnail = thumbnailLink(href);
			if (thumbnail) {
				a.insertAdjacentHTML('beforebegin', thumbnail);
				has = true;
			}
		}
		if (has && config.thumbnailPopup) {
			var imgs = container.getElementsByClassName('thumbnail');
			for (i = imgs.length - 1; i >= 0; i--) {
				imgs[i].addEventListener("mouseover", popupHandler, false);
			}
		}
	};
}

Thumbnail.checkAnimation = function(imgURL) {
	var dfd = $.Deferred();
	var url = imgURL.replace(/\w+$/, "pch");
	if (Env.IS_GM) {
		GM_xmlhttpRequest({
			url: url,
			method: "HEAD",
			onload: function(response) {
				dfd.resolve(response.status === 200);
			}
		});
	} else if (Env.IS_CHROME) {
		$.ajax({
			url: url,
			type: "HEAD"
		}).then(function() {
			dfd.resolve(true);
		}, function() {
			dfd.resolve(false);
		});
	}

	return dfd.promise();
};

function NG(config) {
	this.isEnabled = !!(config.useNG && (config.NGWord || config.NGHandle));
	if (this.isEnabled) {
		if (config.NGHandle) {
			this.NGHandleRE = new RegExp(config.NGHandle);
			this.NGHandleREg = new RegExp(config.NGHandle, "g");
		}
		if (config.NGWord) {
			this.NGWordRE = new RegExp(config.NGWord);
			this.NGWordREg = new RegExp(config.NGWord, "g");
		}
	}
}

function doNothing() {}

function tweakResWindow() {
	var v = document.querySelector("textarea");
	if (v) {
		setTimeout(function() {
			v.scrollIntoView();
			v.setSelectionRange(v.textLength, v.textLength);
			v.scrollTop = v.scrollHeight;
			v.focus();
		});
	}
}

function whatToDo(q) {
	switch (q.m) {
		case "f": //レス窓
			tweakResWindow.wait = true;
			return tweakResWindow;
		case "l": //トピック一覧
		case "c": //個人用設定
			return doNothing;
		case 'g': //過去ログ
			if (!q.btn) { //スレッドボタン・レスボタンがなければ
				return doNothing;
			}
	}

	return app;
}

function checkResWindow() {
	return document.title.endsWith(" 書き込み完了");
}

function closeResWindow() {
	if (Env.IS_CHROME) {
		chrome.runtime.sendMessage({type: "closeTab"});
	} else {
		window.open("", "_parent");
		window.close();
	}
}

function defer(func) {
	return function() {
		var done = $.Deferred();
		var args = arguments;
		var deferred = function() {
			func.apply(null, args).then(done.resolve.bind(done));
		};
		deferred.promise = done;
		return deferred;
	};
}

var eventHandlers = {
	openConfig: function(config) {
		if (Env.IS_CHROME && chrome.extension.getURL) {
			window.open(chrome.extension.getURL("options.html"));
		} else if (!document.getElementById("config")) {
			$(document.body).prepend(new ConfigController(config));
			$(window).scrollTop(0);
		}
		return false;
	},
	tweakLink: function(config, e) {
		var target = e.target;
		if (config.fc2 && /(?:\.fc2\.com|\.ameba\.jp)$/.test(target.hostname)) {
			$(target).attr("rel", "noreferrer");
		}

		if (config.openLinkInNewTab && target.target === "link") {
			$(target).attr("target", "_blank");
		}
	},
	reload: function() {
		var reload = document.getElementById("qtv/reload");
		if (!reload) {
			reload = DOM('<input type="submit" id="qtv/reload" name="reload" value="1" style="display:none;">');
			document.forms[0].appendChild(reload);
		}
		if (Env.IS_LOCAL) {
			console.log("reload");
		} else {
			reload.click();
		}
	},
	midokureload: function() {
		if (Env.IS_LOCAL) {
			console.log("midoku reload");
		} else {
			document.getElementsByName("midokureload")[0].click();
		}
	}
};

var App = {
	common: function(config, body, view) {
		App.zeroWhenNeeded(config);
		App.addCommonEvents(config, body);
		App.setAccesskeyToVWhenNeeded(config);
		App.injectCSS(config);
		App.keyboardNavigationWhenNeeded(config, view);
	},
	keyboardNavigationWhenNeeded: function(config, view) {
		doWhen(config.keyboardNavigation, App.keyboardNavigation.bind(null, config, view));
	},
	keyboardNavigation: function(config, view) {
		$(document).bind("keypress", new KeyboardNavigation(config, view));
	},
	zeroWhenNeeded: function(config) {
		doWhen(config.zero, App.zero);
	},
	zero: function() {
		var d = document.getElementsByName("d")[0];
		if (d && d.value !== "0") {
			d.value = "0";
		}
	},
	addCommonEvents: function(config, body) {
		var $el = $(body);
		$el.on("click", "#openConfig", eventHandlers.openConfig.bind(null, config));
		$el.on("click", "a", eventHandlers.tweakLink.bind(null, config));
	},
	setAccesskeyToVWhenNeeded: function(config) {
		var v = config.accesskeyV;
		doWhen(v.length === 1, App.setAccesskeyToV.bind(null, v));
	},
	setAccesskeyToV: function(v) {
		var vs = document.getElementsByName("v");
		if (vs.length) {
			vs[0].accessKey = v;
		}
	}
	// injectCSSは下の方で定義
};

var Tree = {
	setID: function() {
		var forms = document.forms;
		if (forms.length) {
			var form = forms[0];
			form.id = "form";
			var fonts = form.getElementsByTagName("font");
			if (fonts.length > 6) {
				fonts[6].id = "link";
			}
		}
	},
	template: function(config) {
		var reload = '<input type="button" value="リロード" class="mattari">';
		if (!config.zero) {
			reload = reload.replace('mattari', 'reload');
			reload += '<input type="button" value="未読" class="mattari">';
		}

		var accesskey = config.accesskeyReload;
		if (!/^\w$/.test(accesskey)) {
			accesskey = "R";
		}
		var views = "";
		var viewing = "";
		var forms = document.forms;
		if (forms.length) {
			var fonts = forms[0].getElementsByTagName("font");
			if (fonts.length > 5) {
				var tmp = fonts[5].textContent.match(/\d+/g) || [];
				views = tmp[3];
				viewing = tmp[5];
			}
		}
		var vanishedThreadIDLength = config.vanishedThreadIDs.length;
		var vanishedMessageIDLength = config.vanishedMessageIDs.length;
		var hasVanishings = vanishedThreadIDLength || vanishedMessageIDLength;
		var vanishingStatus = '非表示解除(<a id="clearVanishedThreadIDs" href="#"><span class="length">' + vanishedThreadIDLength + '</span>スレッド</a>/' +
			'<a id="clearVanishedMessageIDs" href="#"><span class="length">' + vanishedMessageIDLength + '</span>投稿</a>) ';

		var containee =
			'<div id="header">' +
				'<span style="float:left">' +
					reload.replace('class="mattari"', '$& accesskey="' + accesskey + '"') + ' ' +
					views + ' / ' + viewing + '名 ' +
					'<span id="postcount"></span>' +
				'</span>' +
				'<ul style="text-align:right;">' +
					'<li><a href="javascript:void(0)" id="openConfig">設定</a></li> ' +
					'<li><a href="#link">link</a></li> ' +
					'<li><a href="#form">投稿フォーム</a></li> ' +
					'<li>' + reload + '</li>' +
				'</ul>' +
			'</div>' +
			'<hr>' +
			'<div id="footer">' +
				'<span style="float:left">' +
					reload + ' ' +
				'</span>' +
				'<ul style="text-align:right">' +
					(hasVanishings ? vanishingStatus : "") +
					'<li>' + reload + '</li>' +
				'</ul>' +
			'</div>';
		return containee;
	},
	render: function(config) {
		var el = document.createElement("div");
		el.id = "container";
		var $el = $(el);
		var click = $el.on.bind($el, "click");

		//event
		click(".reload", eventHandlers.reload);
		click(".mattari", eventHandlers.midokureload);
		click('a[href="#form"]', Tree.focusV);
		['Message', 'Thread'].forEach(function(type) {
			var id = 'clearVanished' + type + 'IDs';
			click('#' + id, Tree.clearVanishedIDs.bind(null, config, id));
		});

		var info = new Info();
		info.textContent = "ダウンロード中...";
		var threads = new Threads();

		el.innerHTML = Tree.template(config);
		var header = el.firstChild;
		header.firstChild.appendChild(info);
		el.insertBefore(threads, header.nextSibling);

		return {container: el, info: info, content: threads};
	},
	deleteOriginal: function() {
		function deleteContents(range, start, end) {
			range.setStartBefore(start);
			range.setEndAfter(end);
			range.deleteContents();
		}
		function startNode() {
			var h1 = document.getElementsByTagName("h1")[0];
			return h1 ? h1 : document.anchors[0].previousElementSibling;
		}

		var firstAnchor = document.anchors[0];
		var end = Tree.kuzuhaEnd(document.body);

		if (!firstAnchor || !end) {
			return;
		}

		var range = document.createRange();
		var start = startNode();

		if (Env.IS_FIREFOX) {
			range.selectNode(document.body);
			var body = range.extractContents();
			deleteContents(range, start, end);
			document.documentElement.appendChild(body);
		} else {
			deleteContents(range, start, end);
		}
	},
	kuzuhaEnd: function(node) {
		var last = node.lastChild;
		while (last) {
			if (last.length === 1 && last.nodeType === 8 && last.nodeValue === ' ') {
				return last;
			} else if (last.nodeType === 1) {
				var name = last.nodeName;
				if (name === "P" && last.align === "right" || name === "H3") {
					return last;
				}
			}
			last = last.previousSibling;
		}

		return null;
	},
	clearVanishedIDs: function(config, method, e) {
		config[method]();
		e.target.firstElementChild.innerHTML = "0";
		return false;
	},
	focusV: function() {
		setTimeout(function() {
			document.getElementsByName("v")[0].focus();
		}, 50);
	},
	tweakURL: function(posts) {
		posts.forEach(function(post) {
			var date = post.date.match(/\d+/g);
			var ff = '&ff=' + date[0] + date[1] + date[2] + '.dat';
			post.threadUrl += ff; //post.threadUrl.replace(/&ac=1$/, "")必要?
			if (post.resUrl) {
				post.resUrl += ff;
			}
			if (post.posterUrl) {
				post.posterUrl += ff;
			}
		});

		return posts;
	},
	fetchFromRemote: function(q, gui, origin) {
		gui.info.innerHTML = '<strong>' + q.ff + "以外の過去ログを検索中...</strong>";
		var posts = Post.makePosts(origin);
		return Post.fetchFromRemote(q, origin).then(function(doms) {
			var makeArray = function(posts, data) {
				var newPosts = Post.makePosts(data.container);
				return posts.concat(newPosts);
			};
			return [].concat(
				doms.afters.reduce(makeArray, []),
				posts,
				doms.befores.reduce(makeArray, [])
			);
		});
	},
	threads: function(posts) {
		var allThreads = Object.create(null);
		var threads = [];

		posts.forEach(function(post) {
			var id = post.threadId;
			var thread = allThreads[id];
			if (!thread) {
				thread = allThreads[id] = new Thread(id);
				threads.push(thread);
			}
			thread.posts.push(post);
		});

		return threads;
	},
	sortThreads: function(config, threads) {
		return config.threadOrder === "ascending" ? threads.reverse() : threads;
	},
	whenToSuggestLinkToLog: function(q, document) {
		return q.m === 't' && !q.ff && /^\d+$/.test(q.s) && !document.querySelector('a[name="' + q.s + '"]');
	},
	suggestLinkToLogWhenNeeded: function(q, document) {
		doWhen(Tree.whenToSuggestLinkToLog(q, document), Tree.suggestLinkToLog);
	},
	suggestLinkToLog: function() {
		var url = location.href + new Date().toLocaleFormat("&ff=%Y%m%d.dat");
		$("#info").after(' <a id="hint" href="' + url + '">過去ログを検索する</a>');
	}
};

function tree1(config, q, gui, container) {
	//通常モードからスレッドボタンを押した場合
	var isThreadSearchWithin1000 = q.m === 't' && !q.ff && /^\d+$/.test(q.s);
	//検索窓→投稿者検索→★の結果の場合
	var isPosterSearchInLog = q.s && q.ff && q.m === 's';
	var whenTweak = isThreadSearchWithin1000 || isPosterSearchInLog;
	var isThreadSearchInLog = q.ff && q.s && q.m === 't' && /dat$/.test(location.search);
	var localPosts = compose(whenTweak ? Tree.tweakURL : identity, Post.makePosts);

	var getPosts = isThreadSearchInLog ? curry3(Tree.fetchFromRemote)(q)(gui) : localPosts;

	return $.when(getPosts(container));
}
function tree2(config, q, gui, posts) {
	var makeThreads = compose(curry2(Tree.sortThreads)(config), Tree.threads);

	var postcount;
	if (posts.length) {
		postcount = posts.length + "件取得";
	} else {
		postcount = "未読メッセージはありません。";
	}
	document.getElementById("postcount").textContent = postcount;

	var postParent;
	var view = $.Deferred();
	var done = view.then(function(threads) {
		gui.info.textContent = " - スレッド構築中";
		Threads.addEventListeners(config, gui.content, postParent);
		return Threads.showThreads(config, gui.content, threads, postParent);
	}).then(function() {
		gui.info.textContent = "";
	});

	postParent = PostParent.create(config, q, done, posts);
	view.resolve(makeThreads(posts));

	return done;
}
function tree(config, q, body) {
	var gui = Tree.render(config);
	var posts = tree1.bind(null, config, q, gui);
	var threads = tree2.bind(null, config, q, gui);
	var mPosts = posts(body);

	mPosts.then(function(posts) {
		doWhen(posts.length && config.deleteOriginal, Tree.deleteOriginal);
		Tree.setID();
		body.insertBefore(gui.container, body.firstChild);
		Tree.suggestLinkToLogWhenNeeded(q, body);
	});

	return mPosts.then(threads);
}

var Stack = {
	common: function(config) {
		Stack.addEventListener();
		Stack.configButton();
		Stack.accesskey(config);
	},
	accesskey: function(config) {
		var midoku = document.getElementsByName("midokureload")[0];
		if (midoku) {
			midoku.accessKey = config.accesskeyReload;
		}
	},
	container: function() {
		var el = document.createElement("div");
		el.id = "container";
		var info = new Info();
		el.appendChild(info);
		document.body.insertBefore(el, document.body.firstChild);
		return {info: info};
	},
	addEventListener: function() {
		$(document.body).on("click", ".showOriginalMessageButton", function() {
			var $button = $(this);
			$button.next().show();
			$button.remove();
		});
	},
	whenToComplement: function(q) {
		return q.ff && q.m === 't' && /dat$/.test(location.search);
	},
	complementLogWhenNeeded: function(config, q) {
		return doWhen(Stack.whenToComplement(q), Stack.complementLog.bind(null, config, q));
	},
	complementLog: function(config, q) {
		var gui = Stack.container();
		gui.info.innerHTML = '<strong>' + q.ff + "以外の過去ログを検索中...</strong>";
		return Post.fetchFromRemote(q, document.body)
			.then(curry3(Stack.addExtraLog)(config)(q))
			.then(function() {
				gui.info.textContent = "";
			});
	},
	addExtraLog: function(config, q, doms) {
		var wrap = (function() {
			var wrap = Stack.wrapA(config);
			return function(f) {
				Array.prototype.forEach.call(f.querySelectorAll("a[name]"), wrap);
				return f;
			};
		})();
		var f = document.createDocumentFragment();
		function format(f, data) {
			var dcontainer = data.container;
			var numberOfPosts = dcontainer.querySelectorAll("a[name]").length;

			f.appendChild(DOM('<h1>' + data.date.toLocaleFormat('%Y%m%d.dat') + '</h1>'));

			if (numberOfPosts) {
				f.appendChild(wrap(dcontainer));
				f.appendChild(DOM('<h3>' + numberOfPosts + '件見つかりました。</h3>'));
			} else {
				f.appendChild(DOM('<hr>'));
				f.appendChild(DOM('<h3>指定されたスレッドは見つかりませんでした。</h3><hr>'));
			}

			return f;
		}

		if (doms.befores.length) {
			f.appendChild(DOM('<hr>'));
		}

		f = doms.befores.reduceRight(format, f);

		f.appendChild(DOM('<hr>'));
		f.appendChild(DOM('<h1>' + q.ff + '</h1>'));

		document.body.insertBefore(f, document.getElementById("container").nextSibling);

		f = doms.afters.reduceRight(format, f);
		document.body.appendChild(f);
	},
	configButton: function() {
		var setups = document.getElementsByName("setup");
		if (setups.length) {
			setups[0].insertAdjacentHTML("afterend", ' <a href="#" id="openConfig">★くわツリービューの設定★</a>');
		}
	},
	hasNG: function(ng) {
		var nextFont = DOM.nextElement("FONT");
		var nextB = DOM.nextElement("B");

		return function(a, blockquote) {
			var isNG;
			if (ng.NGWordRE) {
				var text = blockquote.firstElementChild.innerHTML;
				isNG = ng.NGWordRE.test(text);
			}
			if (!isNG && ng.NGHandleRE) {
				var header = nextFont(a);
				var title = header.firstChild.innerHTML;
				isNG = isNG || ng.NGHandleRE.test(title);
				if (!isNG) {
					var name = nextB(header).innerHTML;
					isNG = isNG || ng.NGHandleRE.test(name);
				}
			}
			return isNG;
		};
	},
	wrapStack: function(config) {
		var range = document.createRange();
		var div = document.createElement("div");
		var ng = new NG(config);
		var thumbnail = new Thumbnail(config);
		var ngButton = DOM('<a class="showOriginalMessageButton" href="javascript:void(0)">NG</a>');
		var needSurround = config.keyboardNavigation || Env.IS_CHROME;
		var useThumbnail = config.thumbnail;
		var utterlyVanishNGStack = config.utterlyVanishNGStack;
		var nextBlockquote = DOM.nextElement("BLOCKQUOTE");
		var nextComment = DOM.nextSibling("#comment");
		var hasNG = Stack.hasNG(ng);

		function wrapOne(a) {
			var blockquote = nextBlockquote(a);
			var isNG = ng.isEnabled && hasNG(a, blockquote);
			if (isNG && utterlyVanishNGStack) {
				var end = nextComment(blockquote);
				range.setStartBefore(a);
				range.setEndAfter(end);
				range.deleteContents();
				return;
			} else if (needSurround || isNG) {
				var wrapper = div.cloneNode(false);
				wrapper.className = "message original" + (isNG ? " NG" : "");
				range.setStartBefore(a);
				range.setEndAfter(blockquote);
				range.surroundContents(wrapper);
				if (isNG) {
					wrapper.parentNode.insertBefore(ngButton.cloneNode(true), wrapper);
				}
			}
			if (useThumbnail) {
				thumbnail.register(blockquote.firstElementChild);
			}
		}

		return wrapOne;
	},
	render: function(config, anchors) {
		var ng = new NG(config);
		if (config.keyboardNavigation || config.thumbnail || ng.isEnabled) {
			if (Env.IS_FIREFOX) {
				var range = document.createRange();
				range.selectNode(document.body);
				var body = range.extractContents();
				anchors.forEach(Stack.wrapA(config));
				document.documentElement.appendChild(body);
			} else {
				return loop(Stack.wrapA(config), anchors);
			}
		}
	},
	wrapA: function(config) {
		return Stack.wrapStack(config);
	},
	wrapOne: function(config) {
		var wrap = Stack.wrapStack(config);
		return function(f) {
			wrap(f.querySelector("a[name]"));
			return f;
		};
	}
};

function stack(config, q) {
	Stack.common(config);

	var anchors = Array.prototype.slice.call(document.anchors);

	return $.when(Stack.complementLogWhenNeeded(config, q), Stack.render(config, anchors));
}

function Info() {
	var el = document.createElement("span");
	el.id = "info";

	return el;
}

function KeyboardNavigation(config, view) {
	//同じキーでもkeypressとkeydownでe.whichの値が違うので注意
	var messages = document.getElementsByClassName("message");
	var focusedIndex = -1;
	var $window = $(window);
	var time = 0;
	// Promise を $.Deferred に変更
	view = $.Deferred(function(dfd) {
		view.then(function() {
			dfd.resolve();
		});
	});

	function isValid(index) {
		return 0 <= index && index < messages.length;
	}

	function indexOfNextVisible(index, dir) {
		if ($(messages[index]).is(":hidden")) {
			return indexOfNextVisible(index + dir, dir);
		}
		return index;
	}
	function focus(dir) {
		var index = indexOfNextVisible(focusedIndex + dir, dir);
		if (isValid(index)) {
			$(".focused").removeClass("focused");
			var $target = $(messages[index]).addClass("focused");
			$window.scrollTop($target.offset().top - config.keyboardNavigationOffsetTop);
			focusedIndex = index;
		} else if (dir === 1) {
			// Promise.race([view, Promise.reject()]).then(eventHandlers.midokureload);
			var dfd = $.Deferred();
			var now = Date.now();
			if (now - time > 500) {
				view.then(function() {
					time = now;
					dfd.resolve();
				});
			}
			dfd.reject();
			dfd.then(eventHandlers.midokureload);
		}
	}

	return function(e) {
		var target = e.target;

		if (/^(?:INPUT|SELECT|TEXTAREA)$/.test(target.nodeName) || target.isContentEditable) {
			return;
		}

		switch (e.which) {
			case 106: //j
				focus(1);
				break;
			case 107: //k
				focus(-1);
				break;
			case 114: //r
				var $f = $(".focused");
				var $res = $f.hasClass("original") ? $f.find("font > a").first() : $f.find(".res");
				if ($res.length) {
					window.open($res.attr("href"));
				}
				break;
			default:
		}
	};
}

function app2(config, q) {
	var body = document.body;
	var view = defer(config.isTreeView() ? tree : stack)(config, q, body);

	App.common(config, body, view.promise);
	view();
}
function app(q) {
	var config = Config.loadFromLocal();
	if (checkResWindow()) {
		if (config.closeResWindow) {
			closeResWindow();
		}
	} else {
		app2(config, q);
	}
}

///////////////////////////////////////////////////////////////////////////////

App.injectCSS = function(config) {
	var css = '\
		.text {\
			white-space: pre-wrap;\
			margin-left: 1em;\
/*			line-height: 1em; */\
			min-width: 20em;\
		}\
		.message, .inner, .outer {\
			position: relative;\
		}\
		.text, .message, .showMessage, .border {\
			display: block;\
		}\
		.tree-mode-ascii .text {\
			margin-left: 0;\
		}\
		.thread-header {\
			background: #447733 none repeat scroll 0 0;\
			border-color: #669955 #225533 #225533 #669955;\
			border-style: solid;\
			border-width: 1px 2px 2px 1px;\
			font-size: 80%;\
			font-family: normal;\
			margin-top: 0.8em;\
			padding: 0;\
			width: 100%;\
		}\
\
		.message-header {\
			font-size: 85%;\
			font-family: normal;\
			white-space: nowrap;\
		}\
		.message-info {\
			font-family: monospace;\
			color: #87CE99;\
		}\
		.tree-mode-ascii .message-header {\
			font-family: monospace;\
			font-size: 100%;\
		}\
\
		.read, .quote {\
			color: #CCB;\
		}\
		#header, #footer {\
			font-size: 90%;\
		}\
		.thread {\
			margin-bottom: 1em;\
		}\
		#header li, #footer li {\
			display:inline-block;\
		}\
		.modified {\
			color: #FBB\
		}\
		.note, .characterEntityOn {\
			font-style: italic;\
		}\
\
		.inner {\
/*			border: 2px solid yellow; */\
			top: -1em;\
		}\
		.outer {\
			border-left: 1px solid #ADB;\
			top: 1em;\
		}\
		.thumbnail {\
			width: 80px;\
			max-height: 400px;\
			image-orientation: from-image;\
		}\
		#image-view {\
			position: absolute;\
			background: #004040;\
			visibility: hidden;\
			color: white;\
			font-weight: bold;\
			font-style: italic;\
			margin: 0;\
			image-orientation: from-image;\
		}\
		.focused {\
			border: 2px solid yellow;\
		}\
		.truncation, .NGThread .messages, .original.NG {\
			display: none;\
		}\
		.spacing {\
			padding-bottom: 1em;\
		}\
		.NGWordHighlight {\
			background-color: red;\
		}\
		.showMessageButton {\
			position: relative;\
			z-index:1;\
		}\
	';
	GM_addStyle(css + config.css);
};

function GM_addStyle(css) {
	var doc = document;
	var head = doc.getElementsByTagName("head")[0];
	var style = null;
	if (head) {
		style = doc.createElement("style");
		style.textContent = css;
		head.appendChild(style);
	}
}

var div_ = document.createElement("div");
function DOM(html) {
	var div = div_.cloneNode(false);
	div.innerHTML = html;
	return div.firstChild;
}
DOM._next = function(type) {
	type = "next" + type;
	return function(nodeName) {
		return function next(node) {
			var el = node[type];
			if (el.nodeName === nodeName) {
				return el;
			} else {
				return next(el);
			}
		};
	};
};
DOM.nextElement = DOM._next("ElementSibling");
DOM.nextSibling = DOM._next("Sibling");
function wrapWithDiv(html) {
	var div = document.createElement("div");
	div.innerHTML = html;
	return div;
}

function extend(child, super_, properties) {
	child.prototype = Object.create(super_.prototype, properties);
}

function loop(func, array) {
	var i = 0, length = array.length, dfd = $.Deferred();
	(function loop() {
		var t = Date.now();
		do {
			if (i === length) {
				dfd.resolve();
				return;
			}
			func(array[i++]);
		} while (Date.now() - t < 20);
		setTimeout(loop, 0);
	})();
	return dfd.promise();
}
;
// ==UserScript==
// @name        tree view for qwerty
// @namespace   strangeworld
// @description くわツリービュー。あやしいわーるど@上海の投稿をツリーで表示できます。スタック表示の方にもいくつか機能を追加できます。
// @include     http://qwerty.on.arena.ne.jp/cgi-bin/bbs.cgi*
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_xmlhttpRequest
// @version     10.1.6
// ==/UserScript==

function parseQuery(search) {
	var obj = {}, kvs = search.substring(1).split("&");
	kvs.forEach(function (kv) {
		obj[kv.split("=")[0]] = kv.split("=")[1];
	});
	return obj;
}

function main() {
	var q = parseQuery(location.search);
	var action = whatToDo(q);
	if (action.wait && document.readyState !== "complete") {
		$(document).one("DOMContentLoaded", action);
	} else {
		action(q);
	}
}

main();
})()