<commit_before>const { URL } = require('url')
const fs = require('fs')
const path = require('path')
const http = require('http')
const https = require('https')
const Wappalyzer = require('./wappalyzer')

const {
  setTechnologies,
  setCategories,
  analyze,
  analyzeManyToMany,
  resolve,
} = Wappalyzer

const { AWS_LAMBDA_FUNCTION_NAME, CHROMIUM_BIN } = process.env

let puppeteer
let chromiumArgs = [
  '--no-sandbox',
  '--disable-gpu',
  '--ignore-certificate-errors',
  '--allow-running-insecure-content',
  '--disable-web-security',
  '--user-data-dir=/tmp/chromium',
]
let chromiumBin = CHROMIUM_BIN

if (AWS_LAMBDA_FUNCTION_NAME) {
  const chromium = require('chrome-aws-lambda')

  ;({ puppeteer } = chromium)

  chromiumArgs = chromiumArgs.concat(chromium.args)
  chromiumBin = chromium.executablePath
} else {
  puppeteer = require('puppeteer')
}

const extensions = /^([^.]+$|\.(asp|aspx|cgi|htm|html|jsp|php)$)/

const { apps: technologies, categories } = JSON.parse(
  fs.readFileSync(path.resolve(`${__dirname}/apps.json`))
)

setTechnologies(technologies)
setCategories(categories)

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function analyzeJs(js) {
  return Array.prototype.concat.apply(
    [],
    js.map(({ name, chain, value }) =>
      analyzeManyToMany(
        Wappalyzer.technologies.find(({ name: _name }) => name === _name),
        'js',
        { [chain]: [value] }
      )
    )
  )
}

function get(url) {
  if (['http:', 'https:'].includes(url.protocol)) {
    const { get } = url.protocol === 'http:' ? http : https

    return new Promise((resolve, reject) =>
      get(url.href, (response) => {
        if (response.statusCode >= 400) {
          return reject(
            new Error(`${response.statusCode} ${response.statusMessage}`)
          )
        }

        response.setEncoding('utf8')

        let body = ''

        response.on('data', (data) => (body += data))
        response.on('error', (error) => reject(new Error(error.message)))
        response.on('end', () => resolve(body))
      })
    )
  } else {
    throw new Error(`Invalid protocol: ${url.protocol}`)
  }
}

class Driver {
  constructor(options = {}) {
    this.options = {
      batchSize: 5,
      debug: false,
      delay: 500,
      htmlMaxCols: 2000,
      htmlMaxRows: 3000,
      maxDepth: 3,
      maxUrls: 10,
      maxWait: 30000,
      recursive: false,
      probe: false,
      ...options,
    }

    this.options.debug = Boolean(+this.options.debug)
    this.options.recursive = Boolean(+this.options.recursive)
    this.options.probe = Boolean(+this.options.probe)
    this.options.delay = parseInt(this.options.delay, 10)
    this.options.maxDepth = parseInt(this.options.maxDepth, 10)
    this.options.maxUrls = parseInt(this.options.maxUrls, 10)
    this.options.maxWait = parseInt(this.options.maxWait, 10)
    this.options.htmlMaxCols = parseInt(this.options.htmlMaxCols, 10)
    this.options.htmlMaxRows = parseInt(this.options.htmlMaxRows, 10)

    this.destroyed = false
  }

  async init() {
    this.log('Launching browser...')

    try {
      this.browser = await puppeteer.launch({
        args: chromiumArgs,
        executablePath: await chromiumBin,
      })

      this.browser.on('disconnected', async () => {
        this.log('Browser disconnected')

        if (!this.destroyed) {
          await this.init()
        }
      })
    } catch (error) {
      throw new Error(error.toString())
    }
  }

  async destroy() {
    this.destroyed = true

    if (this.browser) {
      try {
        await sleep(1)

        await this.browser.close()

        this.log('Browser closed')
      } catch (error) {
        throw new Error(error.toString())
      }
    }
  }

  open(url, headers = {}) {
    return new Site(url.split('#')[0], headers, this)
  }

  log(message, source = 'driver') {
    if (this.options.debug) {
      // eslint-disable-next-line no-console
      console.log(`wappalyzer | log | ${source} |`, message)
    }
  }
}

class Site {
  constructor(url, headers = {}, driver) {
    ;({ options: this.options, browser: this.browser } = driver)

    this.options.headers = {
      ...this.options.headers,
      ...headers,
    }

    this.driver = driver

    try {
      this.originalUrl = new URL(url)
    } catch (error) {
      throw new Error(error.toString())
    }

    this.analyzedUrls = {}
    this.detections = []

    this.listeners = {}

    this.pages = []
  }

  log(message, source = 'driver', type = 'log') {
    if (this.options.debug) {
      // eslint-disable-next-line no-console
      console[type](`wappalyzer | ${type} | ${source} |`, message)
    }

    this.emit(type, { message, source })
  }

  error(error, source = 'driver') {
    this.log(error, source, 'error')
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = []
    }

    this.listeners[event].push(callback)
  }

  emit(event, params) {
    if (this.listeners[event]) {
      return Promise.all(
        this.listeners[event].map((listener) => listener(params))
      )
    }
  }

  timeout() {
    return new Promise((resolve, reject) =>
      setTimeout(() => {
        reject(new Error('The website took too long to respond'))
      }, this.options.maxWait)
    )
  }

  async goto(url) {
    // Return when the URL is a duplicate or maxUrls has been reached
    if (
      this.analyzedUrls[url.href] ||
      Object.keys(this.analyzedUrls).length >= this.options.maxUrls
    ) {
      return
    }

    this.log(`Navigate to ${url}`, 'page')

    this.analyzedUrls[url.href] = {
      status: 0,
    }

    if (!this.browser) {
      throw new Error('Browser closed')
    }

    const page = await this.browser.newPage()

    this.pages.push(page)

    page.setDefaultTimeout(this.options.maxWait)

    await page.setRequestInterception(true)

    page.on('dialog', (dialog) => dialog.dismiss())

    page.on('error', (error) => this.error(error))

    let responseReceived = false

    page.on('request', async (request) => {
      try {
        if (
          (responseReceived && request.isNavigationRequest()) ||
          request.frame() !== page.mainFrame() ||
          !['document', 'script'].includes(request.resourceType())
        ) {
          request.abort('blockedbyclient')
        } else {
          const headers = {
            ...request.headers(),
            ...this.options.headers,
          }

          await this.emit('request', { page, request })

          request.continue({ headers })
        }
      } catch (error) {
        this.error(error)
      }
    })

    page.on('response', async (response) => {
      try {
        if (response.url() === url.href) {
          this.analyzedUrls[url.href] = {
            status: response.status(),
          }

          const rawHeaders = response.headers()
          const headers = {}

          Object.keys(rawHeaders).forEach((key) => {
            headers[key] = [
              ...(headers[key] || []),
              ...(Array.isArray(rawHeaders[key])
                ? rawHeaders[key]
                : [rawHeaders[key]]),
            ]
          })

          this.contentType = headers['content-type'] || null

          if (response.status() >= 300 && response.status() < 400) {
            if (headers.location) {
              url = new URL(headers.location.slice(-1), url)
            }
          } else {
            responseReceived = true

            this.onDetect(analyze({ headers }))

            await this.emit('response', { page, response })
          }
        }
      } catch (error) {
        this.error(error)
      }
    })

    await page.setUserAgent(
      this.options.userAgent ||
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
    )

    try {
      await Promise.race([
        this.timeout(),
        page.goto(url.href, { waitUntil: 'domcontentloaded' }),
      ])

      await sleep(1000)

      // Links
      const links = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.getElementsByTagName('a')).map(
              ({ hash, hostname, href, pathname, protocol, rel }) => ({
                hash,
                hostname,
                href,
                pathname,
                protocol,
                rel,
              })
            )
          )
        ).jsonValue(),
      ])

      // Script tags
      const scripts = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.getElementsByTagName('script'))
              .map(({ src }) => src)
              .filter((src) => src)
          )
        ).jsonValue(),
      ])

      // Meta tags
      const meta = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.querySelectorAll('meta')).reduce(
              (metas, meta) => {
                const key =
                  meta.getAttribute('name') || meta.getAttribute('property')

                if (key) {
                  metas[key.toLowerCase()] = [meta.getAttribute('content')]
                }

                return metas
              },
              {}
            )
          )
        ).jsonValue(),
      ])

      // JavaScript
      const js = await Promise.race([
        this.timeout(),
        page.evaluate(
          (technologies) => {
            return technologies.reduce((technologies, { name, chains }) => {
              chains.forEach((chain) => {
                chain = chain.replace(/\[([^\]]+)\]/g, '.$1')

                const value = chain
                  .split('.')
                  .reduce(
                    (value, method) => (value ? value[method] : undefined),
                    window
                  )

                if (typeof value !== 'undefined') {
                  technologies.push({
                    name,
                    chain,
                    value:
                      typeof value === 'string' || typeof value === 'number'
                        ? value
                        : !!value,
                  })
                }
              })

              return technologies
            }, [])
          },
          Wappalyzer.technologies
            .filter(({ js }) => Object.keys(js).length)
            .map(({ name, js }) => ({ name, chains: Object.keys(js) }))
        ),
      ])

      // Cookies
      const cookies = (await page.cookies()).reduce(
        (cookies, { name, value }) => ({
          ...cookies,
          [name]: [value],
        }),
        {}
      )

      // HTML
      let html = await page.content()

      if (this.options.htmlMaxCols && this.options.htmlMaxRows) {
        const batches = []
        const rows = html.length / this.options.htmlMaxCols

        for (let i = 0; i < rows; i += 1) {
          if (
            i < this.options.htmlMaxRows / 2 ||
            i > rows - this.options.htmlMaxRows / 2
          ) {
            batches.push(
              html.slice(
                i * this.options.htmlMaxCols,
                (i + 1) * this.options.htmlMaxCols
              )
            )
          }
        }

        html = batches.join('\n')
      }

      // Validate response
      if (url.protocol !== 'file:' && !this.analyzedUrls[url.href].status) {
        await page.close()

        this.log('Page closed')

        throw new Error('No response from server')
      }

      this.onDetect(analyzeJs(js))

      this.onDetect(
        analyze({
          url,
          cookies,
          html,
          scripts,
          meta,
        })
      )

      const reducedLinks = Array.prototype.reduce.call(
        links,
        (results, link) => {
          if (
            results &&
            Object.prototype.hasOwnProperty.call(
              Object.getPrototypeOf(results),
              'push'
            ) &&
            link.protocol &&
            link.protocol.match(/https?:/) &&
            link.rel !== 'nofollow' &&
            link.hostname === url.hostname &&
            extensions.test(link.pathname)
          ) {
            results.push(new URL(link.href.split('#')[0]))
          }

          return results
        },
        []
      )

      await this.emit('goto', {
        page,
        url,
        html,
        cookies,
        scripts,
        meta,
        js,
        links: reducedLinks,
      })

      await page.close()

      this.log('Page closed')

      return reducedLinks
    } catch (error) {
      this.error(error)
    }
  }

  async analyze(url = this.originalUrl, index = 1, depth = 1) {
    try {
      if (this.recursive) {
        await sleep(this.options.delay * index)
      }

      if (this.options.probe) {
        await this.probe(url)
      }

      const links = await this.goto(url)

      if (links && this.options.recursive && depth < this.options.maxDepth) {
        await this.batch(links.slice(0, this.options.maxUrls), depth + 1)
      }
    } catch (error) {
      this.analyzedUrls[url.href] = {
        status: 0,
        error: error.message || error.toString(),
      }

      this.error(error)
    }

    const results = {
      urls: this.analyzedUrls,
      technologies: resolve(this.detections).map(
        ({
          slug,
          name,
          confidence,
          version,
          icon,
          website,
          cpe,
          categories,
        }) => ({
          slug,
          name,
          confidence,
          version: version || null,
          icon,
          website,
          cpe,
          categories: categories.map(({ id, slug, name }) => ({
            id,
            slug,
            name,
          })),
        })
      ),
    }

    await this.emit('analyze', results)

    return results
  }

  async probe(url) {
    const files = {
      robots: '/robots.txt',
    }

    for (const file of Object.keys(files)) {
      const path = files[file]

      try {
        await sleep(this.options.delay)

        const body = await get(new URL(path, url.href))

        this.log(`get ${path}: ok`)

        this.onDetect(analyze({ [file]: body }))
      } catch (error) {
        this.error(`get ${path}: ${error.message || error}`)
      }
    }
  }

  async batch(links, depth, batch = 0) {
    if (links.length === 0) {
      return
    }

    const batched = links.splice(0, this.options.batchSize)

    await Promise.all(
      batched.map((link, index) => this.analyze(link, index, depth))
    )

    await this.batch(links, depth, batch + 1)
  }

  onDetect(detections = []) {
    this.detections = this.detections.concat(detections)

    this.detections.filter(
      ({ technology: { name }, pattern: { regex } }, index) =>
        this.detections.findIndex(
          ({ technology: { name: _name }, pattern: { regex: _regex } }) =>
            name === _name && (!regex || regex.toString() === _regex.toString())
        ) === index
    )
  }

  async destroy() {
    await Promise.all(
      this.pages.map(async (page) => {
        if (page) {
          try {
            await page.close()

            this.log('Page closed')
          } catch (error) {
            // Continue
          }
        }
      })
    )

    this.log('Site closed')
  }
}

