bc-SmartTOC

TOC can be generated for github、jianshu.com、cnblogs.com and other sites

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         bc-SmartTOC
// @name:zh-CN   bc-SmartTOC
// @icon         data:image/png;base64,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
// @namespace    BC
// @version      0.1
// @description  TOC can be generated for github、jianshu.com、cnblogs.com and other sites
// @description:zh-cn 可以为github、简书、cnblog等网站生成TOC
// @collaborator    ChandlerVer5
// @match        http*://coursehunters.online/t/*
// @match        *://docs.spring.io/*
// @match       *flaviocopes.com/*
// @match        http*://*/book/*
// @match        http*://www.freecodecamp.org/news/*
// @match        http*://www.cnblogs.com/*/p/*
// @match        http*://*/doc*/*
// @match        http*://blog.csdn.net/*
// @match        http*://www.jianshu.com/p/*
// @match        http*://juejin.im/post/*
// @match        http*://sspai.com/*
// @match        http*://zhuanlan.zhihu.com/p/*
// @match        http*://mp.weixin.qq.com/s*
// @match        http*://cnodejs.org/topic/*
// @match        http*://div.io/topic/*
// @match        http*://www.runoob.com/*/*
// @match        http*://www.code4app.com/article/*
// @match        http*://www.zcfy.cc/article/*
// @match        http*://github.com/*
// @match        http*://gist.github.com/*
// @grant        none
// ==/UserScript==

