import { HAS_TEXT_BLOCK_REG } from '../config'
import { setCursorAtLastLine } from '../codeMirror'
import { getUniqueId } from '../utils'
import selection from '../selection'
import StateRender from '../parser/StateRender'
import enterCtrl from './enterCtrl'
import updateCtrl from './updateCtrl'
import backspaceCtrl from './backspaceCtrl'
import codeBlockCtrl from './codeBlockCtrl'
import tableBlockCtrl from './tableBlockCtrl'
import History from './history'
import arrowCtrl from './arrowCtrl'
import pasteCtrl from './pasteCtrl'
import copyCutCtrl from './copyCutCtrl'
import paragraphCtrl from './paragraphCtrl'
import tabCtrl from './tabCtrl'
import formatCtrl from './formatCtrl'
import searchCtrl from './searchCtrl'
import mathCtrl from './mathCtrl'
import imagePathCtrl from './imagePathCtrl'
import htmlBlockCtrl from './htmlBlock'
import importMarkdown from '../utils/importMarkdown'

const prototypes = [
  tabCtrl,
  enterCtrl,
  updateCtrl,
  backspaceCtrl,
  codeBlockCtrl,
  arrowCtrl,
  pasteCtrl,
  copyCutCtrl,
  tableBlockCtrl,
  paragraphCtrl,
  formatCtrl,
  searchCtrl,
  mathCtrl,
  imagePathCtrl,
  htmlBlockCtrl,
  importMarkdown
]

class ContentState {
  constructor (options) {
    const { eventCenter } = options
    Object.assign(this, options)
    // Use to cache the keys which you don't want to remove.
    this.exemption = new Set()
    this.blocks = [ this.createBlockP() ]
    this.stateRender = new StateRender(eventCenter)
    this.codeBlocks = new Map()
    this.loadMathMap = new Map()
    this.renderRange = [ null, null ]
    this.currentCursor = null
    this.prevCursor = null
    this.historyTimer = null
    this.history = new History(this)
    this.init()
  }

  set cursor (cursor) {
    if (this.currentCursor) {
      const { start, end } = this.currentCursor
      if (
        start.key === cursor.start.key &&
        start.offset === cursor.start.offset &&
        end.key === cursor.end.key &&
        end.offset === cursor.end.offset
      ) {
        return
      }
    }

    const handler = () => {
      const { blocks, renderRange, currentCursor } = this
      this.history.push({
        type: 'normal',
        blocks,
        renderRange,
        cursor: currentCursor
      })
    }
    this.prevCursor = this.currentCursor
    this.currentCursor = cursor

    if (!cursor.noHistory) {
      if (
        this.prevCursor && (this.prevCursor.start.key !== cursor.start.key || this.prevCursor.end.key !== cursor.end.key)
      ) {
        handler()
      } else {
        if (this.historyTimer) clearTimeout(this.historyTimer)
        this.historyTimer = setTimeout(handler, 2000)
      }
    } else {
      cursor.noHistory && delete cursor.noHistory
    }
  }

  get cursor () {
    return this.currentCursor
  }

  init () {
    const lastBlock = this.getLastBlock()
    const { key, text } = lastBlock
    const offset = text.length
    this.searchMatches = {
      value: '', // the search value
      matches: [], // matches
      index: -1 // active match
    }
    this.cursor = {
      start: { key, offset },
      end: { key, offset }
    }
  }

  setCursor () {
    const { start: { key } } = this.cursor
    const block = this.getBlock(key)
    if (block.type === 'pre' && block.functionType !== 'frontmatter') {
      const cm = this.codeBlocks.get(key)
      const { pos } = block
      if (pos) {
        cm.focus()
        cm.setCursor(pos)
      } else {
        setCursorAtLastLine(cm)
      }
    } else {
      selection.setCursorRange(this.cursor)
    }
  }

  setNextRenderRange () {
    const { start, end } = this.cursor
    // console.log(JSON.stringify(this.cursor, null, 2))
    const startBlock = this.getBlock(start.key)
    const endBlock = this.getBlock(end.key)
    const startOutMostBlock = this.findOutMostBlock(startBlock)
    const endOutMostBlock = this.findOutMostBlock(endBlock)
    this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
  }

  render (isRenderCursor = true) {
    const { blocks, cursor, searchMatches: { matches, index } } = this
    const activeBlocks = this.getActiveBlocks()
    matches.forEach((m, i) => {
      m.active = i === index
    })
    this.setNextRenderRange()
    this.stateRender.render(blocks, cursor, activeBlocks, matches)
    this.pre2CodeMirror(isRenderCursor)
    this.renderMath()
    if (isRenderCursor) this.setCursor()
  }

  partialRender () {
    const { blocks, cursor, searchMatches: { matches, index } } = this
    const activeBlocks = this.getActiveBlocks()
    const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
    const [ startKey, endKey ] = this.renderRange
    matches.forEach((m, i) => {
      m.active = i === index
    })
    // console.log(startKey, endKey)
    const startIndex = startKey ? blocks.findIndex(block => block.key === startKey) : 0
    const endIndex = endKey ? blocks.findIndex(block => block.key === endKey) + 1 : blocks.length
    const needRenderBlocks = blocks.slice(startIndex, endIndex)
    // console.log(JSON.stringify(needRenderBlocks, null, 2), startKey, endKey)
    this.setNextRenderRange()
    this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey)
    this.pre2CodeMirror(true, [...new Set([cursorOutMostBlock, ...needRenderBlocks])])
    this.renderMath([...new Set([cursorOutMostBlock, ...needRenderBlocks])])
    this.setCursor()
  }

  /**
   * A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph.
   * a line block must in a `p block` or `pre block(frontmatter)` and `p block`'s children must be line blocks.
   */
  createBlock (type = 'span', text = '') { // span type means it is a line block.
    const key = getUniqueId()
    return {
      key,
      type,
      text,
      parent: null,
      preSibling: null,
      nextSibling: null,
      children: []
    }
  }

  createBlockP (text = '') {
    const pBlock = this.createBlock('p')
    const lineBlock = this.createBlock('span', text)
    this.appendChild(pBlock, lineBlock)
    return pBlock
  }

  isCollapse (cursor = this.cursor) {
    const { start, end } = cursor
    return start.key === end.key && start.offset === end.offset
  }

  // getBlocks
  getBlocks () {
    return this.blocks
  }

  getCursor () {
    return this.cursor
  }

  getBlock (key) {
    if (!key) return null
    let result = null
    const travel = blocks => {
      for (const block of blocks) {
        if (block.key === key) {
          result = block
          return
        }
        const { children } = block
        if (children.length) {
          travel(children)
        }
      }
    }
    travel(this.blocks)
    return result
  }

  getParent (block) {
    if (block && block.parent) {
      return this.getBlock(block.parent)
    }
    return null
  }
  // return block and its parents
  getParents (block) {
    const result = []
    result.push(block)
    let parent = this.getParent(block)
    while (parent) {
      result.push(parent)
      parent = this.getParent(parent)
    }
    return result
  }

  getPreSibling (block) {
    return block.preSibling ? this.getBlock(block.preSibling) : null
  }

  getNextSibling (block) {
    return block.nextSibling ? this.getBlock(block.nextSibling) : null
  }

  /**
   * if target is descendant of parent return true, else return false
   * @param  {[type]}  parent [description]
   * @param  {[type]}  target [description]
   * @return {Boolean}        [description]
   */
  isInclude (parent, target) {
    const children = parent.children
    if (children.length === 0) {
      return false
    } else {
      if (children.some(child => child.key === target.key)) {
        return true
      } else {
        return children.some(child => this.isInclude(child, target))
      }
    }
  }

  removeTextOrBlock (block) {
    const checkerIn = block => {
      if (this.exemption.has(block.key)) {
        return true
      } else {
        const parent = this.getBlock(block.parent)
        return parent ? checkerIn(parent) : false
      }
    }

    const checkerOut = block => {
      const children = block.children
      if (children.length) {
        if (children.some(child => this.exemption.has(child.key))) {
          return true
        } else {
          return children.some(child => checkerOut(child))
        }
      } else {
        return false
      }
    }

    if (checkerIn(block) || checkerOut(block)) {
      block.text = ''
      const { children } = block
      if (children.length) {
        children.forEach(child => this.removeTextOrBlock(child))
      }
    } else {
      this.removeBlock(block)
    }
  }
  // help func in removeBlocks
  findFigure (block) {
    if (block.type === 'figure') {
      return block.key
    } else {
      const parent = this.getBlock(block.parent)
      return this.findFigure(parent)
    }
  }

  /**
   * remove blocks between before and after, and includes after block.
   */
  removeBlocks (before, after, isRemoveAfter = true, isRecursion = false) {
    if (!isRecursion) {
      if (/td|th/.test(before.type)) {
        this.exemption.add(this.findFigure(before))
      }
      if (/td|th/.test(after.type)) {
        this.exemption.add(this.findFigure(after))
      }
    }
    let nextSibling = this.getBlock(before.nextSibling)
    let beforeEnd = false
    while (nextSibling) {
      if (nextSibling.key === after.key || this.isInclude(nextSibling, after)) {
        beforeEnd = true
        break
      }
      this.removeTextOrBlock(nextSibling)
      nextSibling = this.getBlock(nextSibling.nextSibling)
    }
    if (!beforeEnd) {
      const parent = this.getParent(before)
      if (parent) {
        this.removeBlocks(parent, after, false, true)
      }
    }
    let preSibling = this.getBlock(after.preSibling)
    let afterEnd = false
    while (preSibling) {
      if (preSibling.key === before.key || this.isInclude(preSibling, before)) {
        afterEnd = true
        break
      }
      this.removeTextOrBlock(preSibling)
      preSibling = this.getBlock(preSibling.preSibling)
    }
    if (!afterEnd) {
      const parent = this.getParent(after)
      if (parent) {
        const isOnlyChild = this.isOnlyChild(after)
        this.removeBlocks(before, parent, isOnlyChild, true)
      }
    }
    if (isRemoveAfter) {
      this.removeTextOrBlock(after)
    }
    if (!isRecursion) {
      this.exemption.clear()
    }
  }

  removeBlock (block, fromBlocks = this.blocks) {
    if (block.type === 'pre') {
      const codeBlockId = block.key
      if (this.codeBlocks.has(codeBlockId)) {
        this.codeBlocks.delete(codeBlockId)
      }
    }
    const remove = (blocks, block) => {
      const len = blocks.length
      let i
      for (i = 0; i < len; i++) {
        if (blocks[i].key === block.key) {
          const preSibling = this.getBlock(block.preSibling)
          const nextSibling = this.getBlock(block.nextSibling)

          if (preSibling) {
            preSibling.nextSibling = nextSibling ? nextSibling.key : null
          }
          if (nextSibling) {
            nextSibling.preSibling = preSibling ? preSibling.key : null
          }

          return blocks.splice(i, 1)
        } else {
          if (blocks[i].children.length) {
            remove(blocks[i].children, block)
          }
        }
      }
    }
    remove(Array.isArray(fromBlocks) ? fromBlocks : fromBlocks.children, block)
  }

  getActiveBlocks () {
    let result = []
    let block = this.getBlock(this.cursor.start.key)
    if (block) result.push(block)
    while (block && block.parent) {
      block = this.getBlock(block.parent)
      result.push(block)
    }
    return result
  }

  insertAfter (newBlock, oldBlock) {
    const siblings = oldBlock.parent ? this.getBlock(oldBlock.parent).children : this.blocks
    const oldNextSibling = this.getBlock(oldBlock.nextSibling)
    const index = this.findIndex(siblings, oldBlock)
    siblings.splice(index + 1, 0, newBlock)
    oldBlock.nextSibling = newBlock.key
    newBlock.parent = oldBlock.parent
    newBlock.preSibling = oldBlock.key
    if (oldNextSibling) {
      newBlock.nextSibling = oldNextSibling.key
      oldNextSibling.preSibling = newBlock.key
    }
  }

  insertBefore (newBlock, oldBlock) {
    const siblings = oldBlock.parent ? this.getBlock(oldBlock.parent).children : this.blocks
    const oldPreSibling = this.getBlock(oldBlock.preSibling)
    const index = this.findIndex(siblings, oldBlock)
    siblings.splice(index, 0, newBlock)
    oldBlock.preSibling = newBlock.key
    newBlock.parent = oldBlock.parent
    newBlock.nextSibling = oldBlock.key
    newBlock.preSibling = null

    if (oldPreSibling) {
      oldPreSibling.nextSibling = newBlock.key
      newBlock.preSibling = oldPreSibling.key
    }
  }

  findOutMostBlock (block) {
    const parent = this.getBlock(block.parent)
    return parent ? this.findOutMostBlock(parent) : block
  }

  findIndex (children, block) {
    return children.findIndex(child => child === block)
  }

  prependChild (parent, block) {
    if (parent.children.length) {
      const firstChild = parent.children[0]
      this.insertBefore(block, firstChild)
    } else {
      this.appendChild(parent, block)
    }
  }

  appendChild (parent, block) {
    const len = parent.children.length
    const lastChild = parent.children[len - 1]
    parent.children.push(block)
    block.parent = parent.key
    if (lastChild) {
      lastChild.nextSibling = block.key
      block.preSibling = lastChild.key
    } else {
      block.preSibling = null
    }
    block.nextSibling = null
  }

  replaceBlock (newBlock, oldBlock) {
    const blockList = oldBlock.parent ? this.getParent(oldBlock).children : this.blocks
    const index = this.findIndex(blockList, oldBlock)

    blockList.splice(index, 1, newBlock)
    newBlock.parent = oldBlock.parent
    newBlock.preSibling = oldBlock.preSibling
    newBlock.nextSibling = oldBlock.nextSibling
  }

  isFirstChild (block) {
    return !block.preSibling
  }

  isLastChild (block) {
    return !block.nextSibling
  }

  isOnlyChild (block) {
    return !block.nextSibling && !block.preSibling
  }

  getLastChild (block) {
    const len = block.children.length
    if (len) {
      return block.children[len - 1]
    }
    return null
  }

  firstInDescendant (block) {
    const children = block.children
    if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
      return block
    } else if (children.length) {
      if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
        return this.firstInDescendant(children[1])
      } else {
        return this.firstInDescendant(children[0])
      }
    }
  }

  lastInDescendant (block) {
    if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
      return block
    } else if (block.children.length) {
      const children = block.children
      let lastChild = children[children.length - 1]
      while (lastChild.editable === false) {
        lastChild = this.getPreSibling(lastChild)
      }
      return this.lastInDescendant(lastChild)
    }
  }

  findPreBlockInLocation (block) {
    const parent = this.getParent(block)
    const preBlock = this.getPreSibling(block)
    if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
      return this.lastInDescendant(preBlock)
    } else if (parent) {
      return this.findPreBlockInLocation(parent)
    } else {
      return null
    }
  }

  findNextBlockInLocation (block) {
    const parent = this.getParent(block)
    const nextBlock = this.getNextSibling(block)

    if (nextBlock && nextBlock.editable !== false) {
      return this.firstInDescendant(nextBlock)
    } else if (parent) {
      return this.findNextBlockInLocation(parent)
    } else {
      return null
    }
  }

  getLastBlock () {
    const { blocks } = this
    const len = blocks.length
    return this.lastInDescendant(blocks[len - 1])
  }

  clear () {
    this.codeBlocks.clear()
  }
}