module.exports = Driver
<commit_msg>Fix small bug<commit_after>const { URL } = require('url')
const fs = require('fs')
const path = require('path')
const http = require('http')
const https = require('https')
const Wappalyzer = require('./wappalyzer')

const {
  setTechnologies,
  setCategories,
  analyze,
  analyzeManyToMany,
  resolve,
} = Wappalyzer

const { AWS_LAMBDA_FUNCTION_NAME, CHROMIUM_BIN } = process.env

let puppeteer
let chromiumArgs = [
  '--no-sandbox',
  '--disable-gpu',
  '--ignore-certificate-errors',
  '--allow-running-insecure-content',
  '--disable-web-security',
  '--user-data-dir=/tmp/chromium',
]
let chromiumBin = CHROMIUM_BIN

if (AWS_LAMBDA_FUNCTION_NAME) {
  const chromium = require('chrome-aws-lambda')

  ;({ puppeteer } = chromium)

  chromiumArgs = chromiumArgs.concat(chromium.args)
  chromiumBin = chromium.executablePath
} else {
  puppeteer = require('puppeteer')
}

const extensions = /^([^.]+$|\.(asp|aspx|cgi|htm|html|jsp|php)$)/

const { apps: technologies, categories } = JSON.parse(
  fs.readFileSync(path.resolve(`${__dirname}/apps.json`))
)

setTechnologies(technologies)
setCategories(categories)

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

function analyzeJs(js) {
  return Array.prototype.concat.apply(
    [],
    js.map(({ name, chain, value }) =>
      analyzeManyToMany(
        Wappalyzer.technologies.find(({ name: _name }) => name === _name),
        'js',
        { [chain]: [value] }
      )
    )
  )
}

function get(url) {
  if (['http:', 'https:'].includes(url.protocol)) {
    const { get } = url.protocol === 'http:' ? http : https

    return new Promise((resolve, reject) =>
      get(url.href, (response) => {
        if (response.statusCode >= 400) {
          return reject(
            new Error(`${response.statusCode} ${response.statusMessage}`)
          )
        }

        response.setEncoding('utf8')

        let body = ''

        response.on('data', (data) => (body += data))
        response.on('error', (error) => reject(new Error(error.message)))
        response.on('end', () => resolve(body))
      })
    )
  } else {
    throw new Error(`Invalid protocol: ${url.protocol}`)
  }
}

class Driver {
  constructor(options = {}) {
    this.options = {
      batchSize: 5,
      debug: false,
      delay: 500,
      htmlMaxCols: 2000,
      htmlMaxRows: 3000,
      maxDepth: 3,
      maxUrls: 10,
      maxWait: 30000,
      recursive: false,
      probe: false,
      ...options,
    }

    this.options.debug = Boolean(+this.options.debug)
    this.options.recursive = Boolean(+this.options.recursive)
    this.options.probe = Boolean(+this.options.probe)
    this.options.delay = parseInt(this.options.delay, 10)
    this.options.maxDepth = parseInt(this.options.maxDepth, 10)
    this.options.maxUrls = parseInt(this.options.maxUrls, 10)
    this.options.maxWait = parseInt(this.options.maxWait, 10)
    this.options.htmlMaxCols = parseInt(this.options.htmlMaxCols, 10)
    this.options.htmlMaxRows = parseInt(this.options.htmlMaxRows, 10)

    this.destroyed = false
  }

  async init() {
    this.log('Launching browser...')

    try {
      this.browser = await puppeteer.launch({
        args: chromiumArgs,
        executablePath: await chromiumBin,
      })

      this.browser.on('disconnected', async () => {
        this.log('Browser disconnected')

        if (!this.destroyed) {
          await this.init()
        }
      })
    } catch (error) {
      throw new Error(error.toString())
    }
  }

  async destroy() {
    this.destroyed = true

    if (this.browser) {
      try {
        await sleep(1)

        await this.browser.close()

        this.log('Browser closed')
      } catch (error) {
        throw new Error(error.toString())
      }
    }
  }

  open(url, headers = {}) {
    return new Site(url.split('#')[0], headers, this)
  }

  log(message, source = 'driver') {
    if (this.options.debug) {
      // eslint-disable-next-line no-console
      console.log(`wappalyzer | log | ${source} |`, message)
    }
  }
}

class Site {
  constructor(url, headers = {}, driver) {
    ;({ options: this.options, browser: this.browser } = driver)

    this.options.headers = {
      ...this.options.headers,
      ...headers,
    }

    this.driver = driver

    try {
      this.originalUrl = new URL(url)
    } catch (error) {
      throw new Error(error.toString())
    }

    this.analyzedUrls = {}
    this.detections = []

    this.listeners = {}

    this.pages = []
  }

  log(message, source = 'driver', type = 'log') {
    if (this.options.debug) {
      // eslint-disable-next-line no-console
      console[type](`wappalyzer | ${type} | ${source} |`, message)
    }

    this.emit(type, { message, source })
  }

  error(error, source = 'driver') {
    this.log(error, source, 'error')
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = []
    }