(function () {
'use strict';

function getRootWindow() {
  let w = window
  while (w !== w.parent) {
    w = w.parent
  }
  return w
}

function getMaster(root) {
  const iframes = [].slice.apply(root.document.getElementsByTagName('iframe'))

  if (iframes.length === 0) {
    return root
  } else {
    const largestChild = iframes
      .map(f => ({
        elem: f,
        area: f.offsetWidth * f.offsetHeight
      }))
      .sort((a, b) => b.area - a.area)[0]
    const html = root.document.documentElement
    return largestChild.area / (html.offsetWidth * html.offsetHeight) > 0.5
      ? largestChild.elem.contentWindow
      : root
  }
}

function isMasterFrame(w) {
  const root = getRootWindow()
  const master = getMaster(root)
  return w === master
}

var toastCSS = "#smarttoc-toast {\n  all: initial;\n}\n\n#smarttoc-toast * {\n  all: unset;\n}\n\n#smarttoc-toast {\n  display: none;\n  position: fixed;\n  right: 0;\n  top: 0;\n  margin: 1em 2em;\n  padding: 1em;\n  z-index: 10000;\n  box-sizing: border-box;\n  background-color: #fff;\n  border: 1px solid rgba(158, 158, 158, 0.22);\n  color: gray;\n  font-size: calc(12px + 0.15vw);\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  font-weight: normal;\n  -webkit-font-smoothing: subpixel-antialiased;\n  font-smoothing: subpixel-antialiased;\n  transition: opacity 200ms ease-out, transform 200ms ease-out;\n}\n\n#smarttoc-toast.enter {\n  display: block;\n  opacity: 0.01;\n  transform: translate3d(0, -2em, 0);\n}\n\n#smarttoc-toast.enter.enter-active {\n  display: block;\n  opacity: 1;\n  transform: translate3d(0, 0, 0);\n}\n\n#smarttoc-toast.leave {\n  display: block;\n  opacity: 1;\n  transform: translate3d(0, 0, 0);\n}\n\n#smarttoc-toast.leave.leave-active {\n  display: block;\n  opacity: 0.01;\n  transform: translate3d(0, -2em, 0);\n}\n";

function log() {
  if (false) {}
}

function draw(elem, color = 'red') {
  if (false && elem) {}
}

function assert(condition, error) {
  if (!condition) {
    throw new Error(error)
  }
}

// '12px' => 12
const num = (size = '0') =>
  typeof size === 'number' ? size : +size.replace(/px/, '')

// '12px' <= 12
const px = (size = 0) => num(size) + 'px'

function throttle(fn, delay) {
  if (delay) {
    let timer
    return function timerThrottled(...args) {
      clearTimeout(timer)
      timer = setTimeout(function() {
        fn(...args)
      }, delay)
    }
  } else {
    let request
    return function rafThrottled(...args) {
      cancelAnimationFrame(request)
      request = requestAnimationFrame(function() {
        fn(...args)
      })
    }
  }
}

const safe = str => str.replace(/\s+/g, '-')

const unique = (function uniqueGenerator() {
  let set = new Set()
  return function unique(str) {
    let id = 1
    while (set.has(str)) {
      str = str.replace(/(\$\d+)?$/, '') + '$' + id
      id++
    }
    set.add(str)
    return str
  }
})()

const getScroll = (elem, direction = 'top') => {
  if (elem === document.body) {
    return direction === 'top'
      ? document.documentElement.scrollTop || document.body.scrollTop
      : document.documentElement.scrollLeft || document.body.scrollLeft
  } else {
    return direction === 'top' ? elem.scrollTop : elem.scrollLeft
  }
}

const setScroll = (elem, val, direction = 'top') => {
  if (elem === document.body) {
    if (direction === 'top') {
      document.documentElement.scrollTop = val
      window.scrollTo(window.scrollX, val)
    } else {
      document.documentElement.scrollLeft = val
      window.scrollTo(val, window.scrollY)
    }
  } else {
    if (direction === 'top') {
      elem.scrollTop = val
    } else {
      elem.scrollLeft = val
    }
  }
}

const scrollTo = (function scrollToFactory() {
  let request
  const easeOutQuad = function(t, b, c, d) {
    t /= d
    return -c * t * (t - 2) + b
  }
  return function scrollTo({
    targetElem,
    scrollElem,
    topMargin = 0,
    maxDuration = 300,
    easeFn,
    callback
  }) {
    cancelAnimationFrame(request)
    let rect = targetElem.getBoundingClientRect()
    let endScrollTop =
      rect.top -
      (scrollElem === document.body
        ? 0
        : scrollElem.getBoundingClientRect().top) +
      getScroll(scrollElem) -
      topMargin
    let startScrollTop = getScroll(scrollElem)
    let distance = endScrollTop - startScrollTop
    let startTime
    let ease = easeFn || easeOutQuad
    let distanceRatio = Math.min(Math.abs(distance) / 10000, 1)
    let duration = Math.max(
      maxDuration * distanceRatio * (2 - distanceRatio),
      10
    )
    if (!maxDuration) {
      setScroll(scrollElem, endScrollTop)
      if (callback) {
        callback()
      }
    } else {
      requestAnimationFrame(update)
    }

    function update(timestamp) {
      if (!startTime) {
        startTime = timestamp
      }
      let progress = (timestamp - startTime) / duration
      if (progress < 1) {
        setScroll(
          scrollElem,
          ease(timestamp - startTime, startScrollTop, distance, duration)
        )
        requestAnimationFrame(update)
      } else {
        setScroll(scrollElem, endScrollTop)
        if (callback) {
          callback()
        }
      }
    }
  }
})()

function toDash(str) {
  return str.replace(/([A-Z])/g, (match, p1) => '-' + p1.toLowerCase())
}

function applyStyle(elem, style = {}, reset = false) {
  if (reset) {
    elem.style = ''
  }
  if (typeof style === 'string') {
    elem.style = style
  } else {
    for (let prop in style) {
      if (typeof style[prop] === 'number') {
        elem.style.setProperty(toDash(prop), px(style[prop]), 'important')
      } else {
        elem.style.setProperty(toDash(prop), style[prop], 'important')
      }
    }
  }
}

function translate3d(x = 0, y = 0, z = 0) {
  return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, ${Math.round(
    z
  )}px)` // 0.5px => blurred text
}

function setClass(elem, names, delay) {
  if (delay === undefined) {
    elem.classList = names
  } else {
    return setTimeout(() => {
      elem.classList = names
    }, delay)
  }
}

const toast = (function toastFactory() {
  let timers = []
  return function toast(msg) {
    let toast
    insertCSS(toastCSS, 'smarttoc-toast__css')
    if (document.getElementById('smarttoc-toast')) {
      toast = document.getElementById('smarttoc-toast')
    } else {
      toast = document.createElement('DIV')
      toast.id = 'smarttoc-toast'
      document.body.appendChild(toast)
    }
    toast.textContent = msg

    timers.forEach(clearTimeout)
    toast.classList = ''

    const set = setClass.bind(null, toast)

    toast.classList = 'enter'
    timers = [
      set('enter enter-active', 0),
      set('leave', 3000),
      set('leave leave-active', 3000),
      set('', 3000 + 200)
    ]
  }
})()

const insertCSS = function(css, id) {
  if (!document.getElementById(id)) {
    let style = document.createElement('STYLE')
    style.type = 'text/css'
    style.id = id
    style.textContent = css
    document.head.appendChild(style)
    return
  }
}

var tocCSS = "/* EVERYTHING HERE WILL BE '!IMPORTANT'  */\n\n/* reset */\n\n#smarttoc {\n  all: initial;\n}\n\n#smarttoc * {\n  all: unset;\n}\n\n/* container */\n\n#smarttoc {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  position: fixed;\n  max-width: 22em;\n  min-width: 14em;\n  max-height: calc(100vh - 100px);\n  z-index: 10000;\n  box-sizing: border-box;\n  background-color: #fff;\n  color: gray;\n  font-size: calc(12px + 0.1vw);\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: normal;\n  border: 1px solid rgba(158, 158, 158, 0.22);\n  -webkit-font-smoothing: subpixel-antialiased;\n  font-smoothing: subpixel-antialiased;\n  overflow: hidden;\n  will-change: transform, max-width;\n  transition: max-width 0.3s;\n  contain: content;\n}\n\n#smarttoc:hover {\n  max-width: 33vw;\n}\n\n#smarttoc.hidden {\n  display: none;\n}\n\n#smarttoc .handle {\n  -webkit-user-select: none;\n  user-select: none;\n\n  border-bottom: 1px solid rgba(158, 158, 158, 0.22);\n  padding: 0.1em 0.7em;\n  font-variant-caps: inherit;\n  font-variant: small-caps;\n  font-size: 0.9em;\n  color: #bbb;\n  cursor: pointer;\n  text-align: center;\n  opacity: 0;\n  will-change: opacity;\n  transition: opacity 0.3s;\n}\n\n#smarttoc:hover .handle {\n  max-width: 33vw;\n  opacity: 1;\n}\n\n#smarttoc .handle:hover,\n#smarttoc .handle:active {\n  cursor: move;\n}\n\n#smarttoc .handle:active {\n  background: #f9f9f9;\n}\n\n#smarttoc > ul {\n  flex-grow: 1;\n  padding: 0 1.3em 1.3em 1em;\n  overflow-y: auto;\n}\n\n/* all headings  */\n\n#smarttoc ul,\n#smarttoc li {\n  list-style: none;\n  display: block;\n}\n\n#smarttoc a {\n  text-decoration: none;\n  color: gray;\n  display: block;\n  line-height: 1.3;\n  padding-top: 0.2em;\n  padding-bottom: 0.2em;\n  text-overflow: ellipsis;\n  overflow-x: hidden;\n  white-space: nowrap;\n}\n\n#smarttoc a:hover,\n#smarttoc a:active {\n  border-left-color: rgba(86, 61, 124, 0.5);\n  color: #563d7c;\n}\n\n#smarttoc li.active > a {\n  border-left-color: #563d7c;\n  color: #563d7c;\n}\n\n/* heading level: 1 */\n\n#smarttoc ul {\n  line-height: 2;\n}\n\n#smarttoc ul a {\n  font-size: 1em;\n  padding-left: 1.3em;\n cursor:pointer \n}\n\n#smarttoc ul a:hover,\n#smarttoc ul a:active,\n#smarttoc ul li.active > a {\n  border-left-width: 3px;\n  border-left-style: solid;\n  padding-left: calc(1.3em - 3px);\n}\n\n#smarttoc ul li.active > a {\n  font-weight: 700;\n}\n\n/* heading level: 2 (hidden only when there are too many headings)  */\n\n#smarttoc ul ul {\n  line-height: 1.8;\n}\n\n#smarttoc.lengthy ul ul {\n  display: none;\n}\n\n#smarttoc.lengthy ul li.active > ul {\n  display: block;\n}\n\n#smarttoc ul ul a {\n  font-size: 1em;\n  padding-left: 2.7em;\n}\n\n#smarttoc ul ul a:hover,\n#smarttoc ul ul a:active,\n#smarttoc ul ul li.active > a {\n  border-left-width: 2px;\n  border-left-style: solid;\n  padding-left: calc(2.7em - 2px);\n  font-weight: normal;\n}\n\n/* heading level: 3 (hidden unless parent is active) */\n\n#smarttoc ul ul ul {\n  line-height: 1.7;\n  display: none;\n}\n\n#smarttoc ul ul li.active > ul {\n  display: block;\n}\n\n#smarttoc ul ul ul a {\n  font-size: 1em;\n  padding-left: 4em;\n}\n\n#smarttoc ul ul ul a:hover,\n#smarttoc ul ul ul a:active,\n#smarttoc ul ul ul li.active > a {\n  border-left-width: 1px;\n  border-left-style: solid;\n  padding-left: calc(4em - 1px);\n  font-weight: normal;\n}\n";

const proto = {
  subscribe(cb, emitOnSubscribe = true) {
    if (emitOnSubscribe && this.value !== undefined) {
      cb(this.value)
    }
    this.listeners.push(cb)
  },
  addDependent(dependent) {
    this.dependents.push(dependent)
  },
  update(val) {
    this.value = val
    this.changed = true
    this.dependents.forEach(dep => dep.update(val))
  },
  flush() {
    if (this.changed) {
      this.changed = false
      this.listeners.forEach(l => l(this.value))
      this.dependents.forEach(dep => dep.flush())
    }
  },
  unique() {
    let lastValue = this.value
    let $unique = Stream(lastValue)
    this.subscribe(val => {
      if (val !== lastValue) {
        $unique(val)
        lastValue = val
      }
    })
    return $unique
  },
  map(f) {
    return Stream.combine(this, f)
  },
  filter(f) {
    return this.map(output => (f(output) ? output : undefined))
  },
  throttle(delay) {
    let $throttled = Stream(this.value)
    const emit = throttle($throttled, delay)
    this.subscribe(emit)
    return $throttled
  },
  log(name) {
    this.subscribe(e => console.log(`[${name}]: `, e))
    return this
  }
}

function Stream(init) {
  let s = function(val) {
    if (val === undefined) return s.value
    s.update(val)
    s.flush(val)
  }

  s.value = init
  s.changed = false
  s.listeners = []
  s.dependents = []

  return Object.assign(s, proto)
}

Stream.combine = function(...streams) {
  const combiner = streams.pop()
  let cached = streams.map(s => s())
  const combined = Stream(combiner(...cached))

  streams.forEach((s, i) => {
    const dependent = {
      update(val) {
        cached[i] = val
        combined.update(combiner(...cached))
      },
      flush() {
        combined.flush()
      }
    }
    s.addDependent(dependent)
  })

  return combined
}

Stream.interval = function(int) {
  let $interval = Stream()
  setInterval(() => $interval(null), int)
  return $interval
}

Stream.fromEvent = function(elem, type) {
  let $event = Stream()
  elem.addEventListener(type, $event)
  return $event
}

var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

function createCommonjsModule(fn, module) {
	return module = { exports: {} }, fn(module, module.exports), module.exports;
}

var mithril = createCommonjsModule(function (module) {
;(function() {
"use strict"
function Vnode(tag, key, attrs0, children, text, dom) {
	return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, _state: undefined, events: undefined, instance: undefined, skip: false}
}
Vnode.normalize = function(node) {
	if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined)
	if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined)
	return node
}
Vnode.normalizeChildren = function normalizeChildren(children) {
	for (var i = 0; i < children.length; i++) {
		children[i] = Vnode.normalize(children[i])
	}
	return children
}
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
var selectorCache = {}
var hasOwn = {}.hasOwnProperty
function compileSelector(selector) {
	var match, tag = "div", classes = [], attrs = {}
	while (match = selectorParser.exec(selector)) {
		var type = match[1], value = match[2]
		if (type === "" && value !== "") tag = value
		else if (type === "#") attrs.id = value
		else if (type === ".") classes.push(value)
		else if (match[3][0] === "[") {
			var attrValue = match[6]
			if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\")
			if (match[4] === "class") classes.push(attrValue)
			else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true
		}
	}
	if (classes.length > 0) attrs.className = classes.join(" ")
	return selectorCache[selector] = {tag: tag, attrs: attrs}
}
function execSelector(state, attrs, children) {
	var hasAttrs = false, childList, text
	var className = attrs.className || attrs.class
	for (var key in state.attrs) {
		if (hasOwn.call(state.attrs, key)) {
			attrs[key] = state.attrs[key]
		}
	}
	if (className !== undefined) {
		if (attrs.class !== undefined) {
			attrs.class = undefined
			attrs.className = className
		}
		if (state.attrs.className != null) {
			attrs.className = state.attrs.className + " " + className
		}
	}
	for (let key in attrs) {
		if (hasOwn.call(attrs, key) && key !== "key") {
			hasAttrs = true
			break
		}
	}
	if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") {
		text = children[0].children
	} else {
		childList = children
	}
	return Vnode(state.tag, attrs.key, hasAttrs ? attrs : undefined, childList, text)
}
function hyperscript(selector) {
	// Because sloppy mode sucks
	var attrs = arguments[1], start = 2, children
	if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
		throw Error("The selector must be either a string or a component.");
	}
	if (typeof selector === "string") {
		var cached = selectorCache[selector] || compileSelector(selector)
	}
	if (attrs == null) {
		attrs = {}
	} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
		attrs = {}
		start = 1
	}
	if (arguments.length === start + 1) {
		children = arguments[start]
		if (!Array.isArray(children)) children = [children]
	} else {
		children = []
		while (start < arguments.length) children.push(arguments[start++])
	}
	var normalized = Vnode.normalizeChildren(children)
	if (typeof selector === "string") {
		return execSelector(cached, attrs, normalized)
	} else {
		return Vnode(selector, attrs.key, attrs, normalized)
	}
}
hyperscript.trust = function(html) {
	if (html == null) html = ""
	return Vnode("<", undefined, undefined, html, undefined, undefined)
}
hyperscript.fragment = function(attrs1, children) {
	return Vnode("[", attrs1.key, attrs1, Vnode.normalizeChildren(children), undefined, undefined)
}
var m = hyperscript
/** @constructor */
var PromisePolyfill = function(executor) {
	if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`")
	if (typeof executor !== "function") throw new TypeError("executor must be a function")
	var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false)
	var instance = self._instance = {resolvers: resolvers, rejectors: rejectors}
	var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout
	function handler(list, shouldAbsorb) {
		return function execute(value) {
			var then
			try {
				if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") {
					if (value === self) throw new TypeError("Promise can't be resolved w/ itself")
					executeOnce(then.bind(value))
				}
				else {
					callAsync(function() {
						if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value)
						for (var i = 0; i < list.length; i++) list[i](value)
						resolvers.length = 0, rejectors.length = 0
						instance.state = shouldAbsorb
						instance.retry = function() {execute(value)}
					})
				}
			}
			catch (e) {
				rejectCurrent(e)
			}
		}
	}
	function executeOnce(then) {
		var runs = 0
		function run(fn) {
			return function(value) {
				if (runs++ > 0) return
				fn(value)
			}
		}
		var onerror = run(rejectCurrent)
		try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)}
	}
	executeOnce(executor)
}
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) {
	var self = this, instance = self._instance
	function handle(callback, list, next, state) {
		list.push(function(value) {
			if (typeof callback !== "function") next(value)
			else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)}
		})
		if (typeof instance.retry === "function" && state === instance.state) instance.retry()
	}
	var resolveNext, rejectNext
	var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject})
	handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false)
	return promise
}
PromisePolyfill.prototype.catch = function(onRejection) {
	return this.then(null, onRejection)
}
PromisePolyfill.resolve = function(value) {
	if (value instanceof PromisePolyfill) return value
	return new PromisePolyfill(function(resolve) {resolve(value)})
}
PromisePolyfill.reject = function(value) {
	return new PromisePolyfill(function(resolve, reject) {reject(value)})
}
PromisePolyfill.all = function(list) {
	return new PromisePolyfill(function(resolve, reject) {
		var total = list.length, count = 0, values = []
		if (list.length === 0) resolve([])
		else for (var i = 0; i < list.length; i++) {
			(function(i) {
				function consume(value) {
					count++
					values[i] = value
					if (count === total) resolve(values)
				}
				if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") {
					list[i].then(consume, reject)
				}
				else consume(list[i])
			})(i)
		}
	})
}
PromisePolyfill.race = function(list) {
	return new PromisePolyfill(function(resolve, reject) {
		for (var i = 0; i < list.length; i++) {
			list[i].then(resolve, reject)
		}
	})
}
if (typeof window !== "undefined") {
	if (typeof window.Promise === "undefined") window.Promise = PromisePolyfill
	let PromisePolyfill = window.Promise
} else if (typeof commonjsGlobal !== "undefined") {
	if (typeof commonjsGlobal.Promise === "undefined") commonjsGlobal.Promise = PromisePolyfill
	let PromisePolyfill = commonjsGlobal.Promise
} else {
}
var buildQueryString = function(object) {
	if (Object.prototype.toString.call(object) !== "[object Object]") return ""
	var args = []
	for (var key0 in object) {
		destructure(key0, object[key0])
	}
	return args.join("&")
	function destructure(key0, value) {
		if (Array.isArray(value)) {
			for (var i = 0; i < value.length; i++) {
				destructure(key0 + "[" + i + "]", value[i])
			}
		}
		else if (Object.prototype.toString.call(value) === "[object Object]") {
			for (let i in value) {
				destructure(key0 + "[" + i + "]", value[i])
			}
		}
		else args.push(encodeURIComponent(key0) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : ""))
	}
}
var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i")
var _8 = function($window, Promise) {
	var callbackCount = 0
	var oncompletion
	function setCompletionCallback(callback) {oncompletion = callback}
	function finalizer() {
		var count = 0
		function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()}
		return function finalize(promise0) {
			var then0 = promise0.then
			promise0.then = function() {
				count++
				var next = then0.apply(promise0, arguments)
				next.then(complete, function(e) {
					complete()
					if (count === 0) throw e
				})
				return finalize(next)
			}
			return promise0
		}
	}
	function normalize(args, extra) {
		if (typeof args === "string") {
			var url = args
			args = extra || {}
			if (args.url == null) args.url = url
		}
		return args
	}
	function request(args, extra) {
		var finalize = finalizer()
		args = normalize(args, extra)
		var promise0 = new Promise(function(resolve, reject) {
			if (args.method == null) args.method = "GET"
			args.method = args.method.toUpperCase()
			var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true)
			if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify
			if (typeof args.deserialize !== "function") args.deserialize = deserialize
			if (typeof args.extract !== "function") args.extract = extract
			args.url = interpolate(args.url, args.data)
			if (useBody) args.data = args.serialize(args.data)
			else args.url = assemble(args.url, args.data)
			var xhr = new $window.XMLHttpRequest(),
				aborted = false,
				_abort = xhr.abort
			xhr.abort = function abort() {
				aborted = true
				_abort.call(xhr)
			}
			xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined)
			if (args.serialize === JSON.stringify && useBody) {
				xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
			}
			if (args.deserialize === deserialize) {
				xhr.setRequestHeader("Accept", "application/json, text/*")
			}
			if (args.withCredentials) xhr.withCredentials = args.withCredentials
			for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) {
				xhr.setRequestHeader(key, args.headers[key])
			}
			if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr
			xhr.onreadystatechange = function() {
				// Don't throw errors on xhr.abort().
				if(aborted) return
				if (xhr.readyState === 4) {
					try {
						var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args))
						if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) {
							resolve(cast(args.type, response))
						}
						else {
							var error = new Error(xhr.responseText)
							for (var key in response) error[key] = response[key]
							reject(error)
						}
					}
					catch (e) {
						reject(e)
					}
				}
			}
			if (useBody && (args.data != null)) xhr.send(args.data)
			else xhr.send()
		})
		return args.background === true ? promise0 : finalize(promise0)
	}
	function jsonp(args, extra) {
		var finalize = finalizer()
		args = normalize(args, extra)
		var promise0 = new Promise(function(resolve, reject) {
			var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++
			var script = $window.document.createElement("script")
			$window[callbackName] = function(data) {
				script.parentNode.removeChild(script)
				resolve(cast(args.type, data))
				delete $window[callbackName]
			}
			script.onerror = function() {
				script.parentNode.removeChild(script)
				reject(new Error("JSONP request failed"))
				delete $window[callbackName]
			}
			if (args.data == null) args.data = {}
			args.url = interpolate(args.url, args.data)
			args.data[args.callbackKey || "callback"] = callbackName
			script.src = assemble(args.url, args.data)
			$window.document.documentElement.appendChild(script)
		})
		return args.background === true? promise0 : finalize(promise0)
	}
	function interpolate(url, data) {
		if (data == null) return url
		var tokens = url.match(/:[^\/]+/gi) || []
		for (var i = 0; i < tokens.length; i++) {
			var key = tokens[i].slice(1)
			if (data[key] != null) {
				url = url.replace(tokens[i], data[key])
			}
		}
		return url
	}
	function assemble(url, data) {
		var querystring = buildQueryString(data)
		if (querystring !== "") {
			var prefix = url.indexOf("?") < 0 ? "?" : "&"
			url += prefix + querystring
		}
		return url
	}
	function deserialize(data) {
		try {return data !== "" ? JSON.parse(data) : null}
		catch (e) {throw new Error(data)}
	}
	function extract(xhr) {return xhr.responseText}
	function cast(type0, data) {
		if (typeof type0 === "function") {
			if (Array.isArray(data)) {
				for (var i = 0; i < data.length; i++) {
					data[i] = new type0(data[i])
				}
			}
			else return new type0(data)
		}
		return data
	}
	return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback}
}
var requestService = _8(window, PromisePolyfill)
var coreRenderer = function($window) {
	var $doc = $window.document
	var $emptyFragment = $doc.createDocumentFragment()
	var nameSpace = {
		svg: "http://www.w3.org/2000/svg",
		math: "http://www.w3.org/1998/Math/MathML"
	}
	var onevent
	function setEventCallback(callback) {return onevent = callback}
	function getNameSpace(vnode) {
		return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
	}
	//create
	function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
		for (var i = start; i < end; i++) {
			var vnode = vnodes[i]
			if (vnode != null) {
				createNode(parent, vnode, hooks, ns, nextSibling)
			}
		}
	}
	function createNode(parent, vnode, hooks, ns, nextSibling) {
		var tag = vnode.tag
		if (typeof tag === "string") {
			vnode.state = {}
			if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
			switch (tag) {
				case "#": return createText(parent, vnode, nextSibling)
				case "<": return createHTML(parent, vnode, nextSibling)
				case "[": return createFragment(parent, vnode, hooks, ns, nextSibling)
				default: return createElement(parent, vnode, hooks, ns, nextSibling)
			}
		}
		else return createComponent(parent, vnode, hooks, ns, nextSibling)
	}
	function createText(parent, vnode, nextSibling) {
		vnode.dom = $doc.createTextNode(vnode.children)
		insertNode(parent, vnode.dom, nextSibling)
		return vnode.dom
	}
	function createHTML(parent, vnode, nextSibling) {
		var match1 = vnode.children.match(/^\s*?<(\w+)/im) || []
		var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match1[1]] || "div"
		var temp = $doc.createElement(parent1)
		temp.innerHTML = vnode.children
		vnode.dom = temp.firstChild
		vnode.domSize = temp.childNodes.length
		var fragment = $doc.createDocumentFragment()
		var child
		while (child = temp.firstChild) {
			fragment.appendChild(child)
		}
		insertNode(parent, fragment, nextSibling)
		return fragment
	}
	function createFragment(parent, vnode, hooks, ns, nextSibling) {
		var fragment = $doc.createDocumentFragment()
		if (vnode.children != null) {
			var children = vnode.children
			createNodes(fragment, children, 0, children.length, hooks, null, ns)
		}
		vnode.dom = fragment.firstChild
		vnode.domSize = fragment.childNodes.length
		insertNode(parent, fragment, nextSibling)
		return fragment
	}
	function createElement(parent, vnode, hooks, ns, nextSibling) {
		var tag = vnode.tag
		var attrs2 = vnode.attrs
		var is = attrs2 && attrs2.is
		ns = getNameSpace(vnode) || ns
		var element = ns ?
			is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) :
			is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag)
		vnode.dom = element
		if (attrs2 != null) {
			setAttrs(vnode, attrs2, ns)
		}
		insertNode(parent, element, nextSibling)
		if (vnode.attrs != null && vnode.attrs.contenteditable != null) {
			setContentEditable(vnode)
		}
		else {
			if (vnode.text != null) {
				if (vnode.text !== "") element.textContent = vnode.text
				else vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
			}
			if (vnode.children != null) {
				var children = vnode.children
				createNodes(element, children, 0, children.length, hooks, null, ns)
				setLateAttrs(vnode)
			}
		}
		return element
	}
	function initComponent(vnode, hooks) {
		var sentinel
		if (typeof vnode.tag.view === "function") {
			vnode.state = Object.create(vnode.tag)
			sentinel = vnode.state.view
			if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
			sentinel.$$reentrantLock$$ = true
		} else {
			vnode.state = void 0
			sentinel = vnode.tag
			if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
			sentinel.$$reentrantLock$$ = true
			vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
		}
		vnode._state = vnode.state
		if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
		initLifecycle(vnode._state, vnode, hooks)
		vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
		if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
		sentinel.$$reentrantLock$$ = null
	}
	function createComponent(parent, vnode, hooks, ns, nextSibling) {
		initComponent(vnode, hooks)
		if (vnode.instance != null) {
			var element = createNode(parent, vnode.instance, hooks, ns, nextSibling)
			vnode.dom = vnode.instance.dom
			vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0
			insertNode(parent, element, nextSibling)
			return element
		}
		else {
			vnode.domSize = 0
			return $emptyFragment
		}
	}
	//update
	function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) {
		if (old === vnodes || old == null && vnodes == null) return
		else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
		else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
		else {
			if (old.length === vnodes.length) {
				var isUnkeyed = false
				for (var i = 0; i < vnodes.length; i++) {
					if (vnodes[i] != null && old[i] != null) {
						isUnkeyed = vnodes[i].key == null && old[i].key == null
						break
					}
				}
				if (isUnkeyed) {
					for (let i = 0; i < old.length; i++) {
						if (old[i] === vnodes[i]) continue
						else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling))
						else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
						else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns)
					}
					return
				}
			}
			recycling = recycling || isRecyclable(old, vnodes)
			if (recycling) {
				var pool = old.pool
				old = old.concat(old.pool)
			}
			var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
			while (oldEnd >= oldStart && end >= start) {
				var o = old[oldStart], v = vnodes[start]
				if (o === v && !recycling) oldStart++, start++
				else if (o == null) oldStart++
				else if (v == null) start++
				else if (o.key === v.key) {
					var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling)
					oldStart++, start++
					updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns)
					if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
				}
				else {
					let o = old[oldEnd]
					if (o === v && !recycling) oldEnd--, start++
					else if (o == null) oldEnd--
					else if (v == null) start++
					else if (o.key === v.key) {
						let shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
						updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
						if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
						oldEnd--, start++
					}
					else break
				}
			}
			while (oldEnd >= oldStart && end >= start) {
				let o = old[oldEnd], v = vnodes[end]
				if (o === v && !recycling) oldEnd--, end--
				else if (o == null) oldEnd--
				else if (v == null) end--
				else if (o.key === v.key) {
					let shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
					updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
					if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
					if (o.dom != null) nextSibling = o.dom
					oldEnd--, end--
				}
				else {
					if (!map) map = getKeyMap(old, oldEnd)
					if (v != null) {
						var oldIndex = map[v.key]
						if (oldIndex != null) {
							let movable = old[oldIndex]
							let shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling)
							updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
							insertNode(parent, toFragment(movable), nextSibling)
							old[oldIndex].skip = true
							if (movable.dom != null) nextSibling = movable.dom
						}
						else {
							var dom = createNode(parent, v, hooks, ns, nextSibling)
							nextSibling = dom
						}
					}
					end--
				}
				if (end < start) break
			}
			createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
			removeNodes(old, oldStart, oldEnd + 1, vnodes)
		}
	}
	function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
		var oldTag = old.tag, tag = vnode.tag
		if (oldTag === tag) {
			vnode.state = old.state
			vnode._state = old._state
			vnode.events = old.events
			if (!recycling && shouldNotUpdate(vnode, old)) return
			if (typeof oldTag === "string") {
				if (vnode.attrs != null) {
					if (recycling) {
						vnode.state = {}
						initLifecycle(vnode.attrs, vnode, hooks)
					}
					else updateLifecycle(vnode.attrs, vnode, hooks)
				}
				switch (oldTag) {
					case "#": updateText(old, vnode); break
					case "<": updateHTML(parent, old, vnode, nextSibling); break
					case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break
					default: updateElement(old, vnode, recycling, hooks, ns)
				}
			}
			else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns)
		}
		else {
			removeNode(old, null)
			createNode(parent, vnode, hooks, ns, nextSibling)
		}
	}
	function updateText(old, vnode) {
		if (old.children.toString() !== vnode.children.toString()) {
			old.dom.nodeValue = vnode.children
		}
		vnode.dom = old.dom
	}
	function updateHTML(parent, old, vnode, nextSibling) {
		if (old.children !== vnode.children) {
			toFragment(old)
			createHTML(parent, vnode, nextSibling)
		}
		else vnode.dom = old.dom, vnode.domSize = old.domSize
	}
	function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) {
		updateNodes(parent, old.children, vnode.children, recycling, hooks, nextSibling, ns)
		var domSize = 0, children = vnode.children
		vnode.dom = null
		if (children != null) {
			for (var i = 0; i < children.length; i++) {
				var child = children[i]
				if (child != null && child.dom != null) {
					if (vnode.dom == null) vnode.dom = child.dom
					domSize += child.domSize || 1
				}
			}
			if (domSize !== 1) vnode.domSize = domSize
		}
	}
	function updateElement(old, vnode, recycling, hooks, ns) {
		var element = vnode.dom = old.dom
		ns = getNameSpace(vnode) || ns
		if (vnode.tag === "textarea") {
			if (vnode.attrs == null) vnode.attrs = {}
			if (vnode.text != null) {
				vnode.attrs.value = vnode.text //FIXME handle0 multiple children
				vnode.text = undefined
			}
		}
		updateAttrs(vnode, old.attrs, vnode.attrs, ns)
		if (vnode.attrs != null && vnode.attrs.contenteditable != null) {
			setContentEditable(vnode)
		}
		else if (old.text != null && vnode.text != null && vnode.text !== "") {
			if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text
		}
		else {
			if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)]
			if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
			updateNodes(element, old.children, vnode.children, recycling, hooks, null, ns)
		}
	}
	function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) {
		if (recycling) {
			initComponent(vnode, hooks)
		} else {
			vnode.instance = Vnode.normalize(vnode._state.view.call(vnode.state, vnode))
			if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
			if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
			updateLifecycle(vnode._state, vnode, hooks)
		}
		if (vnode.instance != null) {
			if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
			else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling, ns)
			vnode.dom = vnode.instance.dom
			vnode.domSize = vnode.instance.domSize
		}
		else if (old.instance != null) {
			removeNode(old.instance, null)
			vnode.dom = undefined
			vnode.domSize = 0
		}
		else {
			vnode.dom = old.dom
			vnode.domSize = old.domSize
		}
	}
	function isRecyclable(old, vnodes) {
		if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
			var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
			var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0
			var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0
			if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) {
				return true
			}
		}
		return false
	}
	function getKeyMap(vnodes, end) {
		var map = {}, i = 0
		for (let i = 0; i < end; i++) {
			let vnode = vnodes[i]
			if (vnode != null) {
				let key2 = vnode.key
				if (key2 != null) map[key2] = i
			}
		}
		return map
	}
	function toFragment(vnode) {
		var count0 = vnode.domSize
		if (count0 != null || vnode.dom == null) {
			var fragment = $doc.createDocumentFragment()
			if (count0 > 0) {
				var dom = vnode.dom
				while (--count0) fragment.appendChild(dom.nextSibling)
				fragment.insertBefore(dom, fragment.firstChild)
			}
			return fragment
		}
		else return vnode.dom
	}
	function getNextSibling(vnodes, i, nextSibling) {
		for (; i < vnodes.length; i++) {
			if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom
		}
		return nextSibling
	}
	function insertNode(parent, dom, nextSibling) {
		if (nextSibling && nextSibling.parentNode) parent.insertBefore(dom, nextSibling)
		else parent.appendChild(dom)
	}
	function setContentEditable(vnode) {
		var children = vnode.children
		if (children != null && children.length === 1 && children[0].tag === "<") {
			var content = children[0].children
			if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
		}
		else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted")
	}
	//remove
	function removeNodes(vnodes, start, end, context) {
		for (var i = start; i < end; i++) {
			var vnode = vnodes[i]
			if (vnode != null) {
				if (vnode.skip) vnode.skip = false
				else removeNode(vnode, context)
			}
		}
	}
	function removeNode(vnode, context) {
		var expected = 1, called = 0
		if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
			var result = vnode.attrs.onbeforeremove.call(vnode.state, vnode)
			if (result != null && typeof result.then === "function") {
				expected++
				result.then(continuation, continuation)
			}
		}
		if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeremove === "function") {
			let result = vnode._state.onbeforeremove.call(vnode.state, vnode)
			if (result != null && typeof result.then === "function") {
				expected++
				result.then(continuation, continuation)
			}
		}
		continuation()
		function continuation() {
			if (++called === expected) {
				onremove(vnode)
				if (vnode.dom) {
					var count0 = vnode.domSize || 1
					if (count0 > 1) {
						var dom = vnode.dom
						while (--count0) {
							removeNodeFromDOM(dom.nextSibling)
						}
					}
					removeNodeFromDOM(vnode.dom)
					if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements
						if (!context.pool) context.pool = [vnode]
						else context.pool.push(vnode)
					}
				}
			}
		}
	}
	function removeNodeFromDOM(node) {
		var parent = node.parentNode
		if (parent != null) parent.removeChild(node)
	}
	function onremove(vnode) {
		if (vnode.attrs && typeof vnode.attrs.onremove === "function") vnode.attrs.onremove.call(vnode.state, vnode)
		if (typeof vnode.tag !== "string" && typeof vnode._state.onremove === "function") vnode._state.onremove.call(vnode.state, vnode)
		if (vnode.instance != null) onremove(vnode.instance)
		else {
			var children = vnode.children
			if (Array.isArray(children)) {
				for (var i = 0; i < children.length; i++) {
					var child = children[i]
					if (child != null) onremove(child)
				}
			}
		}
	}
	//attrs2
	function setAttrs(vnode, attrs2, ns) {
		for (var key2 in attrs2) {
			setAttr(vnode, key2, null, attrs2[key2], ns)
		}
	}
	function setAttr(vnode, key2, old, value, ns) {
		var element = vnode.dom
		if (key2 === "key" || key2 === "is" || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object" || typeof value === "undefined" || isLifecycleMethod(key2)) return
		var nsLastIndex = key2.indexOf(":")
		if (nsLastIndex > -1 && key2.substr(0, nsLastIndex) === "xlink") {
			element.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(nsLastIndex + 1), value)
		}
		else if (key2[0] === "o" && key2[1] === "n" && typeof value === "function") updateEvent(vnode, key2, value)
		else if (key2 === "style") updateStyle(element, old, value)
		else if (key2 in element && !isAttribute(key2) && ns === undefined && !isCustomElement(vnode)) {
			if (key2 === "value") {
				var normalized0 = "" + value // eslint-disable-line no-implicit-coercion
				//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
				if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return
				//setting select[value] to same value while having select open blinks select dropdown in Chrome
				if (vnode.tag === "select") {
					if (value === null) {
						if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return
					} else {
						if (old !== null && vnode.dom.value === normalized0 && vnode.dom === $doc.activeElement) return
					}
				}
				//setting option[value] to same value while having select open blinks select dropdown in Chrome
				if (vnode.tag === "option" && old != null && vnode.dom.value === normalized0) return
			}
			// If you assign an input type1 that is not supported by IE 11 with an assignment expression, an error0 will occur.
			if (vnode.tag === "input" && key2 === "type") {
				element.setAttribute(key2, value)
				return
			}
			element[key2] = value
		}
		else {
			if (typeof value === "boolean") {
				if (value) element.setAttribute(key2, "")
				else element.removeAttribute(key2)
			}
			else element.setAttribute(key2 === "className" ? "class" : key2, value)
		}
	}
	function setLateAttrs(vnode) {
		var attrs2 = vnode.attrs
		if (vnode.tag === "select" && attrs2 != null) {
			if ("value" in attrs2) setAttr(vnode, "value", null, attrs2.value, undefined)
			if ("selectedIndex" in attrs2) setAttr(vnode, "selectedIndex", null, attrs2.selectedIndex, undefined)
		}
	}
	function updateAttrs(vnode, old, attrs2, ns) {
		if (attrs2 != null) {
			for (let key2 in attrs2) {
				setAttr(vnode, key2, old && old[key2], attrs2[key2], ns)
			}
		}
		if (old != null) {
			for (var key2 in old) {
				if (attrs2 == null || !(key2 in attrs2)) {
					if (key2 === "className") key2 = "class"
					if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined)
					else if (key2 !== "key") vnode.dom.removeAttribute(key2)
				}
			}
		}
	}
	function isFormAttribute(vnode, attr) {
		return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement
	}
	function isLifecycleMethod(attr) {
		return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
	}
	function isAttribute(attr) {
		return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
	}
	function isCustomElement(vnode){
		return vnode.attrs.is || vnode.tag.indexOf("-") > -1
	}
	function hasIntegrationMethods(source) {
		return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
	}
	//style
	function updateStyle(element, old, style) {
		if (old === style) element.style.cssText = "", old = null
		if (style == null) element.style.cssText = ""
		else if (typeof style === "string") element.style.cssText = style
		else {
			if (typeof old === "string") element.style.cssText = ""
			for (var key2 in style) {
				element.style[key2] = style[key2]
			}
			if (old != null && typeof old !== "string") {
				for (var key3 in old) {
					if (!(key3 in style)) element.style[key3] = ""
				}
			}
		}
	}
	//event
	function updateEvent(vnode, key2, value) {
		var element = vnode.dom
		var callback = typeof onevent !== "function" ? value : function(e) {
			var result = value.call(element, e)
			onevent.call(element, e)
			return result
		}
		if (key2 in element) element[key2] = typeof value === "function" ? callback : null
		else {
			var eventName = key2.slice(2)
			if (vnode.events === undefined) vnode.events = {}
			if (vnode.events[key2] === callback) return
			if (vnode.events[key2] != null) element.removeEventListener(eventName, vnode.events[key2], false)
			if (typeof value === "function") {
				vnode.events[key2] = callback
				element.addEventListener(eventName, vnode.events[key2], false)
			}
		}
	}
	//lifecycle
	function initLifecycle(source, vnode, hooks) {
		if (typeof source.oninit === "function") source.oninit.call(vnode.state, vnode)
		if (typeof source.oncreate === "function") hooks.push(source.oncreate.bind(vnode.state, vnode))
	}
	function updateLifecycle(source, vnode, hooks) {
		if (typeof source.onupdate === "function") hooks.push(source.onupdate.bind(vnode.state, vnode))
	}
	function shouldNotUpdate(vnode, old) {
		var forceVnodeUpdate, forceComponentUpdate
		if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") forceVnodeUpdate = vnode.attrs.onbeforeupdate.call(vnode.state, vnode, old)
		if (typeof vnode.tag !== "string" && typeof vnode._state.onbeforeupdate === "function") forceComponentUpdate = vnode._state.onbeforeupdate.call(vnode.state, vnode, old)
		if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
			vnode.dom = old.dom
			vnode.domSize = old.domSize
			vnode.instance = old.instance
			return true
		}
		return false
	}
	function render(dom, vnodes) {
		if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
		var hooks = []
		var active = $doc.activeElement
		var namespace = dom.namespaceURI
		// First time0 rendering into a node clears it out
		if (dom.vnodes == null) dom.textContent = ""
		if (!Array.isArray(vnodes)) vnodes = [vnodes]
		updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
		dom.vnodes = vnodes
		for (var i = 0; i < hooks.length; i++) hooks[i]()
		if ($doc.activeElement !== active) active.focus()
	}
	return {render: render, setEventCallback: setEventCallback}
}
function throttle(callback) {
	//60fps translates to 16.6ms, round it down since setTimeout requires int
	var time = 16
	var last = 0, pending = null
	var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
	return function() {
		var now = Date.now()
		if (last === 0 || now - last >= time) {
			last = now
			callback()
		}
		else if (pending === null) {
			pending = timeout(function() {
				pending = null
				callback()
				last = Date.now()
			}, time - (now - last))
		}
	}
}
var _11 = function($window) {
	var renderService = coreRenderer($window)
	renderService.setEventCallback(function(e) {
		if (e.redraw === false) e.redraw = undefined
		else redraw()
	})
	var callbacks = []
	function subscribe(key1, callback) {
		unsubscribe(key1)
		callbacks.push(key1, throttle(callback))
	}
	function unsubscribe(key1) {
		var index = callbacks.indexOf(key1)
		if (index > -1) callbacks.splice(index, 2)
	}
	function redraw() {
		for (var i = 1; i < callbacks.length; i += 2) {
			callbacks[i]()
		}
	}
	return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
}
var redrawService = _11(window)
requestService.setCompletionCallback(redrawService.redraw)
var _16 = function(redrawService0) {
	return function(root, component) {
		if (component === null) {
			redrawService0.render(root, [])
			redrawService0.unsubscribe(root)
			return
		}

		if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode")

		var run0 = function() {
			redrawService0.render(root, Vnode(component))
		}
		redrawService0.subscribe(root, run0)
		redrawService0.redraw()
	}
}
m.mount = _16(redrawService)
var Promise = PromisePolyfill
var parseQueryString = function(string) {
	if (string === "" || string == null) return {}
	if (string.charAt(0) === "?") string = string.slice(1)
	var entries = string.split("&"), data0 = {}, counters = {}
	for (var i = 0; i < entries.length; i++) {
		var entry = entries[i].split("=")
		var key5 = decodeURIComponent(entry[0])
		var value = entry.length === 2 ? decodeURIComponent(entry[1]) : ""
		if (value === "true") value = true
		else if (value === "false") value = false
		var levels = key5.split(/\]\[?|\[/)
		var cursor = data0
		if (key5.indexOf("[") > -1) levels.pop()
		for (var j = 0; j < levels.length; j++) {
			var level = levels[j], nextLevel = levels[j + 1]
			var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10))
			var isValue = j === levels.length - 1
			if (level === "") {
				var key6 = levels.slice(0, j).join()
				if (counters[key6] == null) counters[key6] = 0
				level = counters[key6]++
			}
			if (cursor[level] == null) {
				cursor[level] = isValue ? value : isNumber ? [] : {}
			}
			cursor = cursor[level]
		}
	}
	return data0
}
var coreRouter = function($window) {
	var supportsPushState = typeof $window.history.pushState === "function"
	var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout
	function normalize1(fragment0) {
		var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent)
		if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data
		return data
	}
	var asyncId
	function debounceAsync(callback0) {
		return function() {
			if (asyncId != null) return
			asyncId = callAsync0(function() {
				asyncId = null
				callback0()
			})
		}
	}
	function parsePath(path, queryData, hashData) {
		var queryIndex = path.indexOf("?")
		var hashIndex = path.indexOf("#")
		var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length
		if (queryIndex > -1) {
			var queryEnd = hashIndex > -1 ? hashIndex : path.length
			var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd))
			for (var key4 in queryParams) queryData[key4] = queryParams[key4]
		}
		if (hashIndex > -1) {
			var hashParams = parseQueryString(path.slice(hashIndex + 1))
			for (var key5 in hashParams) hashData[key5] = hashParams[key5]
		}
		return path.slice(0, pathEnd)
	}
	var router = {prefix: "#!"}
	router.getPath = function() {
		var type2 = router.prefix.charAt(0)
		switch (type2) {
			case "#": return normalize1("hash").slice(router.prefix.length)
			case "?": return normalize1("search").slice(router.prefix.length) + normalize1("hash")
			default: return normalize1("pathname").slice(router.prefix.length) + normalize1("search") + normalize1("hash")
		}
	}
	router.setPath = function(path, data, options) {
		var queryData = {}, hashData = {}
		path = parsePath(path, queryData, hashData)
		if (data != null) {
			for (var key4 in data) queryData[key4] = data[key4]
			path = path.replace(/:([^\/]+)/g, function(match2, token) {
				delete queryData[token]
				return data[token]
			})
		}
		var query = buildQueryString(queryData)
		if (query) path += "?" + query
		var hash = buildQueryString(hashData)
		if (hash) path += "#" + hash
		if (supportsPushState) {
			var state = options ? options.state : null
			var title = options ? options.title : null
			$window.onpopstate()
			if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path)
			else $window.history.pushState(state, title, router.prefix + path)
		}
		else $window.location.href = router.prefix + path
	}
	router.defineRoutes = function(routes, resolve, reject) {
		function resolveRoute() {
			var path = router.getPath()
			var params = {}
			var pathname = parsePath(path, params, params)
			var state = $window.history.state
			if (state != null) {
				for (var k in state) params[k] = state[k]
			}
			for (var route0 in routes) {
				var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
				if (matcher.test(pathname)) {
					pathname.replace(matcher, function() {
						var keys = route0.match(/:[^\/]+/g) || []
						var values = [].slice.call(arguments, 1, -2)
						for (var i = 0; i < keys.length; i++) {
							params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
						}
						resolve(routes[route0], params, path, route0)
					})
					return
				}
			}
			reject(path, params)
		}
		if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute)
		else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute
		resolveRoute()
	}
	return router
}
var _20 = function($window, redrawService0) {
	var routeService = coreRouter($window)
	var identity = function(v) {return v}
	var render1, component, attrs3, currentPath, lastUpdate
	var route = function(root, defaultRoute, routes) {
		if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
		var run1 = function() {
			if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3)))
		}
		var bail = function(path) {
			if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
			else throw new Error("Could not resolve default route " + defaultRoute)
		}
		routeService.defineRoutes(routes, function(payload, params, path) {
			var update = lastUpdate = function(routeResolver, comp) {
				if (update !== lastUpdate) return
				component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
				attrs3 = params, currentPath = path, lastUpdate = null
				render1 = (routeResolver.render || identity).bind(routeResolver)
				run1()
			}
			if (payload.view || typeof payload === "function") update({}, payload)
			else {
				if (payload.onmatch) {
					Promise.resolve(payload.onmatch(params, path)).then(function(resolved) {
						update(payload, resolved)
					}, bail)
				}
				else update(payload, "div")
			}
		}, bail)
		redrawService0.subscribe(root, run1)
	}
	route.set = function(path, data, options) {
		if (lastUpdate != null) {
			options = options || {}
			options.replace = true
		}
		lastUpdate = null
		routeService.setPath(path, data, options)
	}
	route.get = function() {return currentPath}
	route.prefix = function(prefix0) {routeService.prefix = prefix0}
	route.link = function(vnode1) {
		vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href)
		vnode1.dom.onclick = function(e) {
			if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return
			e.preventDefault()
			e.redraw = false
			var href = this.getAttribute("href")
			if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length)
			route.set(href, undefined, undefined)
		}
	}
	route.param = function(key3) {
		if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3]
		return attrs3
	}
	return route
}
m.route = _20(window, redrawService)
m.withAttr = function(attrName, callback1, context) {
	return function(e) {
		callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName))
	}
}
var _28 = coreRenderer(window)
m.render = _28.render
m.redraw = redrawService.redraw
m.request = requestService.request
m.jsonp = requestService.jsonp
m.parseQueryString = parseQueryString
m.buildQueryString = buildQueryString
m.version = "1.1.3"
m.vnode = Vnode
if ('object' !== "undefined") module["exports"] = m
else {}
}());
});