prototypes.forEach(ctrl => ctrl(ContentState))

export default ContentState


const startIndex = startKey ? blocks.findIndex(block => block.key === startKey) : 0

const endIndex = endKey ? blocks.findIndex(block => block.key === endKey) + 1 : blocks.length

const needRenderBlocks = blocks.slice(startIndex, endIndex)

keep keep keep replace keep keep keep keep

import { ipcMain } from 'electron'
import { getMenuItem } from '../utils'

const DISABLE_LABELS = [
  // paragraph menu items
  'Heading 1', 'Heading 2', 'Heading 3', 'Heading 4', 'Heading 5', 'Heading 6',
  'Upgrade Heading Level', 'Degrade Heading Level',
  'Table',
  // formats menu items
  'Hyperlink', 'Image'
]

const LABEL_MAP = {
  'Heading 1': 'h1',
  'Heading 2': 'h2',
  'Heading 3': 'h3',
  'Heading 4': 'h4',
  'Heading 5': 'h5',
  'Heading 6': 'h6',
  'Table': 'figure',
  'Code Fences': 'pre',
  'Quote Block': 'blockquote',
  'Order List': 'ol',
  'Bullet List': 'ul',
  'Task List': 'ul',
  'Paragraph': 'p',
  'Horizontal Line': 'hr',
  'YAML Front Matter': 'pre'
}

const setParagraphMenuItemStatus = bool => {
  const paragraphMenuItem = getMenuItem('Paragraph')
  paragraphMenuItem.submenu.items
    .forEach(item => (item.enabled = bool))
}

const disableNoMultiple = (disableLabels) => {
  const paragraphMenuItem = getMenuItem('Paragraph')

  paragraphMenuItem.submenu.items
    .filter(item => disableLabels.includes(item.label))
    .forEach(item => (item.enabled = false))
}

const setCheckedMenuItem = affiliation => {
  const paragraphMenuItem = getMenuItem('Paragraph')
  paragraphMenuItem.submenu.items.forEach(item => (item.checked = false))
  paragraphMenuItem.submenu.items.forEach(item => {
    if (item.label === 'Loose List Item') {
      let checked = false
      if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) {
        checked = affiliation[0].children[0].isLooseListItem
      } else if (affiliation.length >= 3 && affiliation[1].type === 'li') {
        checked = affiliation[1].isLooseListItem
      }
      item.checked = checked
    } else if (affiliation.some(b => {
      if (b.type === 'ul') {
        if (b.listType === 'bullet') {
          return item.label === 'Bullet List'
        } else {
          return item.label === 'Task List'
        }
      } else if (b.type === 'pre' && b.functionType) {
        if (b.functionType === 'frontmatter') {
          return item.label === 'YAML Front Matter'
        } else if (b.functionType === 'code') {
          return item.label === 'Code Fences'
        }
      } else {
        return b.type === LABEL_MAP[item.label]
      }
    })) {
      item.checked = true
    }
  })
}

export const paragraph = (win, type) => {
  win.webContents.send('AGANI::paragraph', { type })
}

ipcMain.on('AGANI::selection-change', (e, { start, end, affiliation }) => {
  // format menu
  const formatMenuItem = getMenuItem('Format')
  formatMenuItem.submenu.items.forEach(item => (item.enabled = true))
  // handle menu checked
  setCheckedMenuItem(affiliation)
  // handle disable
  setParagraphMenuItemStatus(true)
  if (
    (/th|td/.test(start.type) && /th|td/.test(end.type)) ||
    (start.type === 'span' && start.block.functionType === 'frontmatter') ||
    (end.type === 'span' && end.block.functionType === 'frontmatter')
  ) {
    setParagraphMenuItemStatus(false)
  } else if (start.key !== end.key) {
    formatMenuItem.submenu.items
      .filter(item => DISABLE_LABELS.includes(item.label))
      .forEach(item => (item.enabled = false))
    disableNoMultiple(DISABLE_LABELS)
  } else if (!affiliation.slice(0, 3).some(p => /ul|ol/.test(p.type))) {
    disableNoMultiple(['Loose List Item'])
  }
})


.icon {
  width: 1em; height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}


.ag-float-box {
  position: absolute;
  left: -1000px;
  top: -1000px;
  opacity: 0;
  max-height: 168px;
  min-width: 130px;
  max-width: 150px;
  margin: 0;
  padding: 5px 0;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
  list-style: none;
  transition: opacity .15s ease-in;
  overflow: auto;
  background: #fff;
  z-index: 10000;
}

.ag-float-box::-webkit-scrollbar:vertical {
  width: 5px;
}

.ag-show-float-box {
  opacity: 1;
}

.ag-float-item {
  padding: 0 .2em;
  height: 28px;
  line-height: 28px;
  box-sizing: border-box;
  color: #606266;
  cursor: pointer;
  font-size: 12px;
  display: flex;
  font-weight: 400;
}

.ag-float-item span:nth-of-type(2n) {
  overflow: hidden;
  text-overflow:ellipsis;
  white-space: nowrap;
}

.ag-float-box:hover .ag-float-item-active {
   background: #fff;
   color: #606266;
}
.ag-float-item-active, .ag-float-box .ag-float-item:hover {
  background-color: #ecf5ff;
  color: #66b1ff;
}

.ag-float-item-icon {
  font-size: 14px;
  width: 28px;
  height: 28px;
  text-align: center;
  display: inline-block;
  vertical-align: middle;
  flex-shrink: 0;
  margin-right: .1em;
}


result = [ text.substring(rStart, rEnd) ]

keep keep keep keep replace keep keep keep keep keep

// fix snabbdom-to-html bug https://github.com/snabbdom/snabbdom-to-html/issues/41

if (result.length === 1 && result[0] === '') result.length = 0


'use strict'

import fs from 'fs'
// import chokidar from 'chokidar'
import path from 'path'
import { BrowserWindow, dialog, ipcMain } from 'electron'
import createWindow, { forceClose, windows } from '../createWindow'
import { EXTENSION_HASN, EXTENSIONS } from '../config'
import { writeFile, writeMarkdownFile } from '../filesystem'
import { clearRecentlyUsedDocuments } from '../menu'
import { getPath, isMarkdownFile, log, isFile } from '../utils'
import userPreference from '../preference'

// TODO(fxha): Do we still need this?
const watchAndReload = (pathname, win) => { // when i build, and failed.
  // const watcher = chokidar.watch(pathname, {
  //   persistent: true
  // })
  // const filename = path.basename(pathname)
  // watcher.on('change', path => {
  //   fs.readFile(pathname, 'utf-8', (err, file) => {
  //     if (err) return console.log(err)
  //     win.webContents.send('AGANI::file-change', {
  //       file,
  //       filename,
  //       pathname
  //     })
  //   })
  // })
}