    this.listeners[event].push(callback)
  }

  emit(event, params) {
    if (this.listeners[event]) {
      return Promise.all(
        this.listeners[event].map((listener) => listener(params))
      )
    }
  }

  timeout() {
    return new Promise((resolve, reject) =>
      setTimeout(() => {
        reject(new Error('The website took too long to respond'))
      }, this.options.maxWait)
    )
  }

  async goto(url) {
    // Return when the URL is a duplicate or maxUrls has been reached
    if (
      this.analyzedUrls[url.href] ||
      Object.keys(this.analyzedUrls).length >= this.options.maxUrls
    ) {
      return
    }

    this.log(`Navigate to ${url}`, 'page')

    this.analyzedUrls[url.href] = {
      status: 0,
    }

    if (!this.browser) {
      throw new Error('Browser closed')
    }

    const page = await this.browser.newPage()

    this.pages.push(page)

    page.setDefaultTimeout(this.options.maxWait)

    await page.setRequestInterception(true)

    page.on('dialog', (dialog) => dialog.dismiss())

    page.on('error', (error) => this.error(error))

    let responseReceived = false

    page.on('request', async (request) => {
      try {
        if (
          (responseReceived && request.isNavigationRequest()) ||
          request.frame() !== page.mainFrame() ||
          !['document', 'script'].includes(request.resourceType())
        ) {
          request.abort('blockedbyclient')
        } else {
          const headers = {
            ...request.headers(),
            ...this.options.headers,
          }

          await this.emit('request', { page, request })

          request.continue({ headers })
        }
      } catch (error) {
        this.error(error)
      }
    })

    page.on('response', async (response) => {
      try {
        if (response.url() === url.href) {
          this.analyzedUrls[url.href] = {
            status: response.status(),
          }

          const rawHeaders = response.headers()
          const headers = {}

          Object.keys(rawHeaders).forEach((key) => {
            headers[key] = [
              ...(headers[key] || []),
              ...(Array.isArray(rawHeaders[key])
                ? rawHeaders[key]
                : [rawHeaders[key]]),
            ]
          })

          this.contentType = headers['content-type'] || null

          if (response.status() >= 300 && response.status() < 400) {
            if (headers.location) {
              url = new URL(headers.location.slice(-1), url)
            }
          } else {
            responseReceived = true

            this.onDetect(analyze({ headers }))

            await this.emit('response', { page, response })
          }
        }
      } catch (error) {
        this.error(error)
      }
    })

    await page.setUserAgent(
      this.options.userAgent ||
        'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36'
    )

    try {
      await Promise.race([
        this.timeout(),
        page.goto(url.href, { waitUntil: 'domcontentloaded' }),
      ])

      await sleep(1000)

      // Links
      const links = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.getElementsByTagName('a')).map(
              ({ hash, hostname, href, pathname, protocol, rel }) => ({
                hash,
                hostname,
                href,
                pathname,
                protocol,
                rel,
              })
            )
          )
        ).jsonValue(),
      ])

      // Script tags
      const scripts = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.getElementsByTagName('script'))
              .map(({ src }) => src)
              .filter((src) => src)
          )
        ).jsonValue(),
      ])

      // Meta tags
      const meta = await Promise.race([
        this.timeout(),
        (
          await page.evaluateHandle(() =>
            Array.from(document.querySelectorAll('meta')).reduce(
              (metas, meta) => {
                const key =
                  meta.getAttribute('name') || meta.getAttribute('property')

                if (key) {
                  metas[key.toLowerCase()] = [meta.getAttribute('content')]
                }

                return metas
              },
              {}
            )
          )
        ).jsonValue(),
      ])

      // JavaScript
      const js = await Promise.race([
        this.timeout(),
        page.evaluate(
          (technologies) => {
            return technologies.reduce((technologies, { name, chains }) => {
              chains.forEach((chain) => {
                chain = chain.replace(/\[([^\]]+)\]/g, '.$1')

                const value = chain
                  .split('.')
                  .reduce(
                    (value, method) => (value ? value[method] : undefined),
                    window
                  )

                if (typeof value !== 'undefined') {
                  technologies.push({
                    name,
                    chain,
                    value:
                      typeof value === 'string' || typeof value === 'number'
                        ? value
                        : !!value,
                  })
                }
              })

              return technologies
            }, [])
          },
          Wappalyzer.technologies
            .filter(({ js }) => Object.keys(js).length)
            .map(({ name, js }) => ({ name, chains: Object.keys(js) }))
        ),
      ])

      // Cookies
      const cookies = (await page.cookies()).reduce(
        (cookies, { name, value }) => ({
          ...cookies,
          [name]: [value],
        }),
        {}
      )

      // HTML
      let html = await page.content()

      if (this.options.htmlMaxCols && this.options.htmlMaxRows) {
        const batches = []
        const rows = html.length / this.options.htmlMaxCols

        for (let i = 0; i < rows; i += 1) {
          if (
            i < this.options.htmlMaxRows / 2 ||
            i > rows - this.options.htmlMaxRows / 2
          ) {
            batches.push(
              html.slice(
                i * this.options.htmlMaxCols,
                (i + 1) * this.options.htmlMaxCols
              )
            )
          }
        }

        html = batches.join('\n')
      }

      // Validate response
      if (url.protocol !== 'file:' && !this.analyzedUrls[url.href].status) {
        await page.close()

        this.log('Page closed')

        throw new Error('No response from server')
      }

      this.onDetect(analyzeJs(js))

      this.onDetect(
        analyze({
          url,
          cookies,
          html,
          scripts,
          meta,
        })
      )

      const reducedLinks = Array.prototype.reduce.call(
        links,
        (results, link) => {
          if (
            results &&
            Object.prototype.hasOwnProperty.call(
              Object.getPrototypeOf(results),
              'push'
            ) &&
            link.protocol &&
            link.protocol.match(/https?:/) &&
            link.rel !== 'nofollow' &&
            link.hostname === url.hostname &&
            extensions.test(link.pathname)
          ) {
            results.push(new URL(link.href.split('#')[0]))
          }

          return results
        },
        []
      )

      await this.emit('goto', {
        page,
        url,
        html,
        cookies,
        scripts,
        meta,
        js,
        links: reducedLinks,
      })

      await page.close()

      this.log('Page closed')

      return reducedLinks
    } catch (error) {
      this.error(error)
    }
  }

  async analyze(url = this.originalUrl, index = 1, depth = 1) {
    try {
      if (this.options.recursive) {
        await sleep(this.options.delay * index)
      }

      if (this.options.probe) {
        await this.probe(url)
      }

      const links = await this.goto(url)

      if (links && this.options.recursive && depth < this.options.maxDepth) {
        await this.batch(links.slice(0, this.options.maxUrls), depth + 1)
      }
    } catch (error) {
      this.analyzedUrls[url.href] = {
        status: 0,
        error: error.message || error.toString(),
      }

      this.error(error)
    }

    const results = {
      urls: this.analyzedUrls,
      technologies: resolve(this.detections).map(
        ({
          slug,
          name,
          confidence,
          version,
          icon,
          website,
          cpe,
          categories,
        }) => ({
          slug,
          name,
          confidence,
          version: version || null,
          icon,
          website,
          cpe,
          categories: categories.map(({ id, slug, name }) => ({
            id,
            slug,
            name,
          })),
        })
      ),
    }

    await this.emit('analyze', results)

    return results
  }

  async probe(url) {
    const files = {
      robots: '/robots.txt',
    }

    for (const file of Object.keys(files)) {
      const path = files[file]

      try {
        await sleep(this.options.delay)

        const body = await get(new URL(path, url.href))

        this.log(`get ${path}: ok`)

        this.onDetect(analyze({ [file]: body }))
      } catch (error) {
        this.error(`get ${path}: ${error.message || error}`)
      }
    }
  }

  async batch(links, depth, batch = 0) {
    if (links.length === 0) {
      return
    }

    const batched = links.splice(0, this.options.batchSize)

    await Promise.all(
      batched.map((link, index) => this.analyze(link, index, depth))
    )

    await this.batch(links, depth, batch + 1)
  }

  onDetect(detections = []) {
    this.detections = this.detections.concat(detections)

    this.detections.filter(
      ({ technology: { name }, pattern: { regex } }, index) =>
        this.detections.findIndex(
          ({ technology: { name: _name }, pattern: { regex: _regex } }) =>
            name === _name && (!regex || regex.toString() === _regex.toString())
        ) === index
    )
  }

  async destroy() {
    await Promise.all(
      this.pages.map(async (page) => {
        if (page) {
          try {
            await page.close()

            this.log('Page closed')
          } catch (error) {
            // Continue
          }
        }
      })
    )

    this.log('Site closed')
  }
}

module.exports = Driver
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { Link } from 'react-router'
import LastHarvesting from '../LastHarvesting/LastHarvesting'
import Statistics from '../Statistics/Statistics'
import Percent from '../Statistics/Percent/Percent'
import './Catalog.css'

class Catalog extends Component {
  constructor(props) {
    super(props)
    this.state = {metrics: undefined}
    this.getMetrics()
  }

  getMetrics() {
    return fetch(`https://inspire.data.gouv.fr/api/geogw/catalogs/${this.props.catalog.id}/metrics`)
      .then((response) => response.json())
      .then((metrics) => {
        this.setState({metrics})
      })
      .catch((err) => {
        console.error(err)
      })
  }

  render() {
    if (this.state.metrics) {
      return (
          <Link to={`catalog/${this.props.catalog.id}`}>

              <LastHarvesting harvest={this.props.catalog.lastHarvesting}/>

                  {this.props.catalog.name}

<Percent metrics={this.state.metrics} label="openness" icon="users" size="small" />

<Percent metrics={this.state.metrics} label="download" icon="download" size="small" />

<Statistics value={this.props.catalog.lastHarvesting.recordsFound} size="small" label="Entries" />

          </Link>
      )
    } else {
      return (

)
    }

  }
}

export default Catalog
<commit_msg>Add loaders for Catalogs statistics<commit_after>import React, { Component } from 'react'
import { Link } from 'react-router'
import LastHarvesting from '../LastHarvesting/LastHarvesting'
import Statistics from '../Statistics/Statistics'
import Percent from '../Statistics/Percent/Percent'
import './Catalog.css'

class Catalog extends Component {
  constructor(props) {
    super(props)
    this.state = {metrics: undefined}
    this.getMetrics()
  }

  getMetrics() {
    return fetch(`https://inspire.data.gouv.fr/api/geogw/catalogs/${this.props.catalog.id}/metrics`)
      .then((response) => response.json())
      .then((metrics) => {
        this.setState({metrics})
      })
      .catch((err) => {
        console.error(err)
      })
  }

  render() {
      const loader =

      const openness = this.state.metrics ? <Percent metrics={this.state.metrics} label="openness" icon="users" size="small" /> : loader
      const download = this.state.metrics ? <Percent metrics={this.state.metrics} label="download" icon="users" size="small" /> : loader

      return (
          <Link to={`catalog/${this.props.catalog.id}`}>

              <LastHarvesting harvest={this.props.catalog.lastHarvesting}/>

                  {this.props.catalog.name}

{openness}

{download}

<Statistics value={this.props.catalog.lastHarvesting.recordsFound} size="small" label="Entries" />

          </Link>
      )

  }
}

export default Catalog
<|endoftext|>


<commit_before>import Ember from 'ember';
import App from '../../app';
import InfinityRoute from "ember-infinity/mixins/route";