const restrictScroll = function(e) {
  const toc = e.currentTarget
  const maxScroll = toc.scrollHeight - toc.offsetHeight
  if (toc.scrollTop + e.deltaY < 0) {
    toc.scrollTop = 0
    e.preventDefault()
  } else if (toc.scrollTop + e.deltaY > maxScroll) {
    toc.scrollTop = maxScroll
    e.preventDefault()
  }
  e.redraw = false
}

const TOC = function({ $headings, $activeHeading, onClickHeading }) {
  // $activeHeading.subscribe(activeIndex => {})
  const toTree = function(headings) {
    let i = 0
    let tree = { level: 0, children: [] }
    let stack = [tree]
    const top = arr => arr.slice(-1)[0]

    while (i < headings.length) {
      let { level, isActive } = headings[i]
      if (level === stack.length) {
        const node = {
          heading: headings[i],
          children: []
        }
        top(stack).children.push(node)
        stack.push(node)
        if (isActive) {
          stack.forEach(node => {
            if (node.heading) {
              node.heading.isActive = true
            }
          })
        }
        i++
      } else if (level < stack.length) {
        stack.pop()
      } else if (level > stack.length) {
        const node = {
          heading: null,
          children: []
        }
        top(stack).children.push(node)
        stack.push(node)
      }
    }
    return tree
  }

  const UL = (children, { isRoot = false } = {}) =>
    mithril(
      'ul',
      { onwheel: isRoot && restrictScroll, onclick: isRoot && onClickHeading },
      children.map(LI)
    )

  const LI = ({ heading, children }, index) =>
    mithril(
      'li',
      { class: heading && heading.isActive ? 'active' : '', key: index },
      [
        heading &&
          mithril('a', { href: `#${heading.anchor}` }, heading.node.textContent),
        children && children.length && UL(children)
      ].filter(Boolean)
    )

  return {
    oncreate({ dom }) {
      // scroll to heading if out of view
      $activeHeading.subscribe(index => {
        const target = [].slice.apply(dom.querySelectorAll('.active')).pop()
        if (target) {
          const targetRect = target.getBoundingClientRect()
          const containerRect = dom.getBoundingClientRect()
          const outOfView =
            targetRect.top > containerRect.bottom ||
            targetRect.bottom < containerRect.top
          if (outOfView) {
            scrollTo({
              targetElem: target,
              scrollElem: dom,
              maxDuration: 0,
              topMargin: dom.offsetHeight / 2 - target.offsetHeight / 2
            })
          }
        }
      })
      Stream.combine($headings, $activeHeading, () => null).subscribe(_ =>
        mithril.redraw()
      )
    },
    view() {
      $headings().forEach((h, i) => (h.isActive = i === $activeHeading()))
      const tree = toTree($headings())
      return UL(tree.children, { isRoot: true })
    }
  }
}