// handle the response from render process.
const handleResponseForExport = (e, { type, content, filename, pathname }) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  const extension = EXTENSION_HASN[type]
  const dirname = pathname ? path.dirname(pathname) : getPath('documents')
  const nakedFilename = pathname ? path.basename(pathname, '.md') : 'untitled'
  const defaultPath = `${dirname}/${nakedFilename}${extension}`
  const filePath = dialog.showSaveDialog(win, {
    defaultPath
  })

  if (!content && type === 'pdf') {
    win.webContents.printToPDF({ printBackground: true }, (err, data) => {
      if (err) log(err)
      writeFile(filePath, data, extension, e)
    })
  } else {
    writeFile(filePath, content, extension, e)
  }
}

const handleResponseForSave = (e, { markdown, pathname, options, quitAfterSave = false }) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  if (pathname) {
    writeMarkdownFile(pathname, markdown, '', options, win, e, quitAfterSave)
  } else {
    const filePath = dialog.showSaveDialog(win, {
      defaultPath: getPath('documents') + '/Untitled.md'
    })
    writeMarkdownFile(filePath, markdown, '.md', options, win, e, quitAfterSave)
  }
}

ipcMain.on('AGANI::response-file-save-as', (e, { markdown, pathname, options }) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  let filePath = dialog.showSaveDialog(win, {
    defaultPath: pathname || getPath('documents') + '/Untitled.md'
  })
  writeMarkdownFile(filePath, markdown, '.md', options, win, e)
})

ipcMain.on('AGANI::response-close-confirm', (e, { filename, pathname, markdown, options }) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  dialog.showMessageBox(win, {
    type: 'warning',
    buttons: ['Save', 'Cancel', 'Delete'],
    defaultId: 0,
    message: `Do you want to save the changes you made to ${filename}?`,
    detail: `Your changes will be lost if you don't save them.`,
    cancelId: 1,
    noLink: true
  }, index => {
    switch (index) {
      case 2:
        forceClose(win)
        break
      case 0:
        setTimeout(() => {
          handleResponseForSave(e, { pathname, markdown, options, quitAfterSave: true })
        })
        break
    }
  })
})

ipcMain.on('AGANI::response-file-save', handleResponseForSave)

ipcMain.on('AGANI::response-export', handleResponseForExport)

ipcMain.on('AGANI::close-window', e => {
  const win = BrowserWindow.fromWebContents(e.sender)
  forceClose(win)
})

ipcMain.on('AGANI::window::drop', (e, fileList) => {
  for (const file of fileList) {
    if (isMarkdownFile(file)) {
      createWindow(file)
      break
    }
  }
})

ipcMain.on('AGANI::rename', (e, {
  pathname,
  newPathname
}) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  if (!isFile(newPathname)) {
    fs.renameSync(pathname, newPathname)
    e.sender.send('AGANI::set-pathname', {
      pathname: newPathname,
      filename: path.basename(newPathname)
    })
  } else {
    dialog.showMessageBox(win, {
      type: 'warning',
      buttons: ['Replace', 'Cancel'],
      defaultId: 1,
      message: `The file ${path.basename(newPathname)} is already exists. Do you want to replace it?`,
      cancelId: 1,
      noLink: true
    }, index => {
      if (index === 0) {
        fs.renameSync(pathname, newPathname)
        e.sender.send('AGANI::set-pathname', {
          pathname: newPathname,
          filename: path.basename(newPathname)
        })
      }
    })
  }
})

ipcMain.on('AGANI::response-file-move-to', (e, { pathname }) => {
  const win = BrowserWindow.fromWebContents(e.sender)
  let newPath = dialog.showSaveDialog(win, {
    buttonLabel: 'Move to',
    nameFieldLabel: 'Filename:',
    defaultPath: pathname
  })
  if (newPath === undefined) return
  fs.renameSync(pathname, newPath)
  e.sender.send('AGANI::set-pathname', { pathname: newPath, filename: path.basename(newPath) })
})

export const exportFile = (win, type) => {
  win.webContents.send('AGANI::export', { type })
}

export const print = win => {
  win.webContents.print({ silent: false, printBackground: true, deviceName: '' })
}

export const openDocument = filePath => {
  if (isFile(filePath)) {
    const newWindow = createWindow(filePath)
    watchAndReload(filePath, newWindow)
  }
}

export const open = win => {
  const filename = dialog.showOpenDialog(win, {
    properties: ['openFile'],
    filters: [{
      name: 'text',
      extensions: EXTENSIONS
    }]
  })
  if (filename && filename[0]) {
    openDocument(filename[0])
  }
}

export const newFile = () => {
  createWindow()
}

export const save = win => {
  win.webContents.send('AGANI::ask-file-save')
}

export const saveAs = win => {
  win.webContents.send('AGANI::ask-file-save-as')
}

export const autoSave = (menuItem, browserWindow) => {
  const { checked } = menuItem
  userPreference.setItem('autoSave', checked)
    .then(() => {
      for (const win of windows.values()) {
        win.webContents.send('AGANI::user-preference', { autoSave: checked })
      }
    })
    .catch(log)
}

export const moveTo = win => {
  win.webContents.send('AGANI::ask-file-move-to')
}

export const rename = win => {
  win.webContents.send('AGANI::ask-file-rename')
}

export const clearRecentlyUsed = () => {
  clearRecentlyUsedDocuments()
}


import { app } from 'electron'
import { showAboutDialog } from '../actions/help'
import * as actions from '../actions/marktext'

export default {
  label: 'Mark Text',
  submenu: [{
    label: 'About Mark Text',
    click (menuItem, browserWindow) {
      showAboutDialog(browserWindow)
    }
  }, {
    label: 'Check for updates...',
    click (menuItem, browserWindow) {
      actions.checkUpdates(menuItem, browserWindow)
    }
  }, {
    label: 'Preferences',
    accelerator: 'Cmd+,',
    click (menuItem, browserWindow) {
      actions.userSetting(menuItem, browserWindow)
    }
  }, {
    type: 'separator'
  }, {
    label: 'Services',
    role: 'services',
    submenu: []
  }, {
    type: 'separator'
  }, {
    label: 'Hide Mark Text',
    accelerator: 'Command+H',
    role: 'hide'
  }, {
    label: 'Hide Others',
    accelerator: 'Command+Alt+H',
    role: 'hideothers'
  }, {
    label: 'Show All',
    role: 'unhide'
  }, {
    type: 'separator'
  }, {
    label: 'Quit',
    accelerator: 'Command+Q',
    click: app.quit
  }]
}


import { ipcMain } from 'electron'
import { getMenuItem } from '../utils'

const FORMAT_MAP = {
  'Strong': 'strong',
  'Emphasis': 'em',
  'Inline Code': 'inline_code',
  'Strike': 'del',
  'Hyperlink': 'link',
  'Image': 'image'
}

const selectFormat = formats => {
  const formatMenuItem = getMenuItem('Format')
  formatMenuItem.submenu.items.forEach(item => (item.checked = false))
  formatMenuItem.submenu.items
    .forEach(item => {
      if (formats.some(format => format.type === FORMAT_MAP[item.label])) {
        item.checked = true
      }
    })
}

export const format = (win, type) => {
  win.webContents.send('AGANI::format', { type })
}

ipcMain.on('AGANI::selection-formats', (e, formats) => {
  selectFormat(formats)
})


keep keep keep keep keep replace keep keep keep keep

import * as actions from '../actions/edit'
import userPreference from '../preference'

const { aidou } = userPreference.getAll()

export default {
  label: 'Edit',
  submenu: [{
    label: 'Undo',
    accelerator: 'CmdOrCtrl+Z',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'undo')
    }
  }, {
    label: 'Redo',
    accelerator: 'CmdOrCtrl+Y',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'redo')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Cut',
    accelerator: 'CmdOrCtrl+X',
    role: 'cut'
  }, {
    label: 'Copy',
    accelerator: 'CmdOrCtrl+C',
    role: 'copy'
  }, {
    label: 'Paste',
    accelerator: 'CmdOrCtrl+V',
    role: 'paste'
  }, {
    label: 'Select All',
    accelerator: 'CmdOrCtrl+A',
    role: 'selectall'
  }, {
    type: 'separator'
  }, {
    label: 'Find',
    accelerator: 'CmdOrCtrl+F',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'find')
    }
  }, {
    label: 'Find Next',
    accelerator: 'Alt+CmdOrCtrl+U',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'fineNext')
    }
  }, {
    label: 'FindPrev',
    accelerator: 'Shift+CmdOrCtrl+U',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'findPrev')
    }
  }, {
    label: 'Replace',
    accelerator: 'Alt+CmdOrCtrl+F',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'replace')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Aidou',
    visible: aidou,
    accelerator: 'CmdOrCtrl+/',
    click: (menuItem, browserWindow) => {
      actions.edit(browserWindow, 'aidou')
    }
  }, {
    label: 'Insert Image',
    submenu: [{
      label: 'Absolute Path',
      click (menuItem, browserWindow) {
        actions.insertImage(browserWindow, 'absolute')
      }
    }, {
      label: 'Relative Path',
      click (menuItem, browserWindow) {
        actions.insertImage(browserWindow, 'relative')
      }
    }, {
      label: 'Upload to Cloud (EXP)',
      click (menuItem, browserWindow) {
        actions.insertImage(browserWindow, 'upload')
      }
    }]
  }, {
    type: 'separator'
  }, {
    label: 'Line Ending',
    submenu: [{
      id: 'crlfLineEndingMenuEntry',
      label: 'Carriage return and line feed (CRLF)',
      type: 'radio',
      click (menuItem, browserWindow) {
        actions.lineEnding(browserWindow, 'crlf')
      }
    }, {
      id: 'lfLineEndingMenuEntry',
      label: 'Line feed (LF)',
      type: 'radio',
      click (menuItem, browserWindow) {
        actions.lineEnding(browserWindow, 'lf')
      }
    }]
  }]
}


import fs from 'fs'
import path from 'path'
import { app, Menu } from 'electron'
import configureMenu, { dockMenu } from './menus'
import { isFile, ensureDir, getPath, log } from './utils'

const MAX_RECENTLY_USED_DOCUMENTS = 12
const FILE_NAME = 'recently-used-documents.json'
const recentlyUsedDocumentsPath = path.join(getPath('userData'), FILE_NAME)
const isOsxOrWindows = /darwin|win32/.test(process.platform)
let initMacDock = false