export default Ember.Route.extend(InfinityRoute, {
  perPageParam: 'page[size]',
  pageParam: 'page[number]',
  totalPagesParam: 'meta.record-count',
  queryObject: {},
  storedParams: {},
  parsedQueryForServer: "",
  queryParams: {
    number: {
      refreshModel: true
    },
    year_compl: { refreshModel: true },
    tothu: { refreshModel: true },
    commsf: { refreshModel: true },
    name: { refreshModel: true },
    address: { refreshModel: true },
    municipality: { refreshModel: true },
    redevelopment: { refreshModel: true },
    status: { refreshModel: true },
    asofright: { refreshModel: true },
    age_restricted: { refreshModel: true },
    clusteros: { refreshModel: true },
    cancelled: { refreshModel: true },
    'private': { refreshModel: true },
    saved: { refreshModel: true },
    status: { refreshModel: true }
  },
  filters: ["year_compl","tothu","commsf","name","address","municipality","redevelopment",
                  "status", "asofright", "age_restricted", "clusteros",
                  "phased", "cancelled", "private","saved","status"],
  model(params) {
    this.set("storedParams", params);
    var queryObject = { filter: {} };
    var filters = this.filters;

    this.getSearchLimits();

    filters.forEach(function(property){
      if(params[property] !== null) {
        queryObject.filter[property] = params[property];
      }
    });

    // queryObject.page = {};
    // queryObject.startingPage = 1;
    queryObject.perPage = 1000;
    queryObject['fields[developments]'] = "name,geometry,latitude,longitude,commsf,tothu,year_compl"
    // queryObject.sort = '-start-time';
    this.set('queryObject', queryObject);

    return this.infinityModel("development", queryObject)
  },
  actions: {
    search() {
      this.refresh();
    },
    reset() {
      this.resetController();
    }
  },
  setupController(controller, model, transition) {
    this._super(controller, model);

    this.controllerFor("developments.search").computeRanges();
    if(transition.queryParams.year_compl !== undefined) {
      var year = JSON.parse(transition.queryParams.year_compl);
      this.controllerFor('developments.search').setProperties({ "yearFrom": year[0], "yearTo": year[1] });
    }

    if(transition.queryParams.commsf !== undefined) {
      var commsf = JSON.parse(transition.queryParams.commsf);
      this.controllerFor('developments.search').setProperties({ "sqftFrom": commsf[0], "sqftTo": commsf[1] });
    }

    if(transition.queryParams.tothu !== undefined) {
      var tothu = JSON.parse(transition.queryParams.tothu);
      this.controllerFor('developments.search').setProperties({ "tothuFrom": tothu[0], "tothuTo": tothu[1] });
    }

  },
  resetController: function () {
    var controller = this.controllerFor("developments.search");
    this.filters.forEach((param) => {
      controller.set(param, null);
    });
    controller.rangedProperties.forEach(function(property) {
      controller.set(property.min, null);
      controller.set(property.max, null);
    });
    controller.computeRanges();
  },
  getSearchLimits: function() {
    //http://api.lvh.me:5000/searches/limits
    var host = this.store.adapterFor('application').get('host'),
    endpoint = 'searches/limits';

    Ember.$.ajax({
        url: `${host}/${endpoint}`,
        // your other details...
    }).then((resolve) => {
        // self.set('name', resolve.doc.name);
        this.controllerFor('developments.search').set("limits", resolve);
        // process the result...
    });
  },
  getParsedQueryParamsURL: function() {
    var queryObject = this.get('queryObject');
    var stripped = this.get('filters').filter(function(property){
      return queryObject[property] !== "perPage";
    });
    return this.set('parsedQueryForServer', Ember.$.param(stripped));
  }.property('queryObject.{year_compl,tothu,commsf,name,address,municipality,redevelopment,status,asofright,age_restricted,clusteros,phased,cancelled,private,saved,status}'),
  _canLoadMore: Ember.computed('currentPage', function() {
    var currentPage = this.get('currentPage');
    var perPage = this.get('_perPage');
    var totalRecords = this.get('_totalPages');
    if ((currentPage * perPage) > totalRecords + perPage) {
      return false;
    } else {
      return true;
    }
  })
});
<commit_msg>updating field name<commit_after>import Ember from 'ember';
import App from '../../app';
import InfinityRoute from "ember-infinity/mixins/route";

export default Ember.Route.extend(InfinityRoute, {
  perPageParam: 'page[size]',
  pageParam: 'page[number]',
  totalPagesParam: 'meta.record-count',
  queryObject: {},
  storedParams: {},
  parsedQueryForServer: "",
  queryParams: {
    number: {
      refreshModel: true
    },
    year_compl: { refreshModel: true },
    tothu: { refreshModel: true },
    commsf: { refreshModel: true },
    name: { refreshModel: true },
    address: { refreshModel: true },
    municipality: { refreshModel: true },
    redevelopment: { refreshModel: true },
    status: { refreshModel: true },
    asofright: { refreshModel: true },
    age_restricted: { refreshModel: true },
    clusteros: { refreshModel: true },
    cancelled: { refreshModel: true },
    'private': { refreshModel: true },
    saved: { refreshModel: true },
    status: { refreshModel: true }
  },
  filters: ["year_compl","tothu","commsf","name","address","municipality","redevelopment",
                  "status", "asofright", "age_restricted", "clusteros",
                  "phased", "cancelled", "private","saved","status"],
  model(params) {
    this.set("storedParams", params);
    var queryObject = { filter: {} };
    var filters = this.filters;

    this.getSearchLimits();

    filters.forEach(function(property){
      if(params[property] !== null) {
        queryObject.filter[property] = params[property];
      }
    });

    // queryObject.page = {};
    // queryObject.startingPage = 1;
    queryObject.perPage = 1000;
    queryObject['fields[developments]'] = "name,geometry,latitude,longitude,commsf,tothu,year-compl"
    // queryObject.sort = '-start-time';
    this.set('queryObject', queryObject);

    return this.infinityModel("development", queryObject)
  },
  actions: {
    search() {
      this.refresh();
    },
    reset() {
      this.resetController();
    }
  },
  setupController(controller, model, transition) {
    this._super(controller, model);

    this.controllerFor("developments.search").computeRanges();
    if(transition.queryParams.year_compl !== undefined) {
      var year = JSON.parse(transition.queryParams.year_compl);
      this.controllerFor('developments.search').setProperties({ "yearFrom": year[0], "yearTo": year[1] });
    }

    if(transition.queryParams.commsf !== undefined) {
      var commsf = JSON.parse(transition.queryParams.commsf);
      this.controllerFor('developments.search').setProperties({ "sqftFrom": commsf[0], "sqftTo": commsf[1] });
    }

    if(transition.queryParams.tothu !== undefined) {
      var tothu = JSON.parse(transition.queryParams.tothu);
      this.controllerFor('developments.search').setProperties({ "tothuFrom": tothu[0], "tothuTo": tothu[1] });
    }

  },
  resetController: function () {
    var controller = this.controllerFor("developments.search");
    this.filters.forEach((param) => {
      controller.set(param, null);
    });
    controller.rangedProperties.forEach(function(property) {
      controller.set(property.min, null);
      controller.set(property.max, null);
    });
    controller.computeRanges();
  },
  getSearchLimits: function() {
    //http://api.lvh.me:5000/searches/limits
    var host = this.store.adapterFor('application').get('host'),
    endpoint = 'searches/limits';

    Ember.$.ajax({
        url: `${host}/${endpoint}`,
        // your other details...
    }).then((resolve) => {
        // self.set('name', resolve.doc.name);
        this.controllerFor('developments.search').set("limits", resolve);
        // process the result...
    });
  },
  getParsedQueryParamsURL: function() {
    var queryObject = this.get('queryObject');
    var stripped = this.get('filters').filter(function(property){
      return queryObject[property] !== "perPage";
    });
    return this.set('parsedQueryForServer', Ember.$.param(stripped));
  }.property('queryObject.{year_compl,tothu,commsf,name,address,municipality,redevelopment,status,asofright,age_restricted,clusteros,phased,cancelled,private,saved,status}'),
  _canLoadMore: Ember.computed('currentPage', function() {
    var currentPage = this.get('currentPage');
    var perPage = this.get('_perPage');
    var totalRecords = this.get('_totalPages');
    if ((currentPage * perPage) > totalRecords + perPage) {
      return false;
    } else {
      return true;
    }
  })
});
<|endoftext|>


<commit_before>const config = require('config'); // Explicit import is necessary here for command aliases

const Command = require('../Command');
const RoleDeterminer = require('../Helper/RoleDeterminer');
const generateRoomName = require('../Helper/SupportRoomNameGenerator');
const parseRoomType = require('../Helper/SupportRoomTypeParser');
const convertRoom = require('../Helper/SupportRoomConverter');
const archiveRoom = require('../Helper/SupportRoomArchiver');

const messages = {
    'help': 'You can use `!support <type> <IGN>` to create a room where you can contact the staff team for support in private, where `<IGN>` is your Minecraft username and `<type>` is one of the following:{types}',
    'type': '\n- `{type}`: {description}',
    'unknownType': 'Unknown support type `{type}`.\n\n{help}',
    'roomCreated': 'I\'ve created a support room of type `{type}` and added you to it!',
    'roomConverted': 'I\'ve converted this room\'s support type from `{oldType}` to `{newType}`.',
    'roomClosing': 'Closing this support room...',
    'roomClosed': '{user} closed support room `{room}`',
    'supportType': 'Support type',
    'roomParticipants': 'Participants',
    'noPermission': 'Sorry, it appears that you do not have access to perform this action.',
    'noIgnProvided': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'noIgnProvidedWithType': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} {type} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'notASupportRoom': 'You can only execute this command in a support room.',
    'roomCreationError': 'Sorry, it appears that I have encountered an error whilst trying to create a room. Check back later!',
    'supportWelcome': 'Hello {user} (IGN: {IGN})! I have created this room for you to ask a question relating to `{type}` to the staff team in private. Before asking your question, please read through the support rules below.',
    'supportRules': '**• Do not ping staff**\nMembers of our team (believe it or not) have lives of their own, and being disturbed whilst working or while in class can be very frustrating, so please refrain from pinging any staff without very good cause. If there\'s a forum thread you can use, it is not good cause to ping staff.\n\n**• Don\'t expect live support on this server**\nShotbow does not offer live support. Should you be in luck, and a member of staff is actively chatting at the time you have a matter to raise, you may of course ask them for help. Under no conditions should you ping staff members individually for help.\n\n**• Cheaters**\nIf someone is cheating use the /report command. This is the best way to get the attention of staff currently active in patrol duty. Hackusations are not allowed, and staff should not be pinged to deal with cheaters.\n\nIf you have a question or issue that doesn\'t require the attention of a staff member, it\'s probably better to ask politely in #shotbow without a ping and someone will gladly help you get to where you need to be.\n',
    'supportMessageReceived': 'Thank you for your message! If you have more information, you can continue sending messages in this channel. A staff member will get to you soon. ({roles})'
};