const stop = e => {
  e.stopPropagation()
  e.preventDefault()
}

const Handle = function({ $userOffset }) {
  let [sClientX, sClientY] = [0, 0]
  let [sOffsetX, sOffsetY] = [0, 0]

  const onDrag = throttle(e => {
    stop(e)
    let [dX, dY] = [e.clientX - sClientX, e.clientY - sClientY]
    $userOffset([sOffsetX + dX, sOffsetY + dY])
    e.redraw = false
  })

  const onDragEnd = e => {
    window.removeEventListener('mousemove', onDrag)
    window.removeEventListener('mouseup', onDragEnd)
    e.redraw = false
  }

  const onDragStart = e => {
    if (e.button === 0) {
      stop(e)
      sClientX = e.clientX
      sClientY = e.clientY
      sOffsetX = $userOffset()[0]
      sOffsetY = $userOffset()[1]
      window.addEventListener('mousemove', onDrag)
      window.addEventListener('mouseup', onDragEnd)
    }
    e.redraw = false
  }

  return {
    view() {
      return mithril(
        '.handle',
        {
          onmousedown: onDragStart
        },
        'table of contents'
      )
    }
  }
}

const ARTICLE_TOC_GAP = 150

const makeSticky = function(options) {
  let {
    ref,
    scrollable,
    popper,
    direction,
    gap,
    $refChange,
    $scroll,
    $offset,
    $topMargin
  } = options
  let $refRect = Stream.combine($refChange, () => {
    let refRect = ref.getBoundingClientRect()
    let refStyle = window.getComputedStyle(ref)
    let scrollTop = getScroll(scrollable, 'top')
    let scrollLeft = getScroll(scrollable, 'left')
    let refFullRect = {
      top: refRect.top - scrollTop,
      right: refRect.right - scrollLeft,
      bottom: refRect.bottom - scrollTop,
      left: refRect.left - scrollLeft,
      width: refRect.width,
      height: refRect.height
    }
    if (refStyle['box-sizing'] === 'border-box') {
      refFullRect.left += num(refStyle['padding-left'])
      refFullRect.right -= num(refStyle['padding-right'])
      refFullRect.width -=
        num(refStyle['padding-left']) + num(refStyle['padding-right'])
    }
    return refFullRect
  })
  let popperMetric = popper.getBoundingClientRect()
  const scrollableTop =
    scrollable === document.body ? 0 : scrollable.getBoundingClientRect().top
  return Stream.combine(
    $refRect,
    $scroll,
    $offset,
    $topMargin,
    (ref, [scrollX, scrollY], [offsetX, offsetY], topMargin) => {
      let x =
        direction === 'right'
          ? ref.right + gap
          : ref.left - gap - popperMetric.width
      x = Math.min(Math.max(0, x), window.innerWidth - popperMetric.width) // restrict to visible area
      let y = Math.max(scrollableTop + topMargin, ref.top - scrollY)
      return {
        position: 'fixed',
        left: 0,
        top: 0,
        transform: translate3d(x + offsetX, y + offsetY)
      }
    }
  )
}