export const addRecentlyUsedDocuments = filePath => {
  if (isOsxOrWindows) app.addRecentDocument(filePath)
  if (process.platform === 'darwin') return

  let recentDocuments = getRecentlyUsedDocuments()
  const index = recentDocuments.indexOf(filePath)
  let needSave = index !== 0
  if (index > 0) {
    recentDocuments.splice(index, 1)
  }
  if (index !== 0) {
    recentDocuments.unshift(filePath)
  }

  if (recentDocuments.length > MAX_RECENTLY_USED_DOCUMENTS) {
    needSave = true
    recentDocuments.splice(MAX_RECENTLY_USED_DOCUMENTS, recentDocuments.length - MAX_RECENTLY_USED_DOCUMENTS)
  }

  updateApplicationMenu(recentDocuments)

  if (needSave) {
    ensureDir(getPath('userData'))
    const json = JSON.stringify(recentDocuments, null, 2)
    fs.writeFileSync(recentlyUsedDocumentsPath, json, 'utf-8')
  }
}

export const clearRecentlyUsedDocuments = () => {
  if (isOsxOrWindows) app.clearRecentDocuments()
  if (process.platform === 'darwin') return

  const recentDocuments = []
  updateApplicationMenu(recentDocuments)
  const json = JSON.stringify(recentDocuments, null, 2)
  ensureDir(getPath('userData'))
  fs.writeFileSync(recentlyUsedDocumentsPath, json, 'utf-8')
}

export const getRecentlyUsedDocuments = () => {
  if (!isFile(recentlyUsedDocumentsPath)) {
    return []
  }

  try {
    let recentDocuments = JSON.parse(fs.readFileSync(recentlyUsedDocumentsPath, 'utf-8'))
      .filter(f => f && isFile(f))
    if (recentDocuments.length > MAX_RECENTLY_USED_DOCUMENTS) {
      recentDocuments.splice(MAX_RECENTLY_USED_DOCUMENTS, recentDocuments.length - MAX_RECENTLY_USED_DOCUMENTS)
    }
    return recentDocuments
  } catch (err) {
    log(err)
    return []
  }
}

export const updateApplicationMenu = (recentUsedDocuments) => {
  if (!recentUsedDocuments) {
    recentUsedDocuments = getRecentlyUsedDocuments()
  }

  // "we don't support changing menu object after calling setMenu, the behavior
  // is undefined if user does that." That means we have to recreate the
  // application menu each time.

  const menu = Menu.buildFromTemplate(configureMenu(recentUsedDocuments))
  Menu.setApplicationMenu(menu)
  if (!initMacDock && process.platform === 'darwin') {
    // app.dock is only for macosx
    app.dock.setMenu(dockMenu)
  }
  initMacDock = true
}

export const updateLineEndingnMenu = lineEnding => {
  const menus = Menu.getApplicationMenu()
  const crlfMenu = menus.getMenuItemById('crlfLineEndingMenuEntry')
  const lfMenu = menus.getMenuItemById('lfLineEndingMenuEntry')
  if (lineEnding === 'crlf') {
    crlfMenu.checked = true
  } else {
    lfMenu.checked = true
  }
}


import resource from './resource'

const state = {
  aiLoading: false,
  aiList: []
}

const mutations = {
  SET_AI_LIST (state, { data, type }) {
    if (type === 'search' || type === 'collect') {
      state.aiList = data
    } else {
      state.aiList = [...state.aiList, ...data]
    }
  },
  SET_AI_STATUS (state, bool) {
    state.aiLoading = bool
  }
}

const actions = {
  AI_LIST ({ commit }, data) {
    commit('SET_AI_LIST', data)
  },
  AI_SEARCH ({ commit }, { params, type }) {
    commit('SET_AI_STATUS', true)
    return resource.sogou(params)
      .then(({ data, total }) => {
        commit('SET_AI_LIST', { data, type })
        commit('SET_AI_STATUS', false)
        return { data, total }
      })
      .catch(err => {
        console.log(err)
        commit('SET_AI_STATUS', false)
      })
  }
}

export default { state, mutations, actions }


marktext

Next generation markdown editor

node .electron-vue/build.js && electron-builder --win --linux

node .electron-vue/build.js && electron-builder -mwl

node .electron-vue/build.js && electron-builder --linux

node .electron-vue/build.js && electron-builder --mac

node .electron-vue/build.js && electron-builder --dir

cross-env BUILD_TARGET=clean node .electron-vue/build.js

cross-env BUILD_TARGET=web node .electron-vue/build.js

eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test

eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test

npm run pack:main && npm run pack:renderer

cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js

cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js

Output: build


summary: "Cross browser API for Persistant Storage, PubSub and Request."

summary: "API for Persistant Storage, PubSub and Request"

Package.describe({
  summary: "Automatically preserve all form fields that have a unique id"
});

Package.on_use(function (api, where) {
  api.use(['underscore', 'spark']);
  api.add_files("preserve-inputs.js", "client");
});


/*!
jQuery Waypoints - v1.1.2
Copyright (c) 2011 Caleb Troughton
Dual licensed under the MIT license and GPL license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/MIT-license.txt
https://github.com/imakewebthings/jquery-waypoints/blob/master/GPL-license.txt
*/

/*
Waypoints is a small jQuery plugin that makes it easy to execute a function
whenever you scroll to an element.

GitHub Repository: https://github.com/imakewebthings/jquery-waypoints
Documentation and Examples: http://imakewebthings.github.com/jquery-waypoints

Changelog:
   v1.1.2
      - Fixed error thrown by waypoints with triggerOnce option that were
        triggered via resize refresh.
	v1.1.1
		- Fixed bug in initialization where all offsets were being calculated
		  as if set to 0 initially, causing unwarranted triggers during the
		  subsequent refresh.
		- Added onlyOnScroll, an option for individual waypoints that disables
		  triggers due to an offset refresh that crosses the current scroll
		  point. (All credit to @knuton on this one.)
	v1.1
		- Moved the continuous option out of global settings and into the options
		  object for individual waypoints.
		- Added the context option, which allows for using waypoints within any
		  scrollable element, not just the window.
	v1.0.2
		- Moved scroll and resize handler bindings out of load.  Should play nicer
		  with async loaders like Head JS and LABjs.
		- Fixed a 1px off error when using certain % offsets.
		- Added unit tests.
	v1.0.1
		- Added $.waypoints('viewportHeight').
		- Fixed iOS bug (using the new viewportHeight method).
		- Added offset function alias: 'bottom-in-view'.
	v1.0
		- Initial release.

Support:
	- jQuery versions 1.4.3+
	- IE6+, FF3+, Chrome 6+, Safari 4+, Opera 11
	- Other versions and browsers may work, these are just the ones I've looked at.
*/