module.exports = Command.extend({
    commandName: 'support',
    commandAliases: Object.keys(config.support.types).map(typeKey => `${typeKey}support`),
    processMessage: async function (message, tokens) {
        const commandParameters = this.extractParameters(tokens);

        /* Handle the case of no or invalid arguments provided */
        if (!commandParameters) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            return message.channel.send(this.i18n.__mf(messages.help, {types}));
        }

        /* Handle the unique cases of "close" and "convert" */
        tokens.shift();
        if (tokens.length > 0
            && commandParameters.command === 'support'
            && (tokens[0].toLowerCase() === 'close' || tokens[0].toLowerCase() === 'convert')) {
            const typeKey = parseRoomType(message.channel.name);
            if (!typeKey) {
                return message.channel.send(this.i18n.__mf(messages.notASupportRoom));
            }

            const roleIds = this.config.support.types[typeKey].roles.map(role => role.id);
            if (!RoleDeterminer.hasOneOfRoles(message.member, roleIds)) {
                return message.channel.send(this.i18n.__mf(messages.noPermission));
            }

            if (tokens[0].toLowerCase() === 'close') {
                return await archiveRoom(message, this.i18n, this.discordClient);
            }
            if (tokens[0].toLowerCase() === 'convert') {
                return await convertRoom(message, tokens, this.i18n, this.discordClient);
            }
        }

        /* Get the support type */
        const typeKey = commandParameters.supportType;
        const type = this.config.support.types[typeKey];
        if (!type) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            const helpText = this.i18n.__mf(messages.help, {types});
            return message.channel.send(this.i18n.__mf(messages.unknownType, {type: typeKey, help: helpText}));
        }

        /* Handle the case of no username provided */
        if (!commandParameters.ign) {
            if (commandParameters.command === 'support') {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvidedWithType, {
                    command: commandParameters.command,
                    type: commandParameters.supportType
                }));
            } else {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvided, {
                    command: commandParameters.command
                }));
            }
        }

        /* Create the channel and add the correct people to it */
        const supportCategory = message.guild.channels.cache.get(this.config.support.category);
        const supportChannel = await message.guild.channels.create(generateRoomName(typeKey), {
            type: 'text',
            parent: supportCategory,
        })
            .then(channel => channel.lockPermissions())
            .then(channel => channel.updateOverwrite(message.author.id, {
                ADD_REACTIONS: true,
                ATTACH_FILES: true,
                EMBED_LINKS: true,
                READ_MESSAGE_HISTORY: true,
                SEND_MESSAGES: true,
                USE_EXTERNAL_EMOJIS: true,
                SEND_TTS_MESSAGES: false,
                VIEW_CHANNEL: true
            }))
            .then(async channel => {
                for (const supportRole of type.roles) {
                    await channel.updateOverwrite(supportRole.id, {
                        ADD_REACTIONS: true,
                        ATTACH_FILES: true,
                        EMBED_LINKS: true,
                        READ_MESSAGE_HISTORY: true,
                        SEND_MESSAGES: true,
                        USE_EXTERNAL_EMOJIS: true,
                        SEND_TTS_MESSAGES: true,
                        VIEW_CHANNEL: true
                    });
                }
                return channel;
            })
            .catch(() => {
                /* If something went wrong with room creation, we delete it (if it was created) and notify the user */
                if (supportChannel) {
                    supportChannel.delete().catch(() => {});
                }
                return message.channel.send(this.i18n.__mf(messages.roomCreationError));
            });

        const welcomeMessage = await supportChannel.send(this.i18n.__mf(messages.supportWelcome, {
            user: `<@${message.author.id}>`,
            IGN: commandParameters.ign,
            type: typeKey
        }));

        /* Add the collector to deal with post-creation events */
        this.addCollector(supportChannel, type, message.member, welcomeMessage);

        await supportChannel.send(this.i18n.__mf(messages.supportRules));
        return message.channel.send(this.i18n.__mf(messages.roomCreated, {type: typeKey}));
    },
    addCollector: function (supportChannel, supportType, creator, welcomeMessage) {
        const collectorFilter = (message) => message.member.id === creator.id;

        let collector;
        if (!supportType.autoClose || !supportType.autoClose.closing
            || supportType.autoClose.closing < 0) {
            collector = supportChannel.createMessageCollector(collectorFilter);
        } else {
            const automaticDelete = supportType.autoClose.closing;
            collector = supportChannel.createMessageCollector(collectorFilter,
                {time: automaticDelete});
            collector.on('end', async (_, reason) => {
                if (reason === 'time') {
                    await archiveRoom(welcomeMessage, this.i18n, this.discordClient);
                }
            });
        }

        collector.once('collect', (message) => {
            const supportType = parseRoomType(supportChannel.name);
            const supportRoles = this.config.support.types[supportType].roles
                .filter(supportRole => supportRole.ping)
                .map(supportRole => `<@&${supportRole.id}>`)
                .join(', ');

            message.channel.send(this.i18n.__mf(messages.supportMessageReceived, {roles: supportRoles}));
            collector.stop();
        });
    },
    extractParameters: function (tokens) {
        const clonedTokens = [...tokens];
        /* Extract the support type */
        const command = clonedTokens[0].substr(this.commandPrefix.length, clonedTokens[0].length);
        clonedTokens.shift();
        let supportType;
        if (command === 'support') {
            if (clonedTokens.length === 0) {
                return null;
            } else {
                supportType = clonedTokens[0];
                clonedTokens.shift();
            }
        } else {
            supportType = command.split('support')[0];
        }

        /* Extract the IGN */
        if (clonedTokens.length === 0) {
            return {
                command,
                supportType: supportType.toLowerCase(),
                ign: null
            }
        }
        const ign = clonedTokens[0];

        /* Return the extracted data in an object */
        return {
            command,
            supportType,
            ign
        }
    }
});
<commit_msg>feat(support): auto delete support commands<commit_after>const config = require('config'); // Explicit import is necessary here for command aliases

const Command = require('../Command');
const RoleDeterminer = require('../Helper/RoleDeterminer');
const generateRoomName = require('../Helper/SupportRoomNameGenerator');
const parseRoomType = require('../Helper/SupportRoomTypeParser');
const convertRoom = require('../Helper/SupportRoomConverter');
const archiveRoom = require('../Helper/SupportRoomArchiver');

const messages = {
    'help': 'You can use `!support <type> <IGN>` to create a room where you can contact the staff team for support in private, where `<IGN>` is your Minecraft username and `<type>` is one of the following:{types}',
    'type': '\n- `{type}`: {description}',
    'unknownType': 'Unknown support type `{type}`.\n\n{help}',
    'roomCreated': 'I\'ve created a support room of type `{type}` and added you to it!',
    'roomConverted': 'I\'ve converted this room\'s support type from `{oldType}` to `{newType}`.',
    'roomClosing': 'Closing this support room...',
    'roomClosed': '{user} closed support room `{room}`',
    'supportType': 'Support type',
    'roomParticipants': 'Participants',
    'noPermission': 'Sorry, it appears that you do not have access to perform this action.',
    'noIgnProvided': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'noIgnProvidedWithType': 'It looks like you forgot to add your Minecraft username to the support command. Usage: `!{command} {type} <IGN>`, where `<IGN>` refers to your Minecraft username.',
    'notASupportRoom': 'You can only execute this command in a support room.',
    'roomCreationError': 'Sorry, it appears that I have encountered an error whilst trying to create a room. Check back later!',
    'supportWelcome': 'Hello {user} (IGN: {IGN})! I have created this room for you to ask a question relating to `{type}` to the staff team in private. Before asking your question, please read through the support rules below.',
    'supportRules': '**• Do not ping staff**\nMembers of our team (believe it or not) have lives of their own, and being disturbed whilst working or while in class can be very frustrating, so please refrain from pinging any staff without very good cause. If there\'s a forum thread you can use, it is not good cause to ping staff.\n\n**• Don\'t expect live support on this server**\nShotbow does not offer live support. Should you be in luck, and a member of staff is actively chatting at the time you have a matter to raise, you may of course ask them for help. Under no conditions should you ping staff members individually for help.\n\n**• Cheaters**\nIf someone is cheating use the /report command. This is the best way to get the attention of staff currently active in patrol duty. Hackusations are not allowed, and staff should not be pinged to deal with cheaters.\n\nIf you have a question or issue that doesn\'t require the attention of a staff member, it\'s probably better to ask politely in #shotbow without a ping and someone will gladly help you get to where you need to be.\n',
    'supportMessageReceived': 'Thank you for your message! If you have more information, you can continue sending messages in this channel. A staff member will get to you soon. ({roles})'
};