const getOptimalContainerPos = function(article) {
  const {
    top,
    left,
    right,
    bottom,
    height,
    width
  } = article.getBoundingClientRect()

  const depthOf = function(elem) {
    let depth = 0
    while (elem) {
      elem = elem.parentElement
      depth++
    }
    return depth
  }
  const depthOfPoint = function([x, y]) {
    const elem = document.elementFromPoint(x, y)
    return elem && depthOf(elem)
  }
  const gap = ARTICLE_TOC_GAP
  const testWidth = 200
  const testHeight = 400
  const leftSlotTestPoints = [
    left - gap - testWidth,
    left - gap - testWidth / 2,
    left - gap
  ]
    .map(x => [top, top + testHeight / 2, top + testHeight].map(y => [x, y]))
    .reduce((prev, cur) => prev.concat(cur), [])
  const rightSlotTestPoints = [
    right + gap,
    right + gap + testWidth / 2,
    right + gap + testWidth
  ]
    .map(x => [top, top + testHeight / 2, top + testHeight].map(y => [x, y]))
    .reduce((prev, cur) => prev.concat(cur), [])
  const leftDepths = leftSlotTestPoints.map(depthOfPoint).filter(Boolean)
  const rightDepths = rightSlotTestPoints.map(depthOfPoint).filter(Boolean)
  const leftAvgDepth = leftDepths.length
    ? leftDepths.reduce((a, b) => a + b, 0) / leftDepths.length
    : null
  const rightAvgDepth = rightDepths.length
    ? rightDepths.reduce((a, b) => a + b, 0) / rightDepths.length
    : null

  if (!leftAvgDepth) return { direction: 'right' }
  if (!rightAvgDepth) return { direction: 'left' }
  const spaceDiff = document.documentElement.offsetWidth - right - left
  const scoreDiff =
    spaceDiff * 1 + (rightAvgDepth - leftAvgDepth) * 9 * -10 + 20 // I do like right better
  return scoreDiff > 0 ? { direction: 'right' } : { direction: 'left' }
}