(function($, wp, wps, window, undefined){
	'$:nomunge';

	var $w = $(window),

	// Keeping common strings as variables = better minification
	eventName = 'waypoint.reached',

	/*
	For the waypoint and direction passed in, trigger the waypoint.reached
	event and deal with the triggerOnce option.
	*/
	triggerWaypoint = function(way, dir) {
		way.element.trigger(eventName, dir);
		if (way.options.triggerOnce) {
			way.element[wp]('destroy');
		}
	},

	/*
	Given a jQuery element and Context, returns the index of that element in the waypoints
	array.  Returns the index, or -1 if the element is not a waypoint.
	*/
	waypointIndex = function(el, context) {
		var i = context.waypoints.length - 1;
		while (i >= 0 && context.waypoints[i].element[0] !== el[0]) {
			i -= 1;
		}
		return i;
	},

	// Private list of all elements used as scrolling contexts for waypoints.
	contexts = [],

	/*
	Context Class - represents a scrolling context.  Properties include:
		element: jQuery object containing a single HTML element.
		waypoints: Array of waypoints operating under this scroll context.
		oldScroll: Keeps the previous scroll position to determine scroll direction.
		didScroll: Flag used in scrolling the context's scroll event.
		didResize: Flag used in scrolling the context's resize event.
		doScroll: Function that checks for crossed waypoints. Called from throttler.
	*/
	Context = function(context) {
		$.extend(this, {
			'element': $(context),

			/*
			Starting at a ridiculous negative number allows for a 'down' trigger of 0 or
			negative offset waypoints on load. Useful for setting initial states.
			*/
			'oldScroll': -99999,

			/*
			List of all elements that have been registered as waypoints.
			Each object in the array contains:
				element: jQuery object containing a single HTML element.
				offset: The window scroll offset, in px, that triggers the waypoint event.
				options: Options object that was passed to the waypoint fn function.
			*/
			'waypoints': [],

			didScroll: false,
			didResize: false,

			doScroll: $.proxy(function() {
				var newScroll = this.element.scrollTop(),

				// Are we scrolling up or down? Used for direction argument in callback.
				isDown = newScroll > this.oldScroll,
				that = this,

				// Get a list of all waypoints that were crossed since last scroll move.
				pointsHit = $.grep(this.waypoints, function(el, i) {
					return isDown ?
						(el.offset > that.oldScroll && el.offset <= newScroll) :
						(el.offset <= that.oldScroll && el.offset > newScroll);
				}),
				len = pointsHit.length;

				// iOS adjustment
				if (!this.oldScroll || !newScroll) {
					$[wps]('refresh');
				}

				// Done with scroll comparisons, store new scroll before ejection
				this.oldScroll = newScroll;

				// No waypoints crossed? Eject.
				if (!len) return;

				// If several waypoints triggered, need to do so in reverse order going up
				if (!isDown) pointsHit.reverse();

				/*
				One scroll move may cross several waypoints.  If the waypoint's continuous
				option is true it should fire even if it isn't the last waypoint.  If false,
				it will only fire if it's the last one.
				*/
				$.each(pointsHit, function(i, point) {
					if (point.options.continuous || i === len - 1) {
						triggerWaypoint(point, [isDown ? 'down' : 'up']);
					}
				});
			}, this)
		});

		// Setup scroll and resize handlers.  Throttled at the settings-defined rate limits.
		$(context).scroll($.proxy(function() {
			if (!this.didScroll) {
				this.didScroll = true;
				window.setTimeout($.proxy(function() {
					this.doScroll();
					this.didScroll = false;
				}, this), $[wps].settings.scrollThrottle);
			}
		}, this)).resize($.proxy(function() {
			if (!this.didResize) {
				this.didResize = true;
				window.setTimeout($.proxy(function() {
					$[wps]('refresh');
					this.didResize = false;
				}, this), $[wps].settings.resizeThrottle);
			}
		}, this));

		$w.load($.proxy(function() {
			/*
			Fire a scroll check, should the page be loaded at a non-zero scroll value,
			as with a fragment id link or a page refresh.
			*/
			this.doScroll();
		}, this));
	},

	/* Returns a Context object from the contexts array, given the raw HTML element
	for that context. */
	getContextByElement = function(element) {
		var found = null;

		$.each(contexts, function(i, c) {
			if (c.element[0] === element) {
				found = c;
				return false;
			}
		});

		return found;
	},

	// Methods exposed to the effin' object
	methods = {
		/*
		jQuery.fn.waypoint([handler], [options])

		handler
			function, optional
			A callback function called when the user scrolls past the element.
			The function signature is function(event, direction) where event is
			a standard jQuery Event Object and direction is a string, either 'down'
			or 'up' indicating which direction the user is scrolling.

		options
			object, optional
			A map of options to apply to this set of waypoints, including where on
			the browser window the waypoint is triggered. For a full list of
			options and their defaults, see $.fn.waypoint.defaults.

		This is how you register an element as a waypoint. When the user scrolls past
		that element it triggers waypoint.reached, a custom event. Since the
		parameters for creating a waypoint are optional, we have a few different
		possible signatures. Let’s look at each of them.

		someElements.waypoint();

		Calling .waypoint with no parameters will register the elements as waypoints
		using the default options. The elements will fire the waypoint.reached event,
		but calling it in this way does not bind any handler to the event. You can
		bind to the event yourself, as with any other event, like so:

		someElements.bind('waypoint.reached', function(event, direction) {
		   // make it rain
		});

		You will usually want to create a waypoint and immediately bind a function to
		waypoint.reached, and can do so by passing a handler as the first argument to
		.waypoint:

		someElements.waypoint(function(event, direction) {
		   if (direction === 'down') {
		      // do this on the way down
		   }
		   else {
		      // do this on the way back up through the waypoint
		   }
		});

		This will still use the default options, which will trigger the waypoint when
		the top of the element hits the top of the window. We can pass .waypoint an
		options object to customize things:

		someElements.waypoint(function(event, direction) {
		   // do something amazing
		}, {
		   offset: '50%'  // middle of the page
		});

		You can also pass just an options object.

		someElements.waypoint({
		   offset: 100  // 100px from the top
		});

		This behaves like .waypoint(), in that it registers the elements as waypoints
		but binds no event handlers.

		Calling .waypoint on an existing waypoint will extend the previous options.
		If the call includes a handler, it will be bound to waypoint.reached without
		unbinding any other handlers.
		*/
		init: function(f, options) {
			// Register each element as a waypoint, add to array.
			this.each(function() {
				var cElement = $.fn[wp].defaults.context,
				context,
				$this = $(this);

				// Default window context or a specific element?
				if (options && options.context) {
					cElement = options.context;
				}

				// Find the closest element that matches the context
				if (!$.isWindow(cElement)) {
					cElement = $this.closest(cElement)[0];
				}
				context = getContextByElement(cElement);

				// Not a context yet? Create and push.
				if (!context) {
					context = new Context(cElement);
					contexts.push(context);
				}

				// Extend default and preexisting options
				var ndx = waypointIndex($this, context),
				base = ndx < 0 ? $.fn[wp].defaults : context.waypoints[ndx].options,
				opts = $.extend({}, base, options);

				// Offset aliases
				opts.offset = opts.offset === "bottom-in-view" ?
					function() {
						var cHeight = $.isWindow(cElement) ? $[wps]('viewportHeight')
							: $(cElement).height();
						return cHeight - $(this).outerHeight();
					} : opts.offset;

				// Update, or create new waypoint
				if (ndx < 0) {
					context.waypoints.push({
						'element': $this,
						'offset': null,
						'options': opts
					});
				}
				else {
					context.waypoints[ndx].options = opts;
				}

				// Bind the function if it was passed in.
				if (f) {
					$this.bind(eventName, f);
				}
			});

			// Need to re-sort+refresh the waypoints array after new elements are added.
			$[wps]('refresh');

			return this;
		},

		/*
		jQuery.fn.waypoint('remove')

		Passing the string 'remove' to .waypoint unregisters the elements as waypoints
		and wipes any custom options, but leaves the waypoint.reached events bound.
		Calling .waypoint again in the future would reregister the waypoint and the old
		handlers would continue to work.
		*/
		remove: function() {
			return this.each(function(i, el) {
				var $el = $(el);

				$.each(contexts, function(i, c) {
					var ndx = waypointIndex($el, c);

					if (ndx >= 0) {
						c.waypoints.splice(ndx, 1);
					}
				});
			});
		},

		/*
		jQuery.fn.waypoint('destroy')

		Passing the string 'destroy' to .waypoint will unbind all waypoint.reached
		event handlers on those elements and unregisters them as waypoints.
		*/
		destroy: function() {
			return this.unbind(eventName)[wp]('remove');
		}
	},

	/*
	Methods used by the jQuery object extension.
	*/
	jQMethods = {

		/*
		jQuery.waypoints('refresh')

		This will force a recalculation of each waypoint’s trigger point based on
		its offset option and context. This is called automatically whenever the window
		(or other defined context) is resized, new waypoints are added, or a waypoint’s
		options are modified. If your project is changing the DOM or page layout without
		doing one of these things, you may want to manually call this refresh.
		*/
		refresh: function() {
			$.each(contexts, function(i, c) {
				var isWin = $.isWindow(c.element[0]),
				contextOffset = isWin ? 0 : c.element.offset().top,
				contextHeight = isWin ? $[wps]('viewportHeight') : c.element.height(),
				contextScroll = isWin ? 0 : c.element.scrollTop();

				$.each(c.waypoints, function(j, o) {
				   /* $.each isn't safe from element removal due to triggerOnce.
				   Should rewrite the loop but this is way easier. */
				   if (!o) return;

					// Adjustment is just the offset if it's a px value
					var adjustment = o.options.offset,
					oldOffset = o.offset;

					// Set adjustment to the return value if offset is a function.
					if (typeof o.options.offset === "function") {
						adjustment = o.options.offset.apply(o.element);
					}
					// Calculate the adjustment if offset is a percentage.
					else if (typeof o.options.offset === "string") {
						var amount = parseFloat(o.options.offset);
						adjustment = o.options.offset.indexOf("%") ?
							Math.ceil(contextHeight * (amount / 100)) : amount;
					}

					/*
					Set the element offset to the window scroll offset, less
					all our adjustments.
					*/
					o.offset = o.element.offset().top - contextOffset
						+ contextScroll - adjustment;

					/*
					An element offset change across the current scroll point triggers
					the event, just as if we scrolled past it unless prevented by an
					optional flag.
					*/
					if (o.options.onlyOnScroll) return;

					if (oldOffset !== null && c.oldScroll > oldOffset && c.oldScroll <= o.offset) {
						triggerWaypoint(o, ['up']);
					}
					else if (oldOffset !== null && c.oldScroll < oldOffset && c.oldScroll >= o.offset) {
						triggerWaypoint(o, ['down']);
					}
				});

				// Keep waypoints sorted by offset value.
				c.waypoints.sort(function(a, b) {
					return a.offset - b.offset;
				});
			});
		},

		/*
		jQuery.waypoints('viewportHeight')

		This will return the height of the viewport, adjusting for inconsistencies
		that come with calling $(window).height() in iOS. Recommended for use
		within any offset functions.
		*/
		viewportHeight: function() {
			return (window.innerHeight ? window.innerHeight : $w.height());
		},

		/*
		jQuery.waypoints()

		This will return a jQuery object with a collection of all registered waypoint
		elements.

		$('.post').waypoint();
		$('.ad-unit').waypoint(function(event, direction) {
		   // Passed an ad unit
		});
		console.log($.waypoints());

		The example above would log a jQuery object containing all .post and .ad-unit
		elements.
		*/
		aggregate: function() {
			var points = $();
			$.each(contexts, function(i, c) {
				$.each(c.waypoints, function(i, e) {
					points = points.add(e.element);
				});
			});
			return points;
		}
	};

	/*
	fn extension.  Delegates to appropriate method.
	*/
	$.fn[wp] = function(method) {

		if (methods[method]) {
			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
		}
		else if (typeof method === "function" || !method) {
			return methods.init.apply(this, arguments);
		}
		else if (typeof method === "object") {
			return methods.init.apply(this, [null, method]);
		}
		else {
			$.error( 'Method ' +  method + ' does not exist on jQuery ' + wp );
		}
	};

	/*
	The default options object that is extended when calling .waypoint. It has the
	following properties:

	context
		string | element | jQuery*
		default: window
		The context defines which scrollable element the waypoint belongs to and acts
		within. The default, window, means the waypoint offset is calculated with relation
		to the whole viewport.  You can set this to another element to use the waypoints
		within that element.  Accepts a selector string, *but if you use jQuery 1.6+ it
		also accepts a raw HTML element or jQuery object.

	continuous
		boolean
		default: true
		If true, and multiple waypoints are triggered in one scroll, this waypoint will
		trigger even if it is not the last waypoint reached.  If false, it will only
		trigger if it is the last waypoint.

	offset
		number | string | function
		default: 0
		Determines how far the top of the element must be from the top of the browser
		window to trigger a waypoint. It can be a number, which is taken as a number
		of pixels, a string representing a percentage of the viewport height, or a
		function that will return a number of pixels.

	onlyOnScroll
		boolean
		default: false
		If true, this waypoint will not trigger if an offset change during a refresh
		causes it to pass the current scroll point.

	triggerOnce
		boolean
		default: false
		If true, the waypoint will be destroyed when triggered.

	An offset of 250 would trigger the waypoint when the top of the element is 250px
	from the top of the viewport. Negative values for any offset work as you might
	expect. A value of -100 would trigger the waypoint when the element is 100px above
	the top of the window.

	offset: '100%'

	A string percentage will determine the pixel offset based on the height of the
	window. When resizing the window, this offset will automatically be recalculated
	without needing to call $.waypoints('refresh').

	// The bottom of the element is in view
	offset: function() {
	   return $.waypoints('viewportHeight') - $(this).outerHeight();
	}

	Offset can take a function, which must return a number of pixels from the top of
	the window. The this value will always refer to the raw HTML element of the
	waypoint. As with % values, functions are recalculated automatically when the
	window resizes. For more on recalculating offsets, see $.waypoints('refresh').

	An offset value of 'bottom-in-view' will act as an alias for the function in the
	example above, as this is a common usage.

	offset: 'bottom-in-view'

	You can see this alias in use on the Scroll Analytics example page.

	The triggerOnce flag, if true, will destroy the waypoint after the first trigger.
	This is just a shortcut for calling .waypoint('destroy') within the waypoint
	handler. This is useful in situations such as scroll analytics, where you only
	want to record an event once for each page visit.

	The context option lets you use Waypoints within an element other than the window.
	You can define the context with a selector string and the waypoint will act within
	the nearest ancestor that matches this selector.

	$('.something-scrollable .waypoint').waypoint({
	   context: '.something-scrollable'
	});

	You can see this in action on the Dial Controls example.
	*/
	$.fn[wp].defaults = {
		continuous: true,
		offset: 0,
		triggerOnce: false,
		context: window
	};

	/*
	jQuery object extension. Delegates to appropriate methods above.
	*/
	$[wps] = function(method) {
		if (jQMethods[method]) {
			return jQMethods[method].apply(this);
		}
		else {
			return jQMethods['aggregate']();
		}
	};

	/*
	$.waypoints.settings

	Settings object that determines some of the plugin’s behavior.

	resizeThrottle
		number
		default: 200
		For performance reasons, the refresh performed during resizes is
		throttled. This value is the rate-limit in milliseconds between resize
		refreshes. For more information on throttling, check out Ben Alman’s
		throttle / debounce plugin.
		http://benalman.com/projects/jquery-throttle-debounce-plugin/

	scrollThrottle
		number
		default: 100
		For performance reasons, checking for any crossed waypoints during a
		scroll event is throttled. This value is the rate-limit in milliseconds
		between scroll checks. For more information on throttling, check out Ben
		Alman’s throttle / debounce plugin.
		http://benalman.com/projects/jquery-throttle-debounce-plugin/
	*/
	$[wps].settings = {
		resizeThrottle: 200,
		scrollThrottle: 100
	};

	$w.load(function() {
		// Calculate everything once on load.
		$[wps]('refresh');
	});
})(jQuery, 'waypoint', 'waypoints', this);