module.exports = Command.extend({
    shouldDeleteMessage: true,
    commandName: 'support',
    commandAliases: Object.keys(config.support.types).map(typeKey => `${typeKey}support`),
    processMessage: async function (message, tokens) {
        const commandParameters = this.extractParameters(tokens);

        /* Handle the case of no or invalid arguments provided */
        if (!commandParameters) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            return message.channel.send(this.i18n.__mf(messages.help, {types}));
        }

        /* Handle the unique cases of "close" and "convert" */
        tokens.shift();
        if (tokens.length > 0
            && commandParameters.command === 'support'
            && (tokens[0].toLowerCase() === 'close' || tokens[0].toLowerCase() === 'convert')) {
            const typeKey = parseRoomType(message.channel.name);
            if (!typeKey) {
                return message.channel.send(this.i18n.__mf(messages.notASupportRoom));
            }

            const roleIds = this.config.support.types[typeKey].roles.map(role => role.id);
            if (!RoleDeterminer.hasOneOfRoles(message.member, roleIds)) {
                return message.channel.send(this.i18n.__mf(messages.noPermission));
            }

            if (tokens[0].toLowerCase() === 'close') {
                return await archiveRoom(message, this.i18n, this.discordClient);
            }
            if (tokens[0].toLowerCase() === 'convert') {
                return await convertRoom(message, tokens, this.i18n, this.discordClient);
            }
        }

        /* Get the support type */
        const typeKey = commandParameters.supportType;
        const type = this.config.support.types[typeKey];
        if (!type) {
            const types = Object.keys(this.config.support.types).map(type => this.i18n.__mf(messages.type, {
                type,
                description: this.config.support.types[type].description
            }));
            const helpText = this.i18n.__mf(messages.help, {types});
            return message.channel.send(this.i18n.__mf(messages.unknownType, {type: typeKey, help: helpText}));
        }

        /* Handle the case of no username provided */
        if (!commandParameters.ign) {
            if (commandParameters.command === 'support') {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvidedWithType, {
                    command: commandParameters.command,
                    type: commandParameters.supportType
                }));
            } else {
                return message.channel.send(this.i18n.__mf(messages.noIgnProvided, {
                    command: commandParameters.command
                }));
            }
        }

        /* Create the channel and add the correct people to it */
        const supportCategory = message.guild.channels.cache.get(this.config.support.category);
        const supportChannel = await message.guild.channels.create(generateRoomName(typeKey), {
            type: 'text',
            parent: supportCategory,
        })
            .then(channel => channel.lockPermissions())
            .then(channel => channel.updateOverwrite(message.author.id, {
                ADD_REACTIONS: true,
                ATTACH_FILES: true,
                EMBED_LINKS: true,
                READ_MESSAGE_HISTORY: true,
                SEND_MESSAGES: true,
                USE_EXTERNAL_EMOJIS: true,
                SEND_TTS_MESSAGES: false,
                VIEW_CHANNEL: true
            }))
            .then(async channel => {
                for (const supportRole of type.roles) {
                    await channel.updateOverwrite(supportRole.id, {
                        ADD_REACTIONS: true,
                        ATTACH_FILES: true,
                        EMBED_LINKS: true,
                        READ_MESSAGE_HISTORY: true,
                        SEND_MESSAGES: true,
                        USE_EXTERNAL_EMOJIS: true,
                        SEND_TTS_MESSAGES: true,
                        VIEW_CHANNEL: true
                    });
                }
                return channel;
            })
            .catch(() => {
                /* If something went wrong with room creation, we delete it (if it was created) and notify the user */
                if (supportChannel) {
                    supportChannel.delete().catch(() => {});
                }
                return message.channel.send(this.i18n.__mf(messages.roomCreationError));
            });

        const welcomeMessage = await supportChannel.send(this.i18n.__mf(messages.supportWelcome, {
            user: `<@${message.author.id}>`,
            IGN: commandParameters.ign,
            type: typeKey
        }));

        /* Add the collector to deal with post-creation events */
        this.addCollector(supportChannel, type, message.member, welcomeMessage);

        await supportChannel.send(this.i18n.__mf(messages.supportRules));
        return message.channel.send(this.i18n.__mf(messages.roomCreated, {type: typeKey}));
    },
    addCollector: function (supportChannel, supportType, creator, welcomeMessage) {
        const collectorFilter = (message) => message.member.id === creator.id;

        let collector;
        if (!supportType.autoClose || !supportType.autoClose.closing
            || supportType.autoClose.closing < 0) {
            collector = supportChannel.createMessageCollector(collectorFilter);
        } else {
            const automaticDelete = supportType.autoClose.closing;
            collector = supportChannel.createMessageCollector(collectorFilter,
                {time: automaticDelete});
            collector.on('end', async (_, reason) => {
                if (reason === 'time') {
                    await archiveRoom(welcomeMessage, this.i18n, this.discordClient);
                }
            });
        }

        collector.once('collect', (message) => {
            const supportType = parseRoomType(supportChannel.name);
            const supportRoles = this.config.support.types[supportType].roles
                .filter(supportRole => supportRole.ping)
                .map(supportRole => `<@&${supportRole.id}>`)
                .join(', ');

            message.channel.send(this.i18n.__mf(messages.supportMessageReceived, {roles: supportRoles}));
            collector.stop();
        });
    },
    extractParameters: function (tokens) {
        const clonedTokens = [...tokens];
        /* Extract the support type */
        const command = clonedTokens[0].substr(this.commandPrefix.length, clonedTokens[0].length);
        clonedTokens.shift();
        let supportType;
        if (command === 'support') {
            if (clonedTokens.length === 0) {
                return null;
            } else {
                supportType = clonedTokens[0];
                clonedTokens.shift();
            }
        } else {
            supportType = command.split('support')[0];
        }

        /* Extract the IGN */
        if (clonedTokens.length === 0) {
            return {
                command,
                supportType: supportType.toLowerCase(),
                ign: null
            }
        }
        const ign = clonedTokens[0];

        /* Return the extracted data in an object */
        return {
            command,
            supportType,
            ign
        }
    }
});
<|endoftext|>


<commit_before>import CommandParserError from '../Errors/CommandParserError';

/**
 * Parses the signature of a command.
 */
export default class Parser {
  static parse(signature) {
    if (!signature.trim()) {
      throw new CommandParserError('Signature should not be empty.');
    }

    const signatureRegex = /([^\s]+)(\s*\{\s*(.*?)\s*\})*/;

    if (!signatureRegex.test(signature)) {
      throw new CommandParserError(`Signature could not be parsed. Was given => ${signature}.`);
    }

    const matches = signatureRegex.exec(signature);
  }
}
<commit_msg>WIP - Continue work on parser.<commit_after>import CommandParserError from '../Errors/CommandParserError';

/**
 * Parses the signature of a command.
 */
export default class Parser {
  /**
   * Parses a given command signature.
   * @param {String} signature - The command signature.
   * @returns {Object} Object containing data on the command signature.
   */
  static parse(signature) {
    if (!signature.trim()) {
      throw new CommandParserError('Signature cannot not be empty.');
    }

    const partition = signature.split(' ');
    const identifier = partition[0].trim();

    if (partition.length > 1) {
      const matches = partition.slice(1).join(' ').match(/\{\s*(.*?)\s*\}/g);
      const parameters = Parser.parseParameters(matches);

      return {
        identifier,
        parameters,
      };
    }

    return {
      identifier,
    };
  }

  /**
   * Parses the parameters in a command signature.
   * @param {Array<String>} parameters - Array of parameters parsed from the signature.
   * @returns {Array<Object>} Array containing data on the parsed parameters.
   */
  static parseParameters(parameters) {
    return parameters.map(param => Parser.parseArgument(param));
  }

  /**
   * Parses a single command argument.
   * @param {String} argument - The argument.
   * @returns {Object} Object containing data on the parsed argument.
   */
  static parseArgument(argument) {
    let name = null;
    let description = null;

    if (argument.indexOf(':') !== -1) {
      [name, description] = argument.split(':', 2).map(element => element.trim());
    } else {
      name = argument;
    }

    return {
      name,
      description,
    };
  }
}
<|endoftext|>


<commit_before>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

function load(name) {
    var module = require("../skins/base/views/"+name);
    return module;
};

var ComponentBroker = function() {
    this.components = {};
};

ComponentBroker.prototype = {
    get: function(name) {
        if (this.components[name]) {
            return this.components[name];
        }

        this.components[name] = load(name);
        return this.components[name];
    },

    set: function(name, module) {
        this.components[name] = module;
    }
};

// We define one Component Broker globally, because the intention is
// very much that it is a singleton. Relying on there only being one
// copy of the module can be dicey and not work as browserify's
// behaviour with multiple copies of files etc. is erratic at best.
// XXX: We can still end up with the same file twice in the resulting
// JS bundle which is nonideal.
if (global.componentBroker === undefined) {
    global.componentBroker = new ComponentBroker();
}
module.exports = global.componentBroker;