const Container = function({
  article,
  scrollable,
  $headings,
  theme,
  $activeHeading,
  $isShow,
  $userOffset,
  $relayout,
  $scroll,
  $topbarHeight,
  onClickHeading
}) {
  const handle = Handle({ $userOffset })
  const toc = TOC({ $headings, $activeHeading, onClickHeading })
  return {
    oncreate({ dom }) {
      const { direction } = getOptimalContainerPos(article)
      this.$style = makeSticky({
        ref: article,
        scrollable: scrollable,
        popper: dom,
        direction: direction,
        gap: ARTICLE_TOC_GAP,
        $topMargin: $topbarHeight.map(h => (h || 0) + 50),
        $refChange: $relayout,
        $scroll: $scroll,
        $offset: $userOffset
      })
      this.$style.subscribe(_ => mithril.redraw())
    },
    view() {
      return mithril(
        '#smarttoc',
        {
          class: [
            theme || 'light',
            $headings().filter(h => h.level <= 2).length > 50 && 'lengthy',
            $isShow() ? '' : 'hidden'
          ]
            .filter(Boolean)
            .join(' '),
          style: this.$style && this.$style()
        },
        [mithril(handle), mithril(toc)]
      )
    }
  }
}

const Extender = function({ $headings, scrollable, $isShow, $relayout }) {
  const $extender = Stream()
  // toc: extend body height so we can scroll to the last heading
  let extender = document.createElement('DIV')
  extender.id = 'smarttoc-extender'
  Stream.combine($isShow, $relayout, $headings, (isShow, _, headings) => {
    setTimeout(() => {
      // some delay to ensure page is stable ?
      let lastHeading = headings.slice(-1)[0].node
      let lastRect = lastHeading.getBoundingClientRect()
      let extenderHeight = 0
      if (scrollable === document.body) {
        let heightBelowLastRect =
          document.documentElement.scrollHeight -
          (lastRect.bottom + document.documentElement.scrollTop) -
          num(extender.style.height) // in case we are there already
        extenderHeight = isShow
          ? Math.max(
              window.innerHeight - lastRect.height - heightBelowLastRect,
              0
            )
          : 0
      } else {
        let scrollRect = scrollable.getBoundingClientRect()
        let heightBelowLastRect =
          scrollRect.top +
          scrollable.scrollHeight -
          getScroll(scrollable) - // bottom of scrollable relative to viewport
          lastRect.bottom -
          num(extender.style.height) // in case we are there already
        extenderHeight = isShow
          ? Math.max(
              scrollRect.height - lastRect.height - heightBelowLastRect,
              0
            )
          : 0
      }
      $extender({
        height: extenderHeight
      })
    }, 300)
  })
  $extender.subscribe(style => applyStyle(extender, style))
  return extender
}