Tinytest.add("universal-events - basic", function(test) {

  var runTest = function (testMissingHandlers) {
    var msgs = [];
    var listeners = [];

    var createListener = function () {
      var out = [];
      msgs.push(out);
      var ret = new UniversalEventListener(function(event) {
        var node = event.currentTarget;
        if (DomUtils.elementContains(document.body, node)) {
          out.push(event.currentTarget.nodeName.toLowerCase());
        }
      }, testMissingHandlers);
      listeners.push(ret);
      return ret;
    };

    var L1 = createListener();

    var check = function (event, expected) {
      _.each(msgs, function (m) {
        m.length = 0;
      });
      simulateEvent(DomUtils.find(d.node(), "b"), event);
      for (var i = 0; i < listeners.length; i++)
        test.equal(msgs[i], testMissingHandlers ? [] : expected[i]);
    };

    var d = OnscreenDiv(Meteor.render("
Hello
"));
    L1.addType('mousedown');
    if (!testMissingHandlers)
      L1.installHandler(d.node(), 'mousedown');
    var x = ['b', 'span', 'div', 'div'];
    check('mousedown', [x]);

    check('mouseup', [[]]);

    L1.removeType('mousedown');
    check('mousedown', [[]]);
    L1.removeType('mousedown');
    check('mousedown', [[]]);

    L1.addType('mousedown');
    check('mousedown', [x]);
    L1.addType('mousedown');
    check('mousedown', [x]);
    L1.removeType('mousedown');
    check('mousedown', [[]]);

    var L2 = createListener();
    if (!testMissingHandlers)
      L2.installHandler(d.node(), 'mousedown');

    L1.addType('mousedown');
    check('mousedown', [x, []]);
    L2.addType('mousedown');
    check('mousedown', [x, x]);
    L2.addType('mousedown');
    check('mousedown', [x, x]);
    L1.removeType('mousedown');
    check('mousedown', [[], x]);
    L1.removeType('mousedown');
    check('mousedown', [[], x]);
    L2.removeType('mousedown');
    check('mousedown', [[], []]);
    L1.addType('mousedown');
    check('mousedown', [x, []]);
    L1.removeType('mousedown');
    check('mousedown', [[], []]);
    L2.addType('mousedown');
    check('mousedown', [[], x]);
    L2.removeType('mousedown');
    check('mousedown', [[], []]);

    d.kill();
  };

  runTest(false);
  runTest(true);
});


if (Meteor.isServer) {
  // XXX namespacing
  var Future = __meteor_bootstrap__.require('fibers/future');
}

// list of subscription tokens outstanding during a
// captureDependencies run. only set when we're doing a run. The fact
// that this is a singleton means we can't do recursive
// Meteor.subscriptions(). But what would that even mean?
// XXX namespacing
Meteor._capture_subs = null;

// @param url {String|Object} URL to Meteor app or sockjs endpoint (deprecated),
//     or an object as a test hook (see code)
// Options:
//   reloadOnUpdate: should we try to reload when the server says
//                      there's new code available?
//   reloadWithOutstanding: is it OK to reload if there are outstanding methods?
Meteor._LivedataConnection = function (url, options) {
  var self = this;
  options = _.extend({
    reloadOnUpdate: false,
    reloadWithOutstanding: false
  }, options);

  // as a test hook, allow passing a stream instead of a url.
  if (typeof url === "object") {
    self.stream = url;
    // if we have two test streams, auto reload stuff will break because
    // the url is used as a key for the migration data.
    self.url = "/debug";
  } else {
    self.url = url;
  }

  self.last_session_id = null;
  self.stores = {}; // name -> object with methods
  self.method_handlers = {}; // name -> func
  self.next_method_id = 1;

  // --- Three classes of outstanding methods ---

  // 1. either already sent, or waiting to be sent with no special
  // consideration once we reconnect
  self.outstanding_methods = []; // each item has keys: msg, callback

  // 2. the sole outstanding method that needs to be waited on, or null
  // same keys as outstanding_methods (notably wait is implicitly true
  // but not set)
  self.outstanding_wait_method = null; // same keys as outstanding_methods
  // stores response from `outstanding_wait_method` while we wait for
  // previous method calls to complete, as received in _livedata_result
  self.outstanding_wait_method_response = null;

  // 3. methods blocked on outstanding_wait_method being completed.
  self.blocked_methods = []; // each item has keys: msg, callback, wait

  // if set, called when we reconnect, queuing method calls _before_
  // the existing outstanding ones
  self.onReconnect = null;
  // waiting for data from method
  self.unsatisfied_methods = {}; // map from method_id -> true
  // sub was ready, is no longer (due to reconnect)
  self.unready_subscriptions = {}; // map from sub._id -> true
  // messages from the server that have not been applied
  self.pending_data = []; // array of pending data messages
  // name -> updates for (yet to be created) collection
  self.queued = {};
  // if we're blocking a migration, the retry func
  self._retryMigrate = null;

  // metadata for subscriptions
  self.subs = new LocalCollection;
  // keyed by subs._id. value is unset or an array. if set, sub is not
  // yet ready.
  self.sub_ready_callbacks = {};

  // Per-connection scratch area. This is only used internally, but we
  // should have real and documented API for this sort of thing someday.
  self.sessionData = {};

  // just for testing
  self.quiesce_callbacks = [];

  // Block auto-reload while we're waiting for method responses.
  if (!options.reloadWithOutstanding) {
    Meteor._reload.onMigrate(function (retry) {
      if (!self._readyToMigrate()) {
        if (self._retryMigrate)
          throw new Error("Two migrations in progress?");
        self._retryMigrate = retry;
        return false;
      } else {
        return [true];
      }
    });
  }

  // Setup stream (if not overriden above)
  self.stream = self.stream || new Meteor._Stream(self.url);

  self.stream.on('message', function (raw_msg) {
    try {
      var msg = JSON.parse(raw_msg);
    } catch (err) {
      Meteor._debug("discarding message with invalid JSON", raw_msg);
      return;
    }
    if (typeof msg !== 'object' || !msg.msg) {
      Meteor._debug("discarding invalid livedata message", msg);
      return;
    }

    if (msg.msg === 'connected')
      self._livedata_connected(msg);
    else if (msg.msg === 'data')
      self._livedata_data(msg);
    else if (msg.msg === 'nosub')
      self._livedata_nosub(msg);
    else if (msg.msg === 'result')
      self._livedata_result(msg);
    else if (msg.msg === 'error')
      self._livedata_error(msg);
    else
      Meteor._debug("discarding unknown livedata message type", msg);
  });

  self.stream.on('reset', function () {
    // Send a connect message at the beginning of the stream.
    // NOTE: reset is called even on the first connection, so this is
    // the only place we send this message.
    var msg = {msg: 'connect'};
    if (self.last_session_id)
      msg.session = self.last_session_id;
    self.stream.send(JSON.stringify(msg));

    // Now, to minimize setup latency, go ahead and blast out all of
    // our pending methods ands subscriptions before we've even taken
    // the necessary RTT to know if we successfully reconnected. (1)
    // They're supposed to be idempotent; (2) even if we did
    // reconnect, we're not sure what messages might have gotten lost
    // (in either direction) since we were disconnected (TCP being
    // sloppy about that.)

    // XXX we may have an issue where we lose 'data' messages sent
    // immediately before disconnection.. do we need to add app-level
    // acking of data messages?

    // If an `onReconnect` handler is set, call it first. Go through
    // some hoops to ensure that methods that are called from within
    // `onReconnect` get executed _before_ ones that were originally
    // outstanding (since `onReconnect` is used to re-establish auth
    // certificates)
    if (self.onReconnect)
      self._callOnReconnectAndSendAppropriateOutstandingMethods();
    else
      self._sendOutstandingMethods();

    // add new subscriptions at the end. this way they take effect after
    // the handlers and we don't see flicker.
    self.subs.find().forEach(function (sub) {
      self.stream.send(JSON.stringify(
        {msg: 'sub', id: sub._id, name: sub.name, params: sub.args}));
    });
  });

  if (options.reloadOnUpdate) {
    self.stream.on('update_available', function () {
      // Start trying to migrate to a new version. Until all packages
      // signal that they're ready for a migration, the app will
      // continue running normally.
      Meteor._reload.reload();
    });
  }

  // we never terminate the observe(), since there is no way to
  // destroy a LivedataConnection.. but this shouldn't matter, since we're
  // the only one that holds a reference to the self.subs collection
  self.subs_token = self.subs.find({}).observe({
    added: function (sub) {
      self.stream.send(JSON.stringify({
        msg: 'sub', id: sub._id, name: sub.name, params: sub.args}));
    },
    changed: function (sub) {
      if (sub.count <= 0) {
        // minimongo not re-entrant.
        _.defer(function () { self.subs.remove({_id: sub._id}); });
      }
    },
    removed: function (obj) {
      self.stream.send(JSON.stringify({msg: 'unsub', id: obj._id}));
    }
  });
};

_.extend(Meteor._LivedataConnection.prototype, {
  // 'name' is the name of the data on the wire that should go in the
  // store. 'store' should be an object with methods beginUpdate,
  // update, endUpdate, reset. see Collection for an example.
  registerStore: function (name, store) {
    var self = this;

    if (name in self.stores)
      return false;
    self.stores[name] = store;

    var queued = self.queued[name] || [];
    store.beginUpdate(queued.length);
    _.each(queued, function (msg) {
      store.update(msg);
    });
    store.endUpdate();
    delete self.queued[name];

    return true;
  },

  subscribe: function (name /* .. [arguments] .. callback */) {
    var self = this;
    var id;

    var args = Array.prototype.slice.call(arguments, 1);
    if (args.length && typeof args[args.length - 1] === "function")
      var callback = args.pop();

    // Look for existing subs (ignore those with count=0, since they're going to
    // get removed on the next time through the event loop).
    var existing = self.subs.find(
      {name: name, args: args, count: {$gt: 0}},
      {reactive: false}).fetch();

    if (existing && existing[0]) {
      // already subbed, inc count.
      id = existing[0]._id;
      self.subs.update({_id: id}, {$inc: {count: 1}});

      if (callback) {
        if (self.sub_ready_callbacks[id])
          self.sub_ready_callbacks[id].push(callback);
        else
          callback(); // XXX maybe _.defer?
      }
    } else {
      // new sub, add object.
      // generate our own id so we can know it w/ a find afterwards.
      id = LocalCollection.uuid();
      self.subs.insert({_id: id, name: name, args: args, count: 1});

      self.sub_ready_callbacks[id] = [];

      if (callback)
        self.sub_ready_callbacks[id].push(callback);
    }

    // return an object with a stop method.
    var token = {stop: function () {
      if (!id) return; // must have an id (local from above).
      // just update the database. observe takes care of the rest.
      self.subs.update({_id: id}, {$inc: {count: -1}});
    }};

    if (Meteor._capture_subs)
      Meteor._capture_subs.push(token);

    return token;
  },

  methods: function (methods) {
    var self = this;
    _.each(methods, function (func, name) {
      if (self.method_handlers[name])
        throw new Error("A method named '" + name + "' is already defined");
      self.method_handlers[name] = func;
    });
  },

  call: function (name /* .. [arguments] .. callback */) {
    // if it's a function, the last argument is the result callback,
    // not a parameter to the remote method.
    var args = Array.prototype.slice.call(arguments, 1);
    if (args.length && typeof args[args.length - 1] === "function")
      var callback = args.pop();
    return this.apply(name, args, callback);
  },

  // @param options {Optional Object}
  //   wait: Boolean - Should we block subsequent method calls on this
  //                   method's result having been received?
  //                   (does not affect methods called from within this method)
  // @param callback {Optional Function}
  apply: function (name, args, options, callback) {
    var self = this;

    // We were passed 3 arguments. They may be either (name, args, options)
    // or (name, args, callback)
    if (!callback && typeof options === 'function') {
      callback = options;
      options = {};
    }
    options = options || {};

    if (callback) {
      // XXX would it be better form to do the binding in stream.on,
      // or caller, instead of here?
      callback = Meteor.bindEnvironment(callback, function (e) {
        // XXX improve error message (and how we report it)
        Meteor._debug("Exception while delivering result of invoking '" +
                      name + "'", e.stack);
      });
    }

    if (Meteor.isClient) {
      // If on a client, run the stub, if we have one. The stub is
      // supposed to make some temporary writes to the database to
      // give the user a smooth experience until the actual result of
      // executing the method comes back from the server (whereupon
      // the temporary writes to the database will be reversed during
      // the beginUpdate/endUpdate process.)
      //
      // Normally, we ignore the return value of the stub (even if it
      // is an exception), in favor of the real return value from the
      // server. The exception is if the *caller* is a stub. In that
      // case, we're not going to do a RPC, so we use the return value
      // of the stub as our return value.
      var stub = self.method_handlers[name];
      if (stub) {
        var setUserId = function(userId) {
          self.setUserId(userId);
        };
        var invocation = new Meteor._MethodInvocation({
          isSimulation: true,
          userId: self.userId(), setUserId: setUserId,
          sessionData: self.sessionData
        });
        try {
          var ret = Meteor._CurrentInvocation.withValue(invocation,function () {
            return stub.apply(invocation, args);
          });
        }
        catch (e) {
          var exception = e;
        }
      }

      // If we're in a simulation, stop and return the result we have,
      // rather than going on to do an RPC. If there was no stub,
      // we'll end up returning undefined.
      var enclosing = Meteor._CurrentInvocation.get();
      var isSimulation = enclosing && enclosing.isSimulation;
      if (isSimulation) {
        if (callback) {
          callback(exception, ret);
          return;
        }
        if (exception)
          throw exception;
        return ret;
      }

      // If an exception occurred in a stub, and we're ignoring it
      // because we're doing an RPC and want to use what the server
      // returns instead, log it so the developer knows.
      //
      // Tests can set the 'expected' flag on an exception so it won't
      // go to log.
      if (exception && !exception.expected)
        Meteor._debug("Exception while simulating the effect of invoking '" +
                      name + "'", exception.stack);
    }

    // At this point we're definitely doing an RPC, and we're going to
    // return the value of the RPC to the caller.

    // If the caller didn't give a callback, decide what to do.
    if (!callback) {
      if (Meteor.isClient)
        // On the client, we don't have fibers, so we can't block. The
        // only thing we can do is to return undefined and discard the
        // result of the RPC.
        callback = function () {};
      else {
        // On the server, make the function synchronous.
        var future = new Future;
        callback = function (err, result) {
          future['return']([err, result]);
        };
      }
    }

    // Send the RPC. Note that on the client, it is important that the
    // stub have finished before we send the RPC (or at least we need
    // to guaranteed that the snapshot is not restored until the stub
    // has stopped doing writes.)
    var msg = {
      msg: 'method',
      method: name,
      params: args,
      id: '' + (self.next_method_id++)
    };

    if (self.outstanding_wait_method) {
      self.blocked_methods.push({
        msg: msg,
        callback: callback,
        wait: options.wait
      });
    } else {
      var method_object = {
        msg: msg,
        callback: callback
      };

      if (options.wait)
        self.outstanding_wait_method = method_object;
      else
        self.outstanding_methods.push(method_object);

      self.stream.send(JSON.stringify(msg));
    }

    // Even if we are waiting on other method calls mark this method
    // as unsatisfied so that the user never ends up seeing
    // intermediate versions of the server's datastream
    self.unsatisfied_methods[msg.id] = true;

    // If we're using the default callback on the server,
    // synchronously return the result from the remote host.
    if (future) {
      var outcome = future.wait();
      if (outcome[0])
        throw outcome[0];
      return outcome[1];
    }
  },

  status: function (/*passthrough args*/) {
    var self = this;
    return self.stream.status.apply(self.stream, arguments);
  },

  reconnect: function (/*passthrough args*/) {
    var self = this;
    return self.stream.reconnect.apply(self.stream, arguments);
  },

  ///
  /// Reactive user system
  /// XXX Can/should this be generalized pattern?
  ///
  userId: function () {
    var self = this;
    if (self._userIdListeners)
      self._userIdListeners.addCurrentContext();
    return self._userId;
  },

  setUserId: function (userId) {
    var self = this;
    self._userId = userId;
    if (self._userIdListeners)
      self._userIdListeners.invalidateAll();
  },

  _userId: null,
  _userIdListeners: Meteor.deps && new Meteor.deps._ContextSet,

  // PRIVATE: called when we are up-to-date with the server. intended
  // for use only in tests. currently, you are very limited in what
  // you may do inside your callback -- in particular, don't do
  // anything that could result in another call to onQuiesce, or
  // results are undefined.
  onQuiesce: function (f) {
    var self = this;

    f = Meteor.bindEnvironment(f, function (e) {
      Meteor._debug("Exception in quiesce callback", e.stack);
    });

    for (var method_id in self.unsatisfied_methods) {
      // we are not quiesced -- wait until we are
      self.quiesce_callbacks.push(f);
      return;
    }

    f();
  },

  _livedata_connected: function (msg) {
    var self = this;

    if (typeof (msg.session) === "string") {
      var reconnected = (self.last_session_id === msg.session);
      self.last_session_id = msg.session;
    }

    if (reconnected)
      // successful reconnection -- pick up where we left off.
      return;

    // Server doesn't have our data any more. Re-sync a new session.

    // Put a reset message into the pending data queue and discard any
    // previous messages (they are unimportant now).
    self.pending_data = ["reset"];
    self.queued = {};

    // Mark all currently ready subscriptions as 'unready'.
    var all_subs = self.subs.find({}).fetch();
    self.unready_subscriptions = {};
    _.each(all_subs, function (sub) {
      if (!self.sub_ready_callbacks[sub._id])
        self.unready_subscriptions[sub._id] = true;
    });

    // Do not clear the database here. That happens once all the subs
    // are re-ready and we process pending_data.
  },

  _livedata_data: function (msg) {
    var self = this;

    // Add the data message to the queue
    self.pending_data.push(msg);

    // Process satisfied methods and subscriptions.
    // NOTE: does not fire callbacks here, that happens when
    // the data message is processed for real. This is just for
    // quiescing.
    _.each(msg.methods || [], function (method_id) {
      delete self.unsatisfied_methods[method_id];
    });
    _.each(msg.subs || [], function (sub_id) {
      delete self.unready_subscriptions[sub_id];
    });

    // If there are still method invocations in flight, stop
    for (var method_id in self.unsatisfied_methods)
      return;
    // If there are still uncomplete subscriptions, stop
    for (var sub_id in self.unready_subscriptions)
      return;

    // We have quiesced. Blow away local changes and replace
    // with authoritative changes from server.

    var messagesPerStore = {};
    _.each(self.pending_data, function (msg) {
      if (msg.collection && msg.id && self.stores[msg.collection]) {
        if (_.has(messagesPerStore, msg.collection))
          ++messagesPerStore[msg.collection];
        else
          messagesPerStore[msg.collection] = 1;
      }
    });

    _.each(self.stores, function (s, name) {
      s.beginUpdate(_.has(messagesPerStore, name) ? messagesPerStore[name] : 0);
    });

    _.each(self.pending_data, function (msg) {
      // Reset message from reconnect. Blow away everything.
      //
      // XXX instead of reset message, we could have a flag, and pass
      // that to beginUpdate. This would be more efficient since we don't
      // have to restore a snapshot if we're just going to blow away the
      // db.
      if (msg === "reset") {
        _.each(self.stores, function (s) { s.reset(); });
        return;
      }

      if (msg.collection && msg.id) {
        var store = self.stores[msg.collection];

        if (!store) {
          // Nobody's listening for this data. Queue it up until
          // someone wants it.
          // XXX memory use will grow without bound if you forget to
          // create a collection.. going to have to do something about
          // that.
          if (!(msg.collection in self.queued))
            self.queued[msg.collection] = [];
          self.queued[msg.collection].push(msg);
          return;
        }

        store.update(msg);
      }

      if (msg.subs) {
        _.each(msg.subs, function (id) {
          var arr = self.sub_ready_callbacks[id];
          if (arr) _.each(arr, function (c) { c(); });
          delete self.sub_ready_callbacks[id];
        });
      }
    });

    _.each(self.stores, function (s) { s.endUpdate(); });

    _.each(self.quiesce_callbacks, function (cb) { cb(); });
    self.quiesce_callbacks = [];

    self.pending_data = [];
  },

  _livedata_nosub: function (msg) {
    var self = this;
    // Meteor._debug("NOSUB", msg);
  },

  _livedata_result: function (msg) {
    // id, result or error. error has error (code), reason, details

    var self = this;
    // find the outstanding request
    // should be O(1) in nearly all realistic use cases
    var m;
    if (self.outstanding_wait_method &&
        self.outstanding_wait_method.msg.id === msg.id) {
      m = self.outstanding_wait_method;
      self.outstanding_wait_method_response = msg;
    } else {
      for (var i = 0; i < self.outstanding_methods.length; i++) {
        m = self.outstanding_methods[i];
        if (m.msg.id === msg.id)
          break;
      }

      // remove
      self.outstanding_methods.splice(i, 1);
    }

    if (!m) {
      Meteor._debug("Can't match method response to original method call", msg);
      return;
    }

    if (self.outstanding_wait_method) {
      // Wait until we have completed all outstanding methods.
      if (self.outstanding_methods.length === 0 &&
         self.outstanding_wait_method_response) {

        // Start by saving the outstanding wait method details, since
        // we're going to reshift the blocked ones and try to send
        // them *before* calling the method callback. It is necessary
        // to call method callbacks last since they might themselves
        // call other methods
        var savedOutstandingWaitMethod = self.outstanding_wait_method;
        var savedOutstandingWaitMethodResponse = self.outstanding_wait_method_response;
        self.outstanding_wait_method_response = null;
        self.outstanding_wait_method = null;

        // Find first blocked method with wait: true
        var i;
        for (i = 0; i < self.blocked_methods.length; i++)
          if (self.blocked_methods[i].wait)
            break;

        // Move as many blocked methods as we can into
        // outstanding_methods and outstanding_wait_method if needed
        self.outstanding_methods = _.first(self.blocked_methods, i);
        if (i !== self.blocked_methods.length) {
          self.outstanding_wait_method = self.blocked_methods[i];
          self.blocked_methods = _.rest(self.blocked_methods, i+1);
        } else {
          self.blocked_methods = [];
        }

        // Send any new outstanding methods after we reshift the
        // blocked methods.  Intentionally do this before calling the
        // method response because they might call additional methods
        // that shouldn't be sent twice.
        self._sendOutstandingMethods();

        // Fire necessary outstanding method callbacks, making sure we
        // only fire the outstanding wait method after all other outstanding
        // methods' callbacks were fired
        if (m === savedOutstandingWaitMethod) {
          self._deliverMethodResponse(savedOutstandingWaitMethod,
                       savedOutstandingWaitMethodResponse /*(=== msg)*/);
        } else {
          self._deliverMethodResponse(m, msg);
          self._deliverMethodResponse(savedOutstandingWaitMethod,
                       savedOutstandingWaitMethodResponse /*(!== msg)*/);
        }
      } else {
        if (m !== self.outstanding_wait_method)
          self._deliverMethodResponse(m, msg);
      }
    } else {
      self._deliverMethodResponse(m, msg);
    }

    // if we were blocking a migration, see if it's now possible to
    // continue
    if (self._retryMigrate && self._readyToMigrate()) {
      self._retryMigrate();
      self._retryMigrate = null;
    }
  },

  // @param method {Object} as in `outstanding_methods`
  // @param response {Object{id, result | error}}
  _deliverMethodResponse: function(method, response) {
    // callback will have already been bindEnvironment'd by apply(),
    // so no need to catch exceptions
    if ('error' in response) {
      method.callback(new Meteor.Error(
        response.error.error, response.error.reason,
        response.error.details));
    } else {
      // msg.result may be undefined if the method didn't return a
      // value
      method.callback(undefined, response.result);
    }
  },

  _sendOutstandingMethods: function() {
    var self = this;
    _.each(self.outstanding_methods, function (m) {
      self.stream.send(JSON.stringify(m.msg));
    });
    if (self.outstanding_wait_method)
      self.stream.send(JSON.stringify(self.outstanding_wait_method.msg));
  },

  _livedata_error: function (msg) {
    Meteor._debug("Received error from server: ", msg.reason);
    if (msg.offending_message)
      Meteor._debug("For: ", msg.offending_message);
  },

  _callOnReconnectAndSendAppropriateOutstandingMethods: function() {
    var self = this;
    var old_outstanding_methods = self.outstanding_methods;
    var old_outstanding_wait_method = self.outstanding_wait_method;
    var old_blocked_methods = self.blocked_methods;
    self.outstanding_methods = [];
    self.outstanding_wait_method = null;
    self.blocked_methods = [];

    self.onReconnect();

    if (self.outstanding_wait_method) {
      // self.onReconnect() caused us to wait on a method. Add all old
      // methods to blocked_methods, and we don't need to send any
      // additional methods
      self.blocked_methods = self.blocked_methods.concat(
        old_outstanding_methods);

      if (old_outstanding_wait_method) {
        self.blocked_methods.push(_.extend(
          old_outstanding_wait_method, {wait: true}));
      }

      self.blocked_methods = self.blocked_methods.concat(
        old_blocked_methods);
    } else {
      // self.onReconnect() did not cause us to wait on a method. Add
      // as many methods as we can to outstanding_methods and send
      // them
      _.each(old_outstanding_methods, function(method) {
        self.outstanding_methods.push(method);
        self.stream.send(JSON.stringify(method.msg));
      });

      self.outstanding_wait_method = old_outstanding_wait_method;
      if (self.outstanding_wait_method)
        self.stream.send(JSON.stringify(self.outstanding_wait_method.msg));

      self.blocked_methods = old_blocked_methods;
    }
  },

  _readyToMigrate: function() {
    var self = this;
    return self.outstanding_methods.length === 0 &&
      !self.outstanding_wait_method &&
      self.blocked_methods.length === 0;
  }
});

_.extend(Meteor, {
  // @param url {String} URL to Meteor app, or to sockjs endpoint (deprecated),
  //     e.g.:
  //     "subdomain.meteor.com",
  //     "http://subdomain.meteor.com",
  //     "/",
  //     "http://subdomain.meteor.com/sockjs" (deprecated),
  //     "/sockjs" (deprecated)
  connect: function (url, _reloadOnUpdate) {
    var ret = new Meteor._LivedataConnection(
      url, {reloadOnUpdate: _reloadOnUpdate});
    Meteor._LivedataConnection._allConnections.push(ret); // hack. see below.
    return ret;
  },

  autosubscribe: function (sub_func) {
    var local_subs = [];
    var context = new Meteor.deps.Context();

    context.onInvalidate(function () {
      // recurse.
      Meteor.autosubscribe(sub_func);
      // unsub after re-subbing, to avoid bouncing.
      _.each(local_subs, function (x) { x.stop(); });
    });

    context.run(function () {
      if (Meteor._capture_subs)
        throw new Error("Meteor.autosubscribe may not be called recursively");

      Meteor._capture_subs = [];
      try {
        sub_func();
      } finally {
        local_subs = Meteor._capture_subs;
        Meteor._capture_subs = null;
      }
    });
  }
});

// Hack for `spiderable` package: a way to see if the page is done
// loading all the data it needs.
Meteor._LivedataConnection._allConnections = [];
Meteor._LivedataConnection._allSubscriptionsReady = function () {
  return _.all(Meteor._LivedataConnection._allConnections, function (conn) {
    for (var k in conn.sub_ready_callbacks)
      return false;
    return true;
  });
};


summary: "Automatically publish all data in the database to every client"

summary: "Automatically publish the entire database to all clients"

Package.describe({
  summary: "Require this application always use transport layer encryption"
});

Package.on_use(function (api) {
  api.use('underscore', 'server');
  // make sure we come after livedata, so we load after the sockjs
  // server has been instantiated.
  api.use('livedata', 'server');

  api.add_files('force_ssl_common.js', ['client', 'server']);
  api.add_files('force_ssl_server.js', 'server');

  // Another thing we could do is add a force_ssl_client.js file that
  // makes sure document.location.protocol is 'https'. If it detected
  // the code was loaded from a non-localhost non-https site, it would
  // stop the app from working and pop up an error box or something.
});


<template name="pkg_force_ssl">
{{#better_markdown}}
## `force-ssl`

This package causes Meteor to redirect insecure connections (HTTP) to a
secure URL (HTTPS). Use this package to ensure that communication to the
server is always encrypted to protect users from active spoofing
attacks.

To simplify development, unencrypted connections from `localhost` are
always accepted over HTTP.

Application bundles (`meteor bundle`) do not include an HTTPS server or
certificate. A proxy server that terminates SSL in front of a Meteor
bundle must set the standard `x-forwarded-proto` header for the
`force-ssl` package to work.

Applications deployed to `meteor.com` subdomains with
`meteor deploy` are automatically served via HTTPS using Meteor's
certificate.

{{/better_markdown}}
</template>


/// METEOR WRAPPER
//
// XXX this should get packaged and moved into the Meteor.crypto
// namespace, along with other hash functions.
if (typeof Meteor._srp === "undefined")
  Meteor._srp = {};
Meteor._srp.SHA256 = (function () {

/**
*
*  Secure Hash Algorithm (SHA256)
*  http://www.webtoolkit.info/javascript-sha256.html
*  http://anmar.eu.org/projects/jssha2/
*
*  Original code by Angel Marin, Paul Johnston.
*
**/

function SHA256(s){

	var chrsz   = 8;
	var hexcase = 0;

	function safe_add (x, y) {
		var lsw = (x & 0xFFFF) + (y & 0xFFFF);
		var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
		return (msw << 16) | (lsw & 0xFFFF);
	}

	function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
	function R (X, n) { return ( X >>> n ); }
	function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
	function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
	function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
	function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
	function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
	function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }

	function core_sha256 (m, l) {
		var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
		var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
		var W = new Array(64);
		var a, b, c, d, e, f, g, h, i, j;
		var T1, T2;

		m[l >> 5] |= 0x80 << (24 - l % 32);
		m[((l + 64 >> 9) << 4) + 15] = l;

		for ( var i = 0; i<m.length; i+=16 ) {
			a = HASH[0];
			b = HASH[1];
			c = HASH[2];
			d = HASH[3];
			e = HASH[4];
			f = HASH[5];
			g = HASH[6];
			h = HASH[7];

			for ( var j = 0; j<64; j++) {
				if (j < 16) W[j] = m[j + i];
				else W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);

				T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
				T2 = safe_add(Sigma0256(a), Maj(a, b, c));

				h = g;
				g = f;
				f = e;
				e = safe_add(d, T1);
				d = c;
				c = b;
				b = a;
				a = safe_add(T1, T2);
			}

			HASH[0] = safe_add(a, HASH[0]);
			HASH[1] = safe_add(b, HASH[1]);
			HASH[2] = safe_add(c, HASH[2]);
			HASH[3] = safe_add(d, HASH[3]);
			HASH[4] = safe_add(e, HASH[4]);
			HASH[5] = safe_add(f, HASH[5]);
			HASH[6] = safe_add(g, HASH[6]);
			HASH[7] = safe_add(h, HASH[7]);
		}
		return HASH;
	}

	function str2binb (str) {
		var bin = Array();
		var mask = (1 << chrsz) - 1;
		for(var i = 0; i < str.length * chrsz; i += chrsz) {
			bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
		}
		return bin;
	}

	function Utf8Encode(string) {
		// METEOR change:
		// The webtoolkit.info version of this code added this
		// Utf8Encode function (which does seem necessary for dealing
		// with arbitrary Unicode), but the following line seems
		// problematic:
		//
		// string = string.replace(/\r\n/g,"\n");
		var utftext = "";

		for (var n = 0; n < string.length; n++) {

			var c = string.charCodeAt(n);

			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}

		}

		return utftext;
	}

	function binb2hex (binarray) {
		var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
		var str = "";
		for(var i = 0; i < binarray.length * 4; i++) {
			str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
			hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8  )) & 0xF);
		}
		return str;
	}

	s = Utf8Encode(s);
	return binb2hex(core_sha256(str2binb(s), s.length * chrsz));

}

/// METEOR WRAPPER
return SHA256;
})();