// We need to tell browserify to include all the components
// by direct require syntax in here, but we don't want them
// to be evaluated in this file because then we wouldn't be
// able to override them. if (0) does this.
// Must be in this file (because the require is file-specific) and
// must be at the end because the components include this file.
if (0) {
require('../skins/base/views/atoms/LogoutButton');
require('../skins/base/views/atoms/EnableNotificationsButton');
require('../skins/base/views/atoms/MessageTimestamp');
require('../skins/base/views/atoms/create_room/CreateRoomButton');
require('../skins/base/views/atoms/create_room/RoomNameTextbox');
require('../skins/base/views/atoms/create_room/RoomTopic');
require('../skins/base/views/atoms/create_room/RoomAlias');
require('../skins/base/views/atoms/create_room/Presets');
require('../skins/base/views/atoms/EditableText');
require('../skins/base/views/molecules/MatrixToolbar');
require('../skins/base/views/molecules/RoomTile');
require('../skins/base/views/molecules/MessageTile');
require('../skins/base/views/molecules/SenderProfile');
require('../skins/base/views/molecules/UnknownMessageTile');
require('../skins/base/views/molecules/MTextTile');
require('../skins/base/views/molecules/MNoticeTile');
require('../skins/base/views/molecules/MEmoteTile');
require('../skins/base/views/molecules/MImageTile');
require('../skins/base/views/molecules/MFileTile');
require('../skins/base/views/molecules/MRoomMemberTile');
require('../skins/base/views/molecules/RoomHeader');
require('../skins/base/views/molecules/MessageComposer');
require('../skins/base/views/molecules/ProgressBar');
require('../skins/base/views/molecules/ServerConfig');
require('../skins/base/views/organisms/MemberList');
require('../skins/base/views/molecules/MemberTile');
require('../skins/base/views/organisms/RoomList');
require('../skins/base/views/organisms/RoomView');
require('../skins/base/views/templates/Login');
require('../skins/base/views/templates/Register');
require('../skins/base/views/organisms/Notifier');
require('../skins/base/views/organisms/CreateRoom');
require('../skins/base/views/molecules/UserSelector');
require('../skins/base/views/organisms/UserSettings');
require('../skins/base/views/molecules/ChangeAvatar');
require('../skins/base/views/molecules/ChangePassword');
// new for vector
require('../skins/base/views/organisms/LeftPanel');
require('../skins/base/views/organisms/RightPanel');
require('../skins/base/views/molecules/RoomCreate');
require('../skins/base/views/molecules/RoomDropTarget');
require('../skins/base/views/molecules/DirectoryMenu');
require('../skins/base/views/atoms/voip/VideoFeed');
require('../skins/base/views/molecules/voip/VideoView');
require('../skins/base/views/molecules/voip/CallView');
require('../skins/base/views/molecules/voip/IncomingCallBox');
require('../skins/base/views/molecules/voip/MCallInviteTile');
require('../skins/base/views/molecules/voip/MCallAnswerTile');
require('../skins/base/views/molecules/voip/MCallHangupTile');

}
<commit_msg>Remove deleted atoms<commit_after>/*
Copyright 2015 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

function load(name) {
    var module = require("../skins/base/views/"+name);
    return module;
};

var ComponentBroker = function() {
    this.components = {};
};

ComponentBroker.prototype = {
    get: function(name) {
        if (this.components[name]) {
            return this.components[name];
        }

        this.components[name] = load(name);
        return this.components[name];
    },

    set: function(name, module) {
        this.components[name] = module;
    }
};

// We define one Component Broker globally, because the intention is
// very much that it is a singleton. Relying on there only being one
// copy of the module can be dicey and not work as browserify's
// behaviour with multiple copies of files etc. is erratic at best.
// XXX: We can still end up with the same file twice in the resulting
// JS bundle which is nonideal.
if (global.componentBroker === undefined) {
    global.componentBroker = new ComponentBroker();
}
module.exports = global.componentBroker;

// We need to tell browserify to include all the components
// by direct require syntax in here, but we don't want them
// to be evaluated in this file because then we wouldn't be
// able to override them. if (0) does this.
// Must be in this file (because the require is file-specific) and
// must be at the end because the components include this file.
if (0) {
require('../skins/base/views/atoms/LogoutButton');
require('../skins/base/views/atoms/EnableNotificationsButton');
require('../skins/base/views/atoms/MessageTimestamp');
require('../skins/base/views/atoms/create_room/CreateRoomButton');
require('../skins/base/views/atoms/create_room/RoomAlias');
require('../skins/base/views/atoms/create_room/Presets');
require('../skins/base/views/atoms/EditableText');
require('../skins/base/views/molecules/MatrixToolbar');
require('../skins/base/views/molecules/RoomTile');
require('../skins/base/views/molecules/MessageTile');
require('../skins/base/views/molecules/SenderProfile');
require('../skins/base/views/molecules/UnknownMessageTile');
require('../skins/base/views/molecules/MTextTile');
require('../skins/base/views/molecules/MNoticeTile');
require('../skins/base/views/molecules/MEmoteTile');
require('../skins/base/views/molecules/MImageTile');
require('../skins/base/views/molecules/MFileTile');
require('../skins/base/views/molecules/MRoomMemberTile');
require('../skins/base/views/molecules/RoomHeader');
require('../skins/base/views/molecules/MessageComposer');
require('../skins/base/views/molecules/ProgressBar');
require('../skins/base/views/molecules/ServerConfig');
require('../skins/base/views/organisms/MemberList');
require('../skins/base/views/molecules/MemberTile');
require('../skins/base/views/organisms/RoomList');
require('../skins/base/views/organisms/RoomView');
require('../skins/base/views/templates/Login');
require('../skins/base/views/templates/Register');
require('../skins/base/views/organisms/Notifier');
require('../skins/base/views/organisms/CreateRoom');
require('../skins/base/views/molecules/UserSelector');
require('../skins/base/views/organisms/UserSettings');
require('../skins/base/views/molecules/ChangeAvatar');
require('../skins/base/views/molecules/ChangePassword');
// new for vector
require('../skins/base/views/organisms/LeftPanel');
require('../skins/base/views/organisms/RightPanel');
require('../skins/base/views/molecules/RoomCreate');
require('../skins/base/views/molecules/RoomDropTarget');
require('../skins/base/views/molecules/DirectoryMenu');
require('../skins/base/views/atoms/voip/VideoFeed');
require('../skins/base/views/molecules/voip/VideoView');
require('../skins/base/views/molecules/voip/CallView');
require('../skins/base/views/molecules/voip/IncomingCallBox');
require('../skins/base/views/molecules/voip/MCallInviteTile');
require('../skins/base/views/molecules/voip/MCallAnswerTile');
require('../skins/base/views/molecules/voip/MCallHangupTile');

}
<|endoftext|>


<commit_before>/*
 * instant.js
 *
 * Licence: MIT
 * Author:  Timm Albers (http://timmalbers.de)
 * Version: 0.1.0
 */

// TODO test
Handlebars.registerHelper('observe', function(context, options) {
	var ret = '';
	var namespace = JSON.stringify(this)
			.replace('":' + JSON.stringify(context) + '}', '')
			.replace('{"', '');

	for(var i=0; i < context.length; i++) {
		ret += options.fn(context[i]);
	}

	ret = ret.replace(/<input/g, '<input data-instant-namespace="' + namespace + '"');

	return ret;
});

var instant = (function() {
	return {
		/*
		 * Encapsulates the controller-functionality.
		 */
		controller: (function() {
			var data;
			var options = {
				/*
				 * Finds scripts with "text/template"-type and returns all found templates.
				 * Can be replaced via instant.controller.init().
				 */
				getTemplates: function() {
					var templateElements = $('script[type="text/template"]');
					var templates = {};

					for(var i = 0, j = templateElements.length; i < j; i++) {
						var e = $(templateElements[i]);
						var classes = e.attr('class').split(' ');
						var template = e.html();

						for(var n = 0, m = classes.length; n < m; n++) {
							classes[n] = classes[n].replace('template.', '');
							templates[classes[n]] = template;
						}
					}

					return templates;
				},

				/*
				 * Compiles a template.
				 * Can be replaced via instant.controller.init().
				 */
				compile: function(template, data) {
					return Handlebars.compile(template)(data);
				},

				renderCallback: null
			};

			return {
				/*
				 * Sets up everything.
				 */
				init: function(data_, options_) {
					if(data_) data = data_;

					if(options_) {
						for(var key in options_) {
							options[key] = options_[key];
						}
					}

					/*
					 * Register a handler-function for changes made to the DOM.
					 */
					instant.observer.register(function(subject) {
						var index = subject.element.index() / 2; // TODO
						var namespace = subject.element.attr('data-instant-namespace'); // TODO

						for(var key in data[namespace][index]) {
							if(data[namespace][index][key] == subject.prevValue) {
								data[namespace][index][key] = subject.getValue();
							}
						}
					});
				},

				/*
				 * Calls options.getTemplates(), compiles the template, updates the DOM and rebuilds the actions.
				 */
				render: function() {
					var templates = options.getTemplates();

					for(var key in templates) {
						var html = options.compile(templates[key], data);

						$('.' + key).each(function() {
							if($(this).attr('type') !== 'text/template') $(this).html(html);
						});
					}

					instant.actions.rebuild();
					this.parseDOM();
					if(typeof options.renderCallback === 'function') options.renderCallback();
				},

				/*
				 * Parses the DOM, gets changable elements
				 */
				parseDOM: function() {
					instant.observer.reset();

					$('input').each(function() {
						instant.observer.add($(this));
					});
				}
			};
		})(),

		/*
		 * Encapsulates eveything for observing the DOM for changes.
		 */
		observer: (function() {
			var subjects = [];
			var listeners = [];

			/*
			 * Attaches the handler-function to DOM-events.
			 */
			var init = function() {
				$(document).keyup(onchange);
			};

			/*
			 * Handles changes in the DOM.
			 */
			var onchange = function(e) {
				for(var i = 0; i < subjects.length; i++) {
					if(subjects[i].prevValue !== subjects[i].getValue()) {
						notifyListeners(subjects[i]);

						subjects[i].prevValue = subjects[i].getValue();
					}
				}
			};

			/*
			 *
			 */
			var notifyListeners = function(subject) {
				for(var i = 0; i < listeners.length; i++) {
					if(typeof listeners[i] === 'function') {
						listeners[i](subject);
					}
				}
			};

			init();

			return {
				reset: function() {
					subjects = [];
				},

				/*
				 *
				 */
				add: function(subject) {
					if(typeof subject.element !== 'object') {
						subject = {
							element: subject,
							getValue: function() {
								return this.element.val();
							},
							prevValue: null
						};
					}

					subject.prevValue = subject.getValue();
					subjects.push(subject);
				},

				/*
				 *
				 */
				register: function(listener) {
					if(typeof listener === 'function') {
						listeners.push(listener);

						return true;
					} else {
						return false;
					}
				}
			};
		})(),

		/*
		 * Encapsulates everything regarding actions.
		 */
		actions: (function() {
			/*
			 * The CSS-class-seperator for representing paths.
			 */
			var seperator = '\\.'; // TODO

			/*
			 * Adds the given actions to the DOM.
			 */
			var initDOM = function(object, parentSelector) {
				if(!parentSelector) parentSelector = [];

				/*
				 * Go through listeners.
				 */
				for(var key in object) {
					// Clone array.
					var selector = [].concat(parentSelector);
					selector.push(key);

					// We have a handler-function or -object (with .handler-function) here.
					if(typeof object[key] === 'function' || (typeof object[key].handler === 'function')) {
						// Build the click-handler.
						var click = (function() {
							// Get the specific handler.
							var handler = object[key] === 'function' ? object[key] : object[key].handler;
							// Get the subject. (The "parent"-object.)
							var subject = selector[selector.length-2];

							/*
							 * The concrete handler-function.
							 */
							return function(e) {
								// The DOM-element.
								var item = $(this);
								if(!$(this).hasClass(subject)) {
									item = item.closest('.' + subject);
								}

								// Get identical DOM-elements on the same DOM-layer.
								var elements = item.parent().children();
								var index = elements.index(item);

								// Call the given handler.
								handler(index);
							};
						})();
						// Add the built click-handler to the DOM.
						$('.actions' + seperator + selector.join(seperator)).click(click);

						// Check, if the given object should have a key-handler.
						if(object[key].key) {
							// Build keyboard-handlers.
							var concreteKeyHandler = (function() {
								// Get the specific handler.
								var handler = typeof object[key] === 'function' ? object[key] : object[key].handler;
								// Get the function for getting the index of the DOM-element on the same DOM-layer.
								var getIndex = typeof object[key].getIndex === 'function' ? object[key].getIndex : function() {return -1;};
								var keyCode = object[key].key;

								return function() {
									// Call the given handler.
									handler(getIndex());
								};
							})();

							// Save the built handler in keys, so that it can be called later.
							keys[object[key].key] = {};
							var obj = keys[object[key].key];
							for(var i = 0; i < selector.length; i++) {
								obj[selector[i]] = {};
								obj = obj[selector[i]]
							}
							obj.handler = concreteKeyHandler;
						}

					} else if(typeof object[key] === 'object') {
						// Recursive call.
						initDOM(object[key], selector);
					}
				}
			};

			var keys = {};
			var keyHandler = function(e) {
				var cur = keys[e.keyCode];

				/*
				 * Invokes all handlers attached for a key.
				 */
				var invoke = function(obj) {
					for(var key in obj) {
						if(obj[key].handler) obj[key].handler();
						else invoke(obj[key]);
					}
				};

				if(cur) {
					invoke(cur);
				}
			};
			// Add the key-handler to the DOM.
			$(document).keydown(keyHandler);

			return {
				/*
				 * Initialize the actions.
				 */
				init: function(handlers) {
					this.invoke = handlers;
					initDOM(handlers);
				},

				/*
				 * Rebuilds the actions. Should be called after updating the DOM.
				 */
				rebuild: function() {
					initDOM(this.invoke);
				},

				/*
				 * Placeholder-object for given handlers.
				 */
				invoke: {}
			};
		})()
	};
})();<commit_msg>Updated version<commit_after>/*
 * instant.js
 *
 * Licence: MIT
 * Author:  Timm Albers (http://timmalbers.de)
 * Version: 0.0.0
 */