const relayoutStream = function(article, $resize, $isShow) {
  const readableStyle = function(article) {
    let computed = window.getComputedStyle(article)
    let fontSize = num(computed.fontSize)
    let bestWidth = Math.min(Math.max(fontSize, 12), 16) * 66
    if (computed['box-sizing'] === 'border-box') {
      bestWidth +=
        num(computed['padding-left']) + num(computed['padding-right'])
    }

    return Object.assign(
      num(computed.marginLeft) || num(computed.marginRight)
        ? {}
        : {
            marginLeft: 'auto',
            marginRight: 'auto'
          },
      num(computed.maxWidth)
        ? {}
        : {
            maxWidth: bestWidth
          }
    )
  }
  let oldStyle = article.style.cssText
  let newStyle = readableStyle(article)
  let $relayout = $isShow.map(isShow => {
    if (isShow) {
      applyStyle(article, newStyle)
      return article
    } else {
      applyStyle(article, oldStyle)
    }
  })
  return Stream.combine($relayout, $resize, () => null)
}

const addAnchors = function(headings) {
  const anchoredHeadings = headings.map(function({ node, level, anchor }) {
    if (!anchor) {
      anchor =
        node.id ||
        [].slice
          .apply(node.children)
          .filter(elem => elem.tagName === 'A')
          .map(a => {
            let href = a.getAttribute('href') || ''
            return href.startsWith('#') ? href.substr(1) : a.id
          })
          .filter(Boolean)[0]
      if (!anchor) {
        anchor = node.id = unique(safe(node.textContent))
      } else {
        anchor = unique(anchor)
      }
    }
    return { node, level, anchor }
  })
  return anchoredHeadings
}

const getScrollParent = function(elem) {
  const canScroll = el =>
    ['auto', 'scroll'].includes(window.getComputedStyle(el).overflowY) &&
    el.clientHeight + 1 < el.scrollHeight
  while (elem && elem !== document.body && !canScroll(elem)) {
    elem = elem.parentElement
  }
  log('scrollable', elem)
  draw(elem, 'purple')
  return elem
}

const scrollStream = function(scrollable, $isShow) {
  let $scroll = Stream([getScroll(scrollable, 'left'), getScroll(scrollable)])
  let source = scrollable === document.body ? window : scrollable
  Stream.fromEvent(source, 'scroll')
    .filter(() => $isShow())
    .throttle()
    .subscribe(() => {
      $scroll([getScroll(scrollable, 'left'), getScroll(scrollable)])
    })
  return $scroll
}

const activeHeadingStream = function(
  $headings,
  scrollable,
  $scroll,
  $relayout,
  $topbarHeight
) {
  const $headingScrollYs = Stream.combine(
    $relayout,
    $headings,
    (_, headings) => {
      const scrollableTop =
        (scrollable === document.body
          ? 0
          : scrollable.getBoundingClientRect().top) -
        getScroll(scrollable, 'top')
      return headings.map(
        ({ node }) => node.getBoundingClientRect().top - scrollableTop
      )
    }
  )

  let $curIndex = Stream.combine(
    $headingScrollYs,
    $scroll,
    $topbarHeight,
    function(headingScrollYs, [scrollX, scrollY], topbarHeight = 0) {
      let i = 0
      for (let len = headingScrollYs.length; i < len; i++) {
        if (headingScrollYs[i] > scrollY + topbarHeight + 20) {
          break
        }
      }
      return Math.max(0, i - 1)
    }
  )

  return $curIndex.unique()
}

const scrollToHeading = function(
  { node },
  scrollElem,
  onScrollEnd,
  topMargin = 0
) {
  scrollTo({
    targetElem: node,
    scrollElem: scrollElem,
    topMargin: topMargin,
    maxDuration: 300,
    callback: onScrollEnd && onScrollEnd.bind(null, node)
  })
}

const getTopBarHeight = function(topElem) {
  const findFixedParent = function(elem) {
    const isFixed = elem => {
      let { position, zIndex } = window.getComputedStyle(elem)
      return position === 'fixed' && zIndex
    }
    while (elem !== document.body && !isFixed(elem)) {
      elem = elem.parentElement
    }
    return elem === document.body ? null : elem
  }
  let { left, right, top } = topElem.getBoundingClientRect()
  let leftTopmost = document.elementFromPoint(left + 1, top + 1)
  let rightTopmost = document.elementFromPoint(right - 1, top + 1)
  if (
    leftTopmost &&
    rightTopmost &&
    leftTopmost !== topElem &&
    rightTopmost !== topElem
  ) {
    let leftFixed = findFixedParent(leftTopmost)
    let rightFixed = findFixedParent(rightTopmost)
    if (leftFixed && leftFixed === rightFixed) {
      return leftFixed.offsetHeight
    } else {
      return 0
    }
  } else {
    return 0
  }
}

const getTheme = function(article) {
  let elem = article
  try {
    const parseColor = str =>
      str.replace(/rgba?\(/, '').replace(/\).*/, '').split(/, ?/)
    const getBgColor = elem =>
      parseColor(window.getComputedStyle(elem)['background-color'])
    const isTransparent = ([r, g, b, a]) => a === 0
    const isLight = ([r, g, b, a]) => r + g + b > 255 / 2 * 3
    while (elem && elem.parentElement) {
      const color = getBgColor(elem)
      if (isTransparent(color)) {
        elem = elem.parentElement
      } else {
        return isLight(color) ? 'light' : 'dark'
      }
    }
    return 'light'
  } catch (e) {
    return 'light'
  }
}

const getRoot = function() {
  let root = document.getElementById('smarttoc_wrapper')
  if (!root) {
    root = document.body.appendChild(document.createElement('DIV'))
    root.id = 'smarttoc_wrapper'
  }
  return root
}


// 生成目录
function createTOC({
  article,
  $headings: $headings_,
  userOffset = [0, 0]
}) {
  const $headings = $headings_.map(addAnchors)
  insertCSS(tocCSS, 'smarttoc__css')

  const scrollable = getScrollParent(article)
  const theme = getTheme(article)
  log('theme', theme)

  const $isShow = Stream(true)
  const $topbarHeight = Stream()
  const $resize = Stream.combine(
    Stream.fromEvent(window, 'resize'),
    Stream.fromEvent(document, 'readystatechange'),
    Stream.fromEvent(document, 'load'),
    Stream.fromEvent(document, 'DOMContentLoaded'),
    () => null
  )
    .filter(() => $isShow())
    .throttle()
  const $scroll = scrollStream(scrollable, $isShow)
  const $relayout = relayoutStream(article, $resize, $isShow)
  const $activeHeading = activeHeadingStream(
    $headings,
    scrollable,
    $scroll,
    $relayout,
    $topbarHeight
  )
  const $userOffset = Stream(userOffset)

  /*
  scrollable.appendChild(
    Extender({ $headings, scrollable, $isShow, $relayout })
  )
  */

  const onScrollEnd = function(node) {
    if ($topbarHeight() == null) {
      setTimeout(() => {
        $topbarHeight(getTopBarHeight(node))
        log('topBarHeight', $topbarHeight())
        if ($topbarHeight()) {
          scrollToHeading({ node }, scrollable, null, $topbarHeight() + 10)
        }
      }, 300)
    }
  }

  const onClickHeading = function(e) {
    e.preventDefault()
    e.stopPropagation()
    const anchor = e.target.getAttribute('href').substr(1)
    const heading = $headings().find(heading => heading.anchor === anchor)
    scrollToHeading(
      heading,
      scrollable,
      onScrollEnd,
      ($topbarHeight() || 0) + 10
    )
  }

  mithril.mount(
    getRoot(),
    Container({
      article,
      scrollable,
      $headings,
      theme,
      $activeHeading,
      $isShow,
      $userOffset,
      $relayout,
      $scroll,
      $topbarHeight,
      onClickHeading
    })
  )

  // now show what we've found
  if (article.getBoundingClientRect().top > window.innerHeight - 50) {
    scrollToHeading(
      $headings()[0],
      scrollable,
      onScrollEnd,
      ($topbarHeight() || 0) + 10
    )
  }

  return {
    isValid: () =>
      document.body.contains(article) && article.contains($headings()[0].node),

    isShow: () => $isShow(),

    toggle: () => $isShow(!$isShow()),

    next: () => {
      if ($isShow()) {
        let nextIdx = Math.min($headings().length - 1, $activeHeading() + 1)
        scrollToHeading(
          $headings()[nextIdx],
          scrollable,
          onScrollEnd,
          ($topbarHeight() || 0) + 10
        )
      }
    },

    prev: () => {
      if ($isShow()) {
        let prevIdx = Math.max(0, $activeHeading() - 1)
        scrollToHeading(
          $headings()[prevIdx],
          scrollable,
          onScrollEnd,
          ($topbarHeight() || 0) + 10
        )
      }
    },

    dispose: () => {
      log('dispose')
      $isShow(false)
      mithril.render(getRoot(), mithril(''))
      return { userOffset: $userOffset() }
    }
  }
}

const pathToTop = function(elem, maxLvl = -1) {
  assert(elem, 'no element given')
  const path = []
  while (elem && maxLvl--) {
    path.push(elem)
    elem = elem.parentElement
  }
  return path
}

const isStrongAlsoHeading = function(rootElement = document) {
  return false
  // return rootElement.querySelectorAll('p > strong:only-child').length > 3
}

const extractArticle = function(rootElement = document) {
  log('extracting article')

  const scores = new Map()

  function addScore(elem, inc) {
    scores.set(elem, (scores.get(elem) || 0) + inc)
  }

  function updateScore(elem, weight) {
    let path = pathToTop(elem, weight.length)
    path.forEach((elem, distance) => addScore(elem, weight[distance]))
  }

  // weigh nodes by factor: "selector", "distance from this node"
  const weights = {
    h1: [0, 100, 60, 40, 30, 25, 22].map(s => s * 0.4),
    h2: [0, 100, 60, 40, 30, 25, 22],
    h3: [0, 100, 60, 40, 30, 25, 22].map(s => s * 0.5),
    h4: [0, 100, 60, 40, 30, 25, 22].map(s => s * 0.5 * 0.5),
    h5: [0, 100, 60, 40, 30, 25, 22].map(s => s * 0.5 * 0.5 * 0.5),
    h6: [0, 100, 60, 40, 30, 25, 22].map(s => s * 0.5 * 0.5 * 0.5 * 0.5),
    article: [500],
    '.article': [500],
    '.content': [101],
    sidebar: [-500],
    '.sidebar': [-500],
    aside: [-500],
    '.aside': [-500],
    nav: [-500],
    '.nav': [-500],
    '.navigation': [-500],
    '.toc': [-500],
    '.table-of-contents': [-500]
  }
  const selectors = Object.keys(weights)
  selectors
    .map(selector => ({
      selector: selector,
      elems: [].slice.apply(rootElement.querySelectorAll(selector))
    }))
    .forEach(({ selector, elems }) =>
      elems.forEach(elem => updateScore(elem, weights[selector]))
    )
  const sorted = [...scores].sort((a, b) => b[1] - a[1])

  // reweigh top 5 nodes by factor:  "take-lots-vertical-space", "contain-less-links", "too-narrow"
  let candicates = sorted
    .slice(0, 5)
    .filter(Boolean)
    .map(([elem, score]) => ({ elem, score }))

  let isTooNarrow = e => e.scrollWidth < 400 // rule out sidebars

  candicates.forEach(c => {
    if (isTooNarrow(c.elem)) {
      c.isNarrow = true
      candicates.forEach(parent => {
        if (parent.elem.contains(c.elem)) {
          parent.score *= 0.7
        }
      })
    }
  })
  candicates = candicates.filter(c => !c.isNarrow)

  const reweighted = candicates
    .map(({ elem, score }) => [
      elem,
      score *
        Math.log(
          elem.scrollHeight *
            elem.scrollHeight /
            (elem.querySelectorAll('a').length || 1)
        ),
      elem.scrollHeight,
      elem.querySelectorAll('a').length
    ])
    .sort((a, b) => b[1] - a[1])

  const article = reweighted.length ? reweighted[0][0] : null
  return article
}

const extractHeadings = function(article) {
  log('extracting heading')

  // what to be considered as headings
  const tags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].concat(
    isStrongAlsoHeading(article) ? 'STRONG' : []
  )
  const tagWeight = tag =>
    ({ H1: 4, H2: 9, H3: 9, H4: 10, H5: 10, H6: 10, STRONG: 10 }[tag])
  const isVisible = elem => elem.offsetHeight !== 0
  const isGroupVisible = headings =>
    headings.filter(isVisible).length >= headings.length * 0.5
  const headingGroup = tags
    .map(tag => [].slice.apply(article.getElementsByTagName(tag)))
    .map((headings, i) => ({
      elems: headings,
      tag: tags[i],
      score: headings.length * tagWeight(tags[i])
    }))
    .filter(heading => heading.score >= 10)
    .filter(heading => isGroupVisible(heading.elems))
    .slice(0, 3)

  // use document sequence
  const validTags = headingGroup.map(headings => headings.tag)
  const acceptNode = node =>
    validTags.includes(node.tagName) && isVisible(node)
      ? NodeFilter.FILTER_ACCEPT
      : NodeFilter.FILTER_SKIP
  const treeWalker = document.createTreeWalker(
    article,
    NodeFilter.SHOW_ELEMENT,
    { acceptNode }
  )
  const headings = []
  while (treeWalker.nextNode()) {
    let node = treeWalker.currentNode
    headings.push({
      node,
      level: validTags.indexOf(node.tagName) + 1
    })
  }
  if (false) {}
  return headings
}

function extract() {
  const article = extractArticle(document)
  let $headings
  if (article) {
    $headings = Stream(extractHeadings(article))

    const $articleChange = Stream(null)
    const observer = new MutationObserver(_ => $articleChange(null))
    observer.observe(article, { childList: true })

    $articleChange.throttle(200).subscribe(_ => {
      let headings = extractHeadings(article)
      if (headings && headings.length) {
        $headings(headings)
      }
    })
  }

  return [article, $headings]
}

if (isMasterFrame(window)) {
  let toc

  const generate = function(option = {}) {
    let [article, $headings] = extract()
    if (article && $headings && $headings().length) {
      return createTOC(Object.assign({ article, $headings }, option))
    } else {
      toast('No article/headings are detected.')
      return null
    }
  }

  toc = generate()

  setInterval(() => {
    if (toc && !toc.isValid()) {
      let lastState = toc.dispose()
      toc = generate(lastState)
    }
  }, 3000)

  /*chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    try {
      if (toc) {
        toc[request]()
      } else {
        toc = generate()
      }
    } catch (e) {
      console.error(e)
    }
    sendResponse(true)
  })*/
}

}());