// TODO test
Handlebars.registerHelper('observe', function(context, options) {
	var ret = '';
	var namespace = JSON.stringify(this)
			.replace('":' + JSON.stringify(context) + '}', '')
			.replace('{"', '');

	for(var i=0; i < context.length; i++) {
		ret += options.fn(context[i]);
	}

	ret = ret.replace(/<input/g, '<input data-instant-namespace="' + namespace + '"');

	return ret;
});

var instant = (function() {
	return {
		/*
		 * Encapsulates the controller-functionality.
		 */
		controller: (function() {
			var data;
			var options = {
				/*
				 * Finds scripts with "text/template"-type and returns all found templates.
				 * Can be replaced via instant.controller.init().
				 */
				getTemplates: function() {
					var templateElements = $('script[type="text/template"]');
					var templates = {};

					for(var i = 0, j = templateElements.length; i < j; i++) {
						var e = $(templateElements[i]);
						var classes = e.attr('class').split(' ');
						var template = e.html();

						for(var n = 0, m = classes.length; n < m; n++) {
							classes[n] = classes[n].replace('template.', '');
							templates[classes[n]] = template;
						}
					}

					return templates;
				},

				/*
				 * Compiles a template.
				 * Can be replaced via instant.controller.init().
				 */
				compile: function(template, data) {
					return Handlebars.compile(template)(data);
				},

				renderCallback: null
			};

			return {
				/*
				 * Sets up everything.
				 */
				init: function(data_, options_) {
					if(data_) data = data_;

					if(options_) {
						for(var key in options_) {
							options[key] = options_[key];
						}
					}

					/*
					 * Register a handler-function for changes made to the DOM.
					 */
					instant.observer.register(function(subject) {
						var index = subject.element.index() / 2; // TODO
						var namespace = subject.element.attr('data-instant-namespace'); // TODO

						for(var key in data[namespace][index]) {
							if(data[namespace][index][key] == subject.prevValue) {
								data[namespace][index][key] = subject.getValue();
							}
						}
					});
				},

				/*
				 * Calls options.getTemplates(), compiles the template, updates the DOM and rebuilds the actions.
				 */
				render: function() {
					var templates = options.getTemplates();

					for(var key in templates) {
						var html = options.compile(templates[key], data);

						$('.' + key).each(function() {
							if($(this).attr('type') !== 'text/template') $(this).html(html);
						});
					}

					instant.actions.rebuild();
					this.parseDOM();
					if(typeof options.renderCallback === 'function') options.renderCallback();
				},

				/*
				 * Parses the DOM, gets changable elements
				 */
				parseDOM: function() {
					instant.observer.reset();

					$('input').each(function() {
						instant.observer.add($(this));
					});
				}
			};
		})(),

		/*
		 * Encapsulates eveything for observing the DOM for changes.
		 */
		observer: (function() {
			var subjects = [];
			var listeners = [];

			/*
			 * Attaches the handler-function to DOM-events.
			 */
			var init = function() {
				$(document).keyup(onchange);
			};

			/*
			 * Handles changes in the DOM.
			 */
			var onchange = function(e) {
				for(var i = 0; i < subjects.length; i++) {
					if(subjects[i].prevValue !== subjects[i].getValue()) {
						notifyListeners(subjects[i]);

						subjects[i].prevValue = subjects[i].getValue();
					}
				}
			};

			/*
			 *
			 */
			var notifyListeners = function(subject) {
				for(var i = 0; i < listeners.length; i++) {
					if(typeof listeners[i] === 'function') {
						listeners[i](subject);
					}
				}
			};

			init();

			return {
				reset: function() {
					subjects = [];
				},

				/*
				 *
				 */
				add: function(subject) {
					if(typeof subject.element !== 'object') {
						subject = {
							element: subject,
							getValue: function() {
								return this.element.val();
							},
							prevValue: null
						};
					}

					subject.prevValue = subject.getValue();
					subjects.push(subject);
				},

				/*
				 *
				 */
				register: function(listener) {
					if(typeof listener === 'function') {
						listeners.push(listener);

						return true;
					} else {
						return false;
					}
				}
			};
		})(),

		/*
		 * Encapsulates everything regarding actions.
		 */
		actions: (function() {
			/*
			 * The CSS-class-seperator for representing paths.
			 */
			var seperator = '\\.'; // TODO

			/*
			 * Adds the given actions to the DOM.
			 */
			var initDOM = function(object, parentSelector) {
				if(!parentSelector) parentSelector = [];

				/*
				 * Go through listeners.
				 */
				for(var key in object) {
					// Clone array.
					var selector = [].concat(parentSelector);
					selector.push(key);

					// We have a handler-function or -object (with .handler-function) here.
					if(typeof object[key] === 'function' || (typeof object[key].handler === 'function')) {
						// Build the click-handler.
						var click = (function() {
							// Get the specific handler.
							var handler = object[key] === 'function' ? object[key] : object[key].handler;
							// Get the subject. (The "parent"-object.)
							var subject = selector[selector.length-2];

							/*
							 * The concrete handler-function.
							 */
							return function(e) {
								// The DOM-element.
								var item = $(this);
								if(!$(this).hasClass(subject)) {
									item = item.closest('.' + subject);
								}

								// Get identical DOM-elements on the same DOM-layer.
								var elements = item.parent().children();
								var index = elements.index(item);

								// Call the given handler.
								handler(index);
							};
						})();
						// Add the built click-handler to the DOM.
						$('.actions' + seperator + selector.join(seperator)).click(click);

						// Check, if the given object should have a key-handler.
						if(object[key].key) {
							// Build keyboard-handlers.
							var concreteKeyHandler = (function() {
								// Get the specific handler.
								var handler = typeof object[key] === 'function' ? object[key] : object[key].handler;
								// Get the function for getting the index of the DOM-element on the same DOM-layer.
								var getIndex = typeof object[key].getIndex === 'function' ? object[key].getIndex : function() {return -1;};
								var keyCode = object[key].key;

								return function() {
									// Call the given handler.
									handler(getIndex());
								};
							})();

							// Save the built handler in keys, so that it can be called later.
							keys[object[key].key] = {};
							var obj = keys[object[key].key];
							for(var i = 0; i < selector.length; i++) {
								obj[selector[i]] = {};
								obj = obj[selector[i]]
							}
							obj.handler = concreteKeyHandler;
						}

					} else if(typeof object[key] === 'object') {
						// Recursive call.
						initDOM(object[key], selector);
					}
				}
			};

			var keys = {};
			var keyHandler = function(e) {
				var cur = keys[e.keyCode];

				/*
				 * Invokes all handlers attached for a key.
				 */
				var invoke = function(obj) {
					for(var key in obj) {
						if(obj[key].handler) obj[key].handler();
						else invoke(obj[key]);
					}
				};

				if(cur) {
					invoke(cur);
				}
			};
			// Add the key-handler to the DOM.
			$(document).keydown(keyHandler);

			return {
				/*
				 * Initialize the actions.
				 */
				init: function(handlers) {
					this.invoke = handlers;
					initDOM(handlers);
				},

				/*
				 * Rebuilds the actions. Should be called after updating the DOM.
				 */
				rebuild: function() {
					initDOM(this.invoke);
				},

				/*
				 * Placeholder-object for given handlers.
				 */
				invoke: {}
			};
		})()
	};
})();<|endoftext|>