<commit_before>'use strict'

const {promisify} = require('util')
const {extname, join} = require('path')
const {createWriteStream, mkdir} = require('fs')
const execa = require('execa')
const hasha = require('hasha')
const getStream = require('get-stream')
const {get, omit, pick} = require('lodash')
const contentDisposition = require('content-disposition')
const contentType = require('content-type')
const whichType = require('file-type')
const {is} = require('type-is')
const {extension} = require('mime-types')
const {pipe} = require('mississippi')
const debug = require('debug')('plunger')

const request = require('./util/request')
const {bytesLimit, computeDigest} = require('./util/streams')
const {createTempDirectory} = require('./util/tmpdir')

const {analyzeLocation} = require('./directory')

const mkdirAsync = promisify(mkdir)

async function analyzeURL(location) {
  debug('fetching %s', location)
  const response = await request(location)
  const {finalURL, statusCode, redirectURLs} = response
  debug('responded with status %d', statusCode)
  if (redirectURLs.length > 0) {
    debug('redirected %d times: %s', redirectURLs.length, redirectURLs.join(', '))
  }

  const result = {
    headers: extractHeaders(response),
    statusCode,
    redirectURLs,
    originalURL: location,
    finalURL
  }

  if (statusCode === 304) {
    result.type = 'not-modified'
    response.destroy()
    return result
  }

  if (statusCode !== 200) {
    result.type = 'error'
    response.destroy()
    return result
  }

  if (!response.body) {
    result.type = 'no-body'
    response.destroy()
    return result
  }

  result.contentType = extractContentType(response)
  result.contentDisposition = extractContentDisposition(response)

  const fileNameInURL = getFileNameFromLocation(finalURL)
  const fileNameInHeaders = get(result.contentDisposition, 'parameters.filename')

  result.fileName = fileNameInHeaders || fileNameInURL
  result.fileExtension = getExtension(result.fileName)

  if (isXML(response)) {
    result.type = 'xml'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isHTML(response)) {
    result.type = 'html'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isPlainText(response)) {
    result.type = 'text'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isJSON(response)) {
    result.type = 'json'
    await attachBody(result, response, 'sha384')
    return result
  }

  result.fileType = detectFileType(response)

  if (isArchive(result.fileType)) {
    let tmpDirectory
    try {
      result.type = 'archive'
      result.archiveType = result.fileType.ext
      tmpDirectory = await createTempDirectory()
      const filePath = join(tmpDirectory.path, result.fileName)
      const {digest} = await downloadFile(filePath, response.body)
      result.filePath = filePath
      result.tmpDirectory = tmpDirectory.path
      result.digest = digest
      const unarchivedPath = join(tmpDirectory.path, 'unarchived')
      await extractArchive(filePath, unarchivedPath)
      debug('analyzing directory tree')
      result.files = await analyzeLocation(unarchivedPath)
      debug('directory tree analyzed')
    } catch (err) {
      result.checkError = err.message
      response.destroy()
    } finally {
      // tmpDirectory.clean().catch(console.error)
    }
    return result
  }

  // THEN?

  result.type = 'unknown'
  response.destroy()
  return result
}

/* Helpers */

function extractHeaders(response) {
  return omit(response.headers, 'set-cookie', 'connection')
}

function extractContentDisposition(response) {
  const headerValue = response.headers['content-disposition']
  if (!headerValue) return {}
  return contentDisposition.parse(headerValue)
}

function extractContentType(response) {
  const headerValue = response.headers['content-type']
  if (!headerValue) return {}
  const parsedValue = pick(contentType.parse(headerValue), 'type', 'parameters')
  if (parsedValue.type) parsedValue.ext = extension(parsedValue.type)
  return parsedValue
}

function getFileNameFromLocation(location) {
  const pos = location.lastIndexOf('/')
  return pos < location.length ? location.substr(pos + 1) : null
}

function getExtension(fileName) {
  if (!fileName) return null
  return extname(fileName).substr(1).toLowerCase()
}

function detectFileType(response) {
  return whichType(getFirstChunk(response)) || {}
}

function isHTML(response) {
  return is(response, ['html'])
}

function isJSON(response) {
  return is(response, ['json'])
}

function isXML(response) {
  return is(response, ['xml'])
}

function isPlainText(response) {
  return response.headers['content-type'] === 'text/plain'
}

// function isBinary(response) {
//   return [
//     'application/octet-stream',
//     'application/binary',
//   ].includes(response.headers['content-type'])
// }

function isArchive(fileType) {
  if (!fileType) {
    throw new Error('fileType is required')
  }
  return [
    'zip',
    'tar',
    'rar',
    'gz',
    'bz2',
    '7z',
    'xz',
    'exe'
  ].includes(fileType.ext)
}

function getFirstChunk(response) {
  const firstChunk = response.body.read()
  if (!firstChunk) throw new Error('No chunk available')
  response.body.unshift(firstChunk)
  return firstChunk
}

function getDigestString(digest, algorithm = 'sha384') {
  if (!digest) return ''
  return `${algorithm}-${digest}`
}

async function attachBody(result, response, digestAlgorithm = 'sha384') {
  debug('downloading body')
  const buffer = await getStream.buffer(response.body)
  debug('body downloaded')
  result.body = buffer.toString('utf8')
  result.bodyLength = buffer.length
  debug('computing digest')
  const digest = hasha(buffer, {algorithm: digestAlgorithm})
  debug('digest computed')
  result.digest = getDigestString(digest, digestAlgorithm)
}

function downloadFile(path, body) {
  debug('downloading file (w/ on-the-fly digest computation)')
  return new Promise((resolve, reject) => {
    let digest
    let fileSize

    const onDigest = result => {
      digest = getDigestString(result.digest, 'sha384')
      fileSize = result.bodyLength
    }

    pipe(
      body,
      computeDigest('sha384', onDigest),
      bytesLimit(100 * 1024 * 1024),
      createWriteStream(path),
      err => {
        if (err) return reject(err)
        debug('file downloaded')
        resolve({digest, fileSize})
      }
    )
  })
}

async function extractArchive(archivePath, extractPath) {
  await mkdirAsync(extractPath)
  debug('extracting archive')
  await execa('unar', ['-no-directory', '-output-directory', extractPath, archivePath])
  debug('archive extracted')
}

module.exports = {analyzeURL}
<commit_msg>Fix filename generation<commit_after>'use strict'

const {promisify} = require('util')
const {extname, join} = require('path')
const {createWriteStream, mkdir} = require('fs')
const url = require('url')
const execa = require('execa')
const hasha = require('hasha')
const getStream = require('get-stream')
const {get, omit, pick} = require('lodash')
const contentDisposition = require('content-disposition')
const contentType = require('content-type')
const whichType = require('file-type')
const {is} = require('type-is')
const {extension} = require('mime-types')
const {pipe} = require('mississippi')
const debug = require('debug')('plunger')

const request = require('./util/request')
const {bytesLimit, computeDigest} = require('./util/streams')
const {createTempDirectory} = require('./util/tmpdir')

const {analyzeLocation} = require('./directory')

const mkdirAsync = promisify(mkdir)

async function analyzeURL(location) {
  debug('fetching %s', location)
  const response = await request(location)
  const {finalURL, statusCode, redirectURLs} = response
  debug('responded with status %d', statusCode)
  if (redirectURLs.length > 0) {
    debug('redirected %d times: %s', redirectURLs.length, redirectURLs.join(', '))
  }

  const result = {
    headers: extractHeaders(response),
    statusCode,
    redirectURLs,
    originalURL: location,
    finalURL
  }

  if (statusCode === 304) {
    result.type = 'not-modified'
    response.destroy()
    return result
  }

  if (statusCode !== 200) {
    result.type = 'error'
    response.destroy()
    return result
  }

  if (!response.body) {
    result.type = 'no-body'
    response.destroy()
    return result
  }

  result.contentType = extractContentType(response)
  result.contentDisposition = extractContentDisposition(response)

  const fileNameInURL = getFileNameFromLocation(finalURL)
  const fileNameInHeaders = get(result.contentDisposition, 'parameters.filename')

  result.fileName = fileNameInHeaders || fileNameInURL
  result.fileExtension = getExtension(result.fileName)

  if (isXML(response)) {
    result.type = 'xml'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isHTML(response)) {
    result.type = 'html'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isPlainText(response)) {
    result.type = 'text'
    await attachBody(result, response, 'sha384')
    return result
  }

  if (isJSON(response)) {
    result.type = 'json'
    await attachBody(result, response, 'sha384')
    return result
  }

  result.fileType = detectFileType(response)

  if (isArchive(result.fileType)) {
    let tmpDirectory
    try {
      result.type = 'archive'
      result.archiveType = result.fileType.ext
      tmpDirectory = await createTempDirectory()
      const filePath = join(tmpDirectory.path, result.fileName)
      const {digest} = await downloadFile(filePath, response.body)
      result.filePath = filePath
      result.tmpDirectory = tmpDirectory.path
      result.digest = digest
      const unarchivedPath = join(tmpDirectory.path, 'unarchived')
      await extractArchive(filePath, unarchivedPath)
      debug('analyzing directory tree')
      result.files = await analyzeLocation(unarchivedPath)
      debug('directory tree analyzed')
    } catch (err) {
      result.checkError = err.message
      response.destroy()
    } finally {
      // tmpDirectory.clean().catch(console.error)
    }
    return result
  }

  // THEN?

  result.type = 'unknown'
  response.destroy()
  return result
}

/* Helpers */

function extractHeaders(response) {
  return omit(response.headers, 'set-cookie', 'connection')
}

function extractContentDisposition(response) {
  const headerValue = response.headers['content-disposition']
  if (!headerValue) return {}
  return contentDisposition.parse(headerValue)
}

function extractContentType(response) {
  const headerValue = response.headers['content-type']
  if (!headerValue) return {}
  const parsedValue = pick(contentType.parse(headerValue), 'type', 'parameters')
  if (parsedValue.type) parsedValue.ext = extension(parsedValue.type)
  return parsedValue
}

function getFileNameFromLocation(location) {
  const {pathname} = url.parse(location)

  if (!pathname) {
    return null
  }

  return pathname
    .split('/')
    .pop()
    .split('#')[0]
    .split('?')[0] || null
}

function getExtension(fileName) {
  if (!fileName) return null
  return extname(fileName).substr(1).toLowerCase()
}

function detectFileType(response) {
  return whichType(getFirstChunk(response)) || {}
}

function isHTML(response) {
  return is(response, ['html'])
}

function isJSON(response) {
  return is(response, ['json'])
}

function isXML(response) {
  return is(response, ['xml'])
}

function isPlainText(response) {
  return response.headers['content-type'] === 'text/plain'
}

// function isBinary(response) {
//   return [
//     'application/octet-stream',
//     'application/binary',
//   ].includes(response.headers['content-type'])
// }

function isArchive(fileType) {
  if (!fileType) {
    throw new Error('fileType is required')
  }
  return [
    'zip',
    'tar',
    'rar',
    'gz',
    'bz2',
    '7z',
    'xz',
    'exe'
  ].includes(fileType.ext)
}

function getFirstChunk(response) {
  const firstChunk = response.body.read()
  if (!firstChunk) throw new Error('No chunk available')
  response.body.unshift(firstChunk)
  return firstChunk
}

function getDigestString(digest, algorithm = 'sha384') {
  if (!digest) return ''
  return `${algorithm}-${digest}`
}

async function attachBody(result, response, digestAlgorithm = 'sha384') {
  debug('downloading body')
  const buffer = await getStream.buffer(response.body)
  debug('body downloaded')
  result.body = buffer.toString('utf8')
  result.bodyLength = buffer.length
  debug('computing digest')
  const digest = hasha(buffer, {algorithm: digestAlgorithm})
  debug('digest computed')
  result.digest = getDigestString(digest, digestAlgorithm)
}

function downloadFile(path, body) {
  debug('downloading file (w/ on-the-fly digest computation)')
  return new Promise((resolve, reject) => {
    let digest
    let fileSize

    const onDigest = result => {
      digest = getDigestString(result.digest, 'sha384')
      fileSize = result.bodyLength
    }

    pipe(
      body,
      computeDigest('sha384', onDigest),
      bytesLimit(100 * 1024 * 1024),
      createWriteStream(path),
      err => {
        if (err) return reject(err)
        debug('file downloaded')
        resolve({digest, fileSize})
      }
    )
  })
}

async function extractArchive(archivePath, extractPath) {
  await mkdirAsync(extractPath)
  debug('extracting archive')
  await execa('unar', ['-no-directory', '-output-directory', extractPath, archivePath])
  debug('archive extracted')
}

module.exports = {analyzeURL}
<|endoftext|>


<commit_before>var todoList = document.getElementById("todo-list");
var todoListPlaceholder = document.getElementById("todo-list-placeholder");
var form = document.getElementById("todo-form");
var todoTitle = document.getElementById("new-todo");
var itemsLeft = document.getElementById("count-label");
var error = document.getElementById("error");

form.onsubmit = function(event) {
    var title = todoTitle.value;
    createTodo(title, function() {
        reloadTodoList();
    });
    todoTitle.value = "";
    event.preventDefault();
};

function createTodo(title, callback) {
    var createRequest = new XMLHttpRequest();
    createRequest.open("POST", "/api/todo");
    createRequest.setRequestHeader("Content-type", "application/json");
    createRequest.send(JSON.stringify({
        title: title,
        isComplete: "false"
    }));
    createRequest.onload = function() {
        if (this.status === 201) {
            callback();
        } else {
            error.textContent = "Failed to create item. Server returned " + this.status + " - " + this.responseText;
        }
    };
}

function getTodoList(callback) {
    var createRequest = new XMLHttpRequest();
    createRequest.open("GET", "/api/todo");
    createRequest.onload = function() {
        if (this.status === 200) {
            callback(JSON.parse(this.responseText));
        } else {
            error.textContent = "Failed to get list. Server returned " + this.status + " - " + this.responseText;
        }
    };
    createRequest.send();
}

function reloadTodoList() {
    while (todoList.firstChild) {
        todoList.removeChild(todoList.firstChild);
    }
    todoListPlaceholder.style.display = "block";

    var totalItems = 0;
    var leftItems = 0;

    getTodoList(function(todos) {
        todoListPlaceholder.style.display = "none";

        var deleteAll = document.createElement("button");
        //var delAllText = document.createTextNode("Delete Completed");
        //deleteAll.appendChild(delAllText);

        // delete button
        var deleteAllButton = document.createElement("button");
        deleteAllButton.setAttribute("type", "button");
        deleteAllButton.setAttribute("class", "btn btn-default");
        deleteAllButton.setAttribute("id", "deleteAllButton");
        deleteAllButton.setAttribute("style", "right: 1%; position: absolute");
        var delAll = document.createElement("span");
        delAll.setAttribute("class", "glyphicon glyphicon-remove");
        delAll.setAttribute("aria-hidden", "true");
        deleteAllButton.appendChild(delAll);

        todoList.appendChild(deleteAllButton);
        itemsLeft.appendChild(deleteAllButton);

        deleteAll.onclick = function () {

            todos.forEach(function(todo) {
                if (todo.isComplete === "true") {
                    var createRequest = new XMLHttpRequest();
                    createRequest.open("DELETE", "/api/todo/" + todo.id);
                    createRequest.onload = function() {
                        if (this.status === 200) {
                        } else {
                            error.textContent = "Failed to delete item. Server returned ";
                            error.textContent += this.status + " - " + this.responseText;
                        }
                    };
                    createRequest.send();
                }
            });
            reloadTodoList();
        };

        todos.forEach(function(todo) {

            totalItems += 1 ;
            if (todo.isComplete === "false") {
                leftItems += 1 ;
            }

            var row = document.createElement("li");
            row.setAttribute("class", "list-group-item");
            row.setAttribute("style", "margin: 10px");

            var listItem = document.createElement("div");

            var item = document.createElement("div");
            item.textContent = todo.title;
            item.setAttribute("id", "item");
            if (todo.isComplete==="true") {
                item.setAttribute("style", "display: inline-block;  text-decoration: line-through; font-style: italic; padding-left: 10px");
            } else {
                item.setAttribute("style", "display: inline-block;  text-decoration: none; font-style: normal; padding-left: 10px");
            }

            var deleteButton = document.createElement("button");
            deleteButton.setAttribute("type", "button");
            deleteButton.setAttribute("class", "btn btn-default");
            deleteButton.setAttribute("id", "deleteButton");
            deleteButton.setAttribute("style", "right: 1%; position: absolute");
            var del = document.createElement("span");
            del.setAttribute("class", "glyphicon glyphicon-remove");
            del.setAttribute("aria-hidden", "true");
            deleteButton.appendChild(del);

            var updateButton = document.createElement("button");
            updateButton.setAttribute("type", "button");
            updateButton.setAttribute("class", "btn btn-default");
            updateButton.setAttribute("id", "updateButton");
            updateButton.setAttribute("style", "right: 5%; position: absolute");
            var update = document.createElement("span");
            update.setAttribute("class", "glyphicon glyphicon-pencil");
            update.setAttribute("aria-hidden", "true");
            updateButton.appendChild(update);

            var complete = document.createElement("button");
            complete.setAttribute("class", "btn btn-default");
            var tick = document.createElement("input");
            tick.setAttribute("type", "checkbox");
            tick.setAttribute("id", "tick");
            if (todo.isComplete === "true") {
                tick.setAttribute("checked", "true");
            } else {
                tick.removeAttribute("checked");
            }
            complete.appendChild(tick);

            listItem.appendChild(complete);
            listItem.appendChild(item);
            listItem.appendChild(deleteButton);
            listItem.appendChild(updateButton);

            row.appendChild(listItem);

            deleteButton.onclick = function() {
                var createRequest = new XMLHttpRequest();
                createRequest.open("DELETE", "/api/todo/"+todo.id);
                createRequest.send();
                createRequest.onload = function() {
                    if (this.status === 200) {
                        reloadTodoList();
                    } else {
                        error.textContent = "Failed to delete item. Server returned " + this.status + " - " + this.responseText;
                    }
                };
            };

            updateButton.onclick = function() {
                item.setAttribute("contenteditable", "true");

                var updButton = document.createElement("button");
                updButton.setAttribute("type", "button");
                updButton.setAttribute("class", "btn btn-default");
                updButton.setAttribute("id", "doneButton");
                updButton.setAttribute("style", "right: 5%; position: absolute");

                var upd = document.createElement("span");
                upd.setAttribute("class", "glyphicon glyphicon-ok");
                upd.setAttribute("aria-hidden", "true");
                updButton.appendChild(upd);

                listItem.removeChild(updateButton);
                listItem.appendChild(updButton);

                updButton.onclick = function() {

                    listItem.removeChild(updButton);
                    listItem.appendChild(updateButton);

                    var title = item.textContent;
                    item.removeAttribute("contenteditable");

                    var createRequest = new XMLHttpRequest();
                    createRequest.open("PUT", "/api/todo/" + todo.id);
                    createRequest.setRequestHeader("Content-type", "application/json");
                    createRequest.send(JSON.stringify({
                        title: title,
                        isComplete: todo.isComplete,
                        id : todo.id
                    }));

                    createRequest.onload = function() {
                        if (this.status === 200) {
                            reloadTodoList();
                        } else {
                            error.textContent = "Failed to update item. Server returned ";
                            error.textContent += this.status + " - " + this.responseText;
                        }
                    };
                };
            };

            tick.onclick = function () {
                var title = item.textContent;
                var complete;
                if (todo.isComplete === "true") {
                    complete = "false";
                }
                else {
                    complete = "true";
                }

                var createRequest = new XMLHttpRequest();
                createRequest.open("PUT", "/api/todo/" + todo.id);
                createRequest.setRequestHeader("Content-type", "application/json");
                createRequest.send(JSON.stringify({
                    title: title,
                    id : todo.id,
                    isComplete: complete
                }));

                createRequest.onload = function() {
                    if (this.status === 200) {
                        reloadTodoList();
                    } else {
                        error.textContent = "Failed to update item. Server returned ";
                        error.textContent += this.status + " - " + this.responseText;
                    }
                };
            };

            todoList.appendChild(row);
            todoList.appendChild(deleteAllButton);
        });

    itemsLeft.textContent = "You have " + leftItems.toString();
    itemsLeft.textContent += " items left to complete out of " + totalItems.toString();

    });

}

reloadTodoList();
<commit_msg>added delete all button<commit_after>var todoList = document.getElementById("todo-list");
var todoListPlaceholder = document.getElementById("todo-list-placeholder");
var form = document.getElementById("todo-form");
var todoTitle = document.getElementById("new-todo");
var itemsLeft = document.getElementById("count-label");
var error = document.getElementById("error");

form.onsubmit = function(event) {
    var title = todoTitle.value;
    createTodo(title, function() {
        reloadTodoList();
    });
    todoTitle.value = "";
    event.preventDefault();
};

function createTodo(title, callback) {
    var createRequest = new XMLHttpRequest();
    createRequest.open("POST", "/api/todo");
    createRequest.setRequestHeader("Content-type", "application/json");
    createRequest.send(JSON.stringify({
        title: title,
        isComplete: "false"
    }));
    createRequest.onload = function() {
        if (this.status === 201) {
            callback();
        } else {
            error.textContent = "Failed to create item. Server returned " + this.status + " - " + this.responseText;
        }
    };
}

function getTodoList(callback) {
    var createRequest = new XMLHttpRequest();
    createRequest.open("GET", "/api/todo");
    createRequest.onload = function() {
        if (this.status === 200) {
            callback(JSON.parse(this.responseText));
        } else {
            error.textContent = "Failed to get list. Server returned " + this.status + " - " + this.responseText;
        }
    };
    createRequest.send();
}

function reloadTodoList() {
    while (todoList.firstChild) {
        todoList.removeChild(todoList.firstChild);
    }
    todoListPlaceholder.style.display = "block";

    var totalItems = 0;
    var leftItems = 0;

    getTodoList(function(todos) {
        todoListPlaceholder.style.display = "none";

        //var deleteAll = document.createElement("button");
        //var delAllText = document.createTextNode("Delete Completed");
        //deleteAll.appendChild(delAllText);

        // delete button
        var deleteAllButton = document.createElement("button");
        deleteAllButton.setAttribute("type", "button");
        deleteAllButton.setAttribute("class", "btn btn-default");
        deleteAllButton.setAttribute("id", "deleteAllButton");
        //deleteAllButton.setAttribute("style", "right: 1%; position: absolute");
        var delAllText = document.createTextNode("Delete Completed");
        deleteAllButton.appendChild(delAllText);

        //todoList.appendChild(deleteAllButton);
        //itemsLeft.appendChild(deleteAllButton);

        deleteAllButton.onclick = function () {

            todos.forEach(function(todo) {
                if (todo.isComplete === "true") {
                    var createRequest = new XMLHttpRequest();
                    createRequest.open("DELETE", "/api/todo/" + todo.id);
                    createRequest.onload = function() {
                        if (this.status === 200) {
                        } else {
                            error.textContent = "Failed to delete item. Server returned ";
                            error.textContent += this.status + " - " + this.responseText;
                        }
                    };
                    createRequest.send();
                }
            });
            reloadTodoList();
        };

        todoList.appendChild(deleteAllButton);

        todos.forEach(function(todo) {

            totalItems += 1 ;
            if (todo.isComplete === "false") {
                leftItems += 1 ;
            }

            var row = document.createElement("li");
            row.setAttribute("class", "list-group-item");
            row.setAttribute("style", "margin: 10px");

            var listItem = document.createElement("div");

            var item = document.createElement("div");
            item.textContent = todo.title;
            item.setAttribute("id", "item");
            if (todo.isComplete==="true") {
                item.setAttribute("style", "display: inline-block;  text-decoration: line-through; font-style: italic; padding-left: 10px");
            } else {
                item.setAttribute("style", "display: inline-block;  text-decoration: none; font-style: normal; padding-left: 10px");
            }

            var deleteButton = document.createElement("button");
            deleteButton.setAttribute("type", "button");
            deleteButton.setAttribute("class", "btn btn-default");
            deleteButton.setAttribute("id", "deleteButton");
            deleteButton.setAttribute("style", "right: 1%; position: absolute");
            var del = document.createElement("span");
            del.setAttribute("class", "glyphicon glyphicon-remove");
            del.setAttribute("aria-hidden", "true");
            deleteButton.appendChild(del);

            var updateButton = document.createElement("button");
            updateButton.setAttribute("type", "button");
            updateButton.setAttribute("class", "btn btn-default");
            updateButton.setAttribute("id", "updateButton");
            updateButton.setAttribute("style", "right: 5%; position: absolute");
            var update = document.createElement("span");
            update.setAttribute("class", "glyphicon glyphicon-pencil");
            update.setAttribute("aria-hidden", "true");
            updateButton.appendChild(update);

            var complete = document.createElement("button");
            complete.setAttribute("class", "btn btn-default");
            var tick = document.createElement("input");
            tick.setAttribute("type", "checkbox");
            tick.setAttribute("id", "tick");
            if (todo.isComplete === "true") {
                tick.setAttribute("checked", "true");
            } else {
                tick.removeAttribute("checked");
            }
            complete.appendChild(tick);

            listItem.appendChild(complete);
            listItem.appendChild(item);
            listItem.appendChild(deleteButton);
            listItem.appendChild(updateButton);

            row.appendChild(listItem);

            deleteButton.onclick = function() {
                var createRequest = new XMLHttpRequest();
                createRequest.open("DELETE", "/api/todo/"+todo.id);
                createRequest.send();
                createRequest.onload = function() {
                    if (this.status === 200) {
                        reloadTodoList();
                    } else {
                        error.textContent = "Failed to delete item. Server returned " + this.status + " - " + this.responseText;
                    }
                };
            };

            updateButton.onclick = function() {
                item.setAttribute("contenteditable", "true");

                var updButton = document.createElement("button");
                updButton.setAttribute("type", "button");
                updButton.setAttribute("class", "btn btn-default");
                updButton.setAttribute("id", "doneButton");
                updButton.setAttribute("style", "right: 5%; position: absolute");

                var upd = document.createElement("span");
                upd.setAttribute("class", "glyphicon glyphicon-ok");
                upd.setAttribute("aria-hidden", "true");
                updButton.appendChild(upd);

                listItem.removeChild(updateButton);
                listItem.appendChild(updButton);

                updButton.onclick = function() {

                    listItem.removeChild(updButton);
                    listItem.appendChild(updateButton);

                    var title = item.textContent;
                    item.removeAttribute("contenteditable");

                    var createRequest = new XMLHttpRequest();
                    createRequest.open("PUT", "/api/todo/" + todo.id);
                    createRequest.setRequestHeader("Content-type", "application/json");
                    createRequest.send(JSON.stringify({
                        title: title,
                        isComplete: todo.isComplete,
                        id : todo.id
                    }));

                    createRequest.onload = function() {
                        if (this.status === 200) {
                            reloadTodoList();
                        } else {
                            error.textContent = "Failed to update item. Server returned ";
                            error.textContent += this.status + " - " + this.responseText;
                        }
                    };
                };
            };

            tick.onclick = function () {
                var title = item.textContent;
                var complete;
                if (todo.isComplete === "true") {
                    complete = "false";
                }
                else {
                    complete = "true";
                }

                var createRequest = new XMLHttpRequest();
                createRequest.open("PUT", "/api/todo/" + todo.id);
                createRequest.setRequestHeader("Content-type", "application/json");
                createRequest.send(JSON.stringify({
                    title: title,
                    id : todo.id,
                    isComplete: complete
                }));

                createRequest.onload = function() {
                    if (this.status === 200) {
                        reloadTodoList();
                    } else {
                        error.textContent = "Failed to update item. Server returned ";
                        error.textContent += this.status + " - " + this.responseText;
                    }
                };
            };

            todoList.appendChild(row);

        });
    //itemsLeft.appendChild(deleteAllButton);
    //todoList.appendChild(deleteAllButton);

    itemsLeft.textContent = "You have " + leftItems.toString();
    itemsLeft.textContent += " items left to complete out of " + totalItems.toString();

    });

}

reloadTodoList();
<|endoftext|>


<commit_before>import cache from './cache.js';
import {
	errorTpl,
	orgTpl,
	reposTpl,
	repoTpl,
	contributorsTpl,
} from './templates.js';

const API_URL = 'https://api.github.com';
const USE_CACHE = true;

const render = (el, data, tpl = errorTpl, append = false) => {
	const html = tpl(data);

	if (append) {
		el.insertAdjacentHTML('beforeend', html);
	} else {
		el.innerHTML = html;
	}

	return el;
};

// TODO: refactor into something more elegant.
// Luke, use the fetch!
const getJSON = (url, callback) => {
	const isError = Math.floor(Math.random() * 10) === 0; // 10% error chance

	if (isError) {
		return callback(new Error('Extreme network error!'));
	}

	const isCached = USE_CACHE && cache.get(url);

	if (isCached) {
		return callback(null, isCached);
	}

	fetch(url)
		.then((res) => {
			res.json()
				.then((json) => {
					if (USE_CACHE) {
						cache.set(url, json);
					}

					callback(null, json);
				});
		});
};

// IIFE to kick it all off
(() => {
	getJSON(`${API_URL}/orgs/vicompany`, (err, org) => {
		const el = document.querySelector('#org');

		if (err) {
			return render(el, err);
		}

		render(el, org, orgTpl);

		const { repos_url: reposUrl } = org;

		getJSON(reposUrl, (err, repos) => {
			const reposEl = document.querySelector('#repos');

			if (err) {
				return render(reposEl, { message: 'They took my repos. Dook err derr!' });
			}

			repos = repos
				.filter(r => r.fork === false)
				.sort(r => new Date(r.updated_at).getTime());

			render(reposEl, repos, reposTpl);
		});
	});

	document
		.querySelector('main')
		.addEventListener('click', (e) => {
			const { target } = e;
			const modal = document.querySelector('#modal');

			if (target.classList.contains('js-repo')) {
				e.preventDefault();

				getJSON(target.href, (err, repo) => {
					if (err) {
						return render(target, err, errorTpl);
					}

					render(modal, repo, repoTpl);

					modal.querySelector('dialog').showModal();
				});
			}

			if (target.classList.contains('js-contributors')) {
				e.preventDefault();

				getJSON(target.href, (err, contributors = []) => {
					if (err) {
						return render(target, err, errorTpl);
					}

					// TODO: retrieve user data!
					const data = {
						contributors,
						users: contributors.map(c => ({
							url: c.url,
							avatar: c.avatar_url,
							login: c.login,
						})),
					};

					render(modal, data, contributorsTpl);

					modal.querySelector('dialog').showModal();
				});
			}

			if (target.classList.contains('js-close')) {
				e.preventDefault();
				target.closest('dialog').close();
			}
		});
})();
<commit_msg>rename cached var<commit_after>import cache from './cache.js';
import {
	errorTpl,
	orgTpl,
	reposTpl,
	repoTpl,
	contributorsTpl,
} from './templates.js';

const API_URL = 'https://api.github.com';
const USE_CACHE = true;

const render = (el, data, tpl = errorTpl, append = false) => {
	const html = tpl(data);

	if (append) {
		el.insertAdjacentHTML('beforeend', html);
	} else {
		el.innerHTML = html;
	}

	return el;
};

// TODO: refactor into something more elegant.
// Luke, use the fetch!
const getJSON = (url, callback) => {
	const isError = Math.floor(Math.random() * 10) === 0; // 10% error chance

	if (isError) {
		return callback(new Error('Extreme network error!'));
	}

	const fromCache = USE_CACHE && cache.get(url);

	if (fromCache) {
		return callback(null, fromCache);
	}

	fetch(url)
		.then((res) => {
			res.json()
				.then((json) => {
					if (USE_CACHE) {
						cache.set(url, json);
					}

					callback(null, json);
				});
		});
};

// IIFE to kick it all off
(() => {
	getJSON(`${API_URL}/orgs/vicompany`, (err, org) => {
		const el = document.querySelector('#org');

		if (err) {
			return render(el, err);
		}

		render(el, org, orgTpl);

		const { repos_url: reposUrl } = org;

		getJSON(reposUrl, (err, repos) => {
			const reposEl = document.querySelector('#repos');

			if (err) {
				return render(reposEl, { message: 'They took my repos. Dook err derr!' });
			}

			repos = repos
				.filter(r => r.fork === false)
				.sort(r => new Date(r.updated_at).getTime());

			render(reposEl, repos, reposTpl);
		});
	});

	document
		.querySelector('main')
		.addEventListener('click', (e) => {
			const { target } = e;
			const modal = document.querySelector('#modal');

			if (target.classList.contains('js-repo')) {
				e.preventDefault();

				getJSON(target.href, (err, repo) => {
					if (err) {
						return render(target, err, errorTpl);
					}

					render(modal, repo, repoTpl);

					modal.querySelector('dialog').showModal();
				});
			}

			if (target.classList.contains('js-contributors')) {
				e.preventDefault();

				getJSON(target.href, (err, contributors = []) => {
					if (err) {
						return render(target, err, errorTpl);
					}

					// TODO: retrieve user data!
					const data = {
						contributors,
						users: contributors.map(c => ({
							url: c.url,
							avatar: c.avatar_url,
							login: c.login,
						})),
					};

					render(modal, data, contributorsTpl);

					modal.querySelector('dialog').showModal();
				});
			}

			if (target.classList.contains('js-close')) {
				e.preventDefault();
				target.closest('dialog').close();
			}
		});
})();
<|endoftext|>


<commit_before>'use strict';

const Joi = require('joi');
const Hoek = require('hoek');
const Async = require('async');
const XLSX = require('xlsx');

const internals = {
    routeMap: new Map()
};

exports.register = (server, options, next) => {

    internals.separator = options.separator || ',';
    internals.maximumElementsInArray = options.maximumElementsInArray || 5;
    internals.enableExcel = !!options.enableExcel;

    // Build up routeMap for all routes on all connections
    server.ext('onPreStart', (srv, nxt) => {

        srv.connections.forEach((connection) => {

            return connection.table().forEach((route) => {

                if (route.settings.response.schema) {
                    internals.routeMap.set(internals.createRouteMethodString(route.path, route.method), route.settings.response.schema);
                }
            });
        });

        return nxt();
    });

    // allow .csv requests
    server.ext('onRequest', (request, reply) => {

        const path = request.path;

        if (path.endsWith('.csv')) {
            request.setUrl(`${path.substring(0, path.length - 4)}${request.url.search ? request.url.search : ''}`);
            request.headers.accept = 'text/csv';
        }

        if (internals.enableExcel && path.endsWith('.xlsx')) {
            request.setUrl(`${path.substring(0, path.length - 5)}${request.url.search ? request.url.search : ''}`);
            request.headers.accept = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
        }

        return reply.continue();
    });

    // Check for header and route mapping and convert and reply with csv if needed
    const allowedTypesRegex = /(text\/csv)|(application\/csv)|(application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet)/i;

    server.ext('onPreResponse', (request, reply) => {

        //todo
        if (request.response.isBoom) {
            return reply.continue();
        }

        if (!request.headers.accept) {
            return reply.continue();
        }

        const result = allowedTypesRegex.exec(request.headers.accept);
        const preferredType = result && result[0];

        if (!internals.enableExcel && preferredType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
            return reply.continue();
        }

        if (!(preferredType && internals.routeMap.has(internals.createRouteMethodString(request.route.path, request.route.method)))) {
            return reply.continue();
        }

        const dynamicHandlerObject = request.route.settings.plugins['hapi-csv'] || {};

        const resolvedSchemasObject = {};

        return Async.forEachOf(dynamicHandlerObject, (handler, path, callback) => {

            return handler(request, (err, dynamicSchema) => {

                if (err) {
                    return callback(err);
                }

                resolvedSchemasObject[path] = dynamicSchema;

                return callback();
            });
        }, (err) => {

            if (err) {
                return reply(err);
            }

            const schema = internals.routeMap.get(internals.createRouteMethodString(request.route.path, request.route.method));

            if (internals.enableExcel && preferredType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
                const fileName = request.path.substring(request.path.lastIndexOf('/') + 1 || 0, request.path.length);
                const xlsx = internals.processSchema(schema, resolvedSchemasObject, request.response.source, options.resultKey, internals.headerQueryMapToExcel);
                const excel = XLSX.write(xlsx, { bookType:'xlsx', bookSST:true, type: 'base64' });
                const response = reply(excel);
                return response.encoding('base64').type(`${preferredType}; charset=${response.settings.charset}; header=present;`).header(`'content-disposition', 'attachment'; filename=${fileName}.xlsx;`).header('Content-Length', excel.length);
            }

            const csv = internals.processSchema(schema, resolvedSchemasObject, request.response.source, options.resultKey, internals.headerQueryMapToCsv);

            const response = reply(csv);
            return response.type(`${preferredType}; charset=${response.settings.charset}; header=present;`).header('content-disposition', 'attachment;');
        });
    });

    return next();
};

exports.register.attributes = {
    pkg: require('../package.json'),
    once: true
};

internals.processSchema = (schema, dynamicSchemas, dataset, resultKey, callback) => {

    // We return the dataset if the dataset is not an array or an object, just a primitive type
    if (!(Array.isArray(dataset)) && !(dataset === Object(dataset))) {
        return internals.escapeQuotesInString(dataset);
    }

    let schemaDescription = Joi.compile(schema).describe();

    if (schemaDescription.children && schemaDescription.children[resultKey]) {
        schemaDescription = schemaDescription.children[resultKey];
        dataset = dataset[resultKey];
    }

    const headerQueryArray = internals.parseSchema(schemaDescription, dynamicSchemas);
    const headerQueryMap = internals.arrayToMap(headerQueryArray);

    return callback(headerQueryMap, dataset);
};

/*
 * Recursive function which parses a Joi schema to an array of header, query entries
 * `parentIsArray` is true when the previous call to the function handled a joi schema of type array
 */
internals.parseSchema = (joiSchema, dynamicSchemas, keyName, path, parentIsArray) => {

    if (dynamicSchemas[path]) {
        joiSchema =  Joi.compile(dynamicSchemas[path]).describe();
    }

    if (joiSchema.type === 'object') {

        if (joiSchema.children) {
            const childrenKeys = Object.keys(joiSchema.children);
            let children = childrenKeys.map((key) => internals.parseSchema(joiSchema.children[key], dynamicSchemas, key, path !== undefined ? `${path}.${key}` : key));

            if (!keyName) {
                return children;
            }

            children = Hoek.flatten(children);

            return children.map((child) => {

                const key = Object.keys(child)[0];
                child[key].unshift(keyName);

                // If the previous call was not for a joi schema of type array, we alter the keys to prefix them with the keyName
                if (!parentIsArray) {
                    const name = `${keyName}.${key}`;
                    child[name] = child[key];

                    delete child[key];
                }

                return child;
            });
        }

        return false;
    }

    if (joiSchema.type === 'array') {

        // FUTURE: Make it work for arrays with multiple item definitions/alternatives
        const item = joiSchema.items[0];
        const parsedItem = internals.parseSchema(item, dynamicSchemas, keyName, path, true);

        if (keyName && parsedItem) {
            const prefixedItemArray = [];

            for (let i = 0; i < internals.maximumElementsInArray; ++i) {

                prefixedItemArray.push(parsedItem.map((headerQuery) => {

                    const key = Object.keys(headerQuery)[0];
                    const name = parsedItem.length === 1 ? `${keyName}_${i}` : `${keyName}_${i}.${key}`;
                    const sliced = headerQuery[key].slice();

                    // We splice the index after the array key
                    sliced.splice(1, 0, i);

                    return { [name]: sliced };
                }));
            }

            return prefixedItemArray;
        }

        return parsedItem;
    }

    // First square brackets are used to convert to String, second brackets are used to convert to an array
    return [{ [keyName]: [keyName] }];
};

// Utility function to convert an array to a Map
internals.arrayToMap = (array) => {

    const resultMap = new Map();

    Hoek.flatten(array).forEach((item) => {

        const key = Object.keys(item)[0];

        if (key !== undefined) {
            resultMap.set(key, item[key]);
        }
    });

    return resultMap;
};

internals.headerQueryMapToCsv = (headerQueryMap, dataset) => {

    let headerRow = '';

    for (const header of headerQueryMap.keys()) {
        headerRow += `${header}${internals.separator}`;
    }

    let csv = `${headerRow}\n`;

    if (!Array.isArray(dataset)) {
        dataset = [dataset];
    }

    let valueFound = true;

    for (let i = 0; i < dataset.length; ++i) {
        let dataRow = '';

        for (const query of headerQueryMap.values()) {
            let temp = dataset[i];

            for (const queryPart of query) {
                if (temp[queryPart] === null || temp[queryPart] === undefined) {
                    valueFound = false;

                    // We break out of the for loop because there is no need to dig deeper into the object, the object is already undefined or null
                    break;
                }
                else {
                    temp = temp[queryPart];
                }
            }

            if (!valueFound) {
                valueFound = true;
            }
            else {
                temp = internals.dateToISOString(temp);
                dataRow += `"${internals.escapeQuotesInString(temp)}"`;
            }

            dataRow += internals.separator;
        }

        csv += `${dataRow}\n`;
    }

    return csv.trim();
};

internals.headerQueryMapToExcel = (headerQueryMap, dataset) => {

    const dataRows = [];

    if (!Array.isArray(dataset)) {
        dataset = [dataset];
    }

    for (let i = 0; i < dataset.length; ++i) {
        const dataRow = {};

        for (const key of headerQueryMap.keys()) {

            const query = headerQueryMap.get(key);
            let temp = dataset[i];

            for (const queryPart of query) {
                temp = temp[queryPart];
            }

            temp = internals.dateToISOString(temp);
            dataRow[key] = internals.escapeQuotesInString(temp);
        }

        dataRows.push(dataRow);
    }

    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(dataRows, headerQueryMap.keys()));
    return wb;
};

internals.escapeQuotesInString = (str) => {

    if (typeof str === 'string' || str instanceof String) {
        return str.replace(/"/g, '""');
    }

    return str;
};

internals.dateToISOString = (str) => {

    if (str instanceof Date) {
        return str.toISOString();
    }

    return str;
};

internals.createRouteMethodString = (route, method) => {

    return `${route}_${method}`;
};
<commit_msg>fix(excel): add extra check for empty object<commit_after>'use strict';

const Joi = require('joi');
const Hoek = require('hoek');
const Async = require('async');
const XLSX = require('xlsx');

const internals = {
    routeMap: new Map()
};

exports.register = (server, options, next) => {

    internals.separator = options.separator || ',';
    internals.maximumElementsInArray = options.maximumElementsInArray || 5;
    internals.enableExcel = !!options.enableExcel;

    // Build up routeMap for all routes on all connections
    server.ext('onPreStart', (srv, nxt) => {

        srv.connections.forEach((connection) => {

            return connection.table().forEach((route) => {

                if (route.settings.response.schema) {
                    internals.routeMap.set(internals.createRouteMethodString(route.path, route.method), route.settings.response.schema);
                }
            });
        });

        return nxt();
    });

    // allow .csv requests
    server.ext('onRequest', (request, reply) => {

        const path = request.path;

        if (path.endsWith('.csv')) {
            request.setUrl(`${path.substring(0, path.length - 4)}${request.url.search ? request.url.search : ''}`);
            request.headers.accept = 'text/csv';
        }

        if (internals.enableExcel && path.endsWith('.xlsx')) {
            request.setUrl(`${path.substring(0, path.length - 5)}${request.url.search ? request.url.search : ''}`);
            request.headers.accept = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
        }

        return reply.continue();
    });

    // Check for header and route mapping and convert and reply with csv if needed
    const allowedTypesRegex = /(text\/csv)|(application\/csv)|(application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet)/i;

    server.ext('onPreResponse', (request, reply) => {

        //todo
        if (request.response.isBoom) {
            return reply.continue();
        }

        if (!request.headers.accept) {
            return reply.continue();
        }

        const result = allowedTypesRegex.exec(request.headers.accept);
        const preferredType = result && result[0];

        if (!internals.enableExcel && preferredType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
            return reply.continue();
        }

        if (!(preferredType && internals.routeMap.has(internals.createRouteMethodString(request.route.path, request.route.method)))) {
            return reply.continue();
        }

        const dynamicHandlerObject = request.route.settings.plugins['hapi-csv'] || {};

        const resolvedSchemasObject = {};

        return Async.forEachOf(dynamicHandlerObject, (handler, path, callback) => {

            return handler(request, (err, dynamicSchema) => {

                if (err) {
                    return callback(err);
                }

                resolvedSchemasObject[path] = dynamicSchema;

                return callback();
            });
        }, (err) => {

            if (err) {
                return reply(err);
            }

            const schema = internals.routeMap.get(internals.createRouteMethodString(request.route.path, request.route.method));

            if (internals.enableExcel && preferredType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
                const fileName = request.path.substring(request.path.lastIndexOf('/') + 1 || 0, request.path.length);
                const xlsx = internals.processSchema(schema, resolvedSchemasObject, request.response.source, options.resultKey, internals.headerQueryMapToExcel);
                const excel = XLSX.write(xlsx, { bookType:'xlsx', bookSST:true, type: 'base64' });
                const response = reply(excel);
                return response.encoding('base64').type(`${preferredType}; charset=${response.settings.charset}; header=present;`).header(`'content-disposition', 'attachment'; filename=${fileName}.xlsx;`).header('Content-Length', excel.length);
            }

            const csv = internals.processSchema(schema, resolvedSchemasObject, request.response.source, options.resultKey, internals.headerQueryMapToCsv);

            const response = reply(csv);
            return response.type(`${preferredType}; charset=${response.settings.charset}; header=present;`).header('content-disposition', 'attachment;');
        });
    });

    return next();
};

exports.register.attributes = {
    pkg: require('../package.json'),
    once: true
};

internals.processSchema = (schema, dynamicSchemas, dataset, resultKey, callback) => {

    // We return the dataset if the dataset is not an array or an object, just a primitive type
    if (!(Array.isArray(dataset)) && !(dataset === Object(dataset))) {
        return internals.escapeQuotesInString(dataset);
    }

    let schemaDescription = Joi.compile(schema).describe();

    if (schemaDescription.children && schemaDescription.children[resultKey]) {
        schemaDescription = schemaDescription.children[resultKey];
        dataset = dataset[resultKey];
    }

    const headerQueryArray = internals.parseSchema(schemaDescription, dynamicSchemas);
    const headerQueryMap = internals.arrayToMap(headerQueryArray);

    return callback(headerQueryMap, dataset);
};

/*
 * Recursive function which parses a Joi schema to an array of header, query entries
 * `parentIsArray` is true when the previous call to the function handled a joi schema of type array
 */
internals.parseSchema = (joiSchema, dynamicSchemas, keyName, path, parentIsArray) => {

    if (dynamicSchemas[path]) {
        joiSchema =  Joi.compile(dynamicSchemas[path]).describe();
    }

    if (joiSchema.type === 'object') {

        if (joiSchema.children) {
            const childrenKeys = Object.keys(joiSchema.children);
            let children = childrenKeys.map((key) => internals.parseSchema(joiSchema.children[key], dynamicSchemas, key, path !== undefined ? `${path}.${key}` : key));

            if (!keyName) {
                return children;
            }

            children = Hoek.flatten(children);

            return children.map((child) => {

                const key = Object.keys(child)[0];
                child[key].unshift(keyName);

                // If the previous call was not for a joi schema of type array, we alter the keys to prefix them with the keyName
                if (!parentIsArray) {
                    const name = `${keyName}.${key}`;
                    child[name] = child[key];

                    delete child[key];
                }

                return child;
            });
        }

        return false;
    }

    if (joiSchema.type === 'array') {

        // FUTURE: Make it work for arrays with multiple item definitions/alternatives
        const item = joiSchema.items[0];
        const parsedItem = internals.parseSchema(item, dynamicSchemas, keyName, path, true);

        if (keyName && parsedItem) {
            const prefixedItemArray = [];

            for (let i = 0; i < internals.maximumElementsInArray; ++i) {

                prefixedItemArray.push(parsedItem.map((headerQuery) => {

                    const key = Object.keys(headerQuery)[0];
                    const name = parsedItem.length === 1 ? `${keyName}_${i}` : `${keyName}_${i}.${key}`;
                    const sliced = headerQuery[key].slice();

                    // We splice the index after the array key
                    sliced.splice(1, 0, i);

                    return { [name]: sliced };
                }));
            }

            return prefixedItemArray;
        }

        return parsedItem;
    }

    // First square brackets are used to convert to String, second brackets are used to convert to an array
    return [{ [keyName]: [keyName] }];
};

// Utility function to convert an array to a Map
internals.arrayToMap = (array) => {

    const resultMap = new Map();

    Hoek.flatten(array).forEach((item) => {

        const key = Object.keys(item)[0];

        if (key !== undefined) {
            resultMap.set(key, item[key]);
        }
    });

    return resultMap;
};

internals.headerQueryMapToCsv = (headerQueryMap, dataset) => {

    let headerRow = '';

    for (const header of headerQueryMap.keys()) {
        headerRow += `${header}${internals.separator}`;
    }

    let csv = `${headerRow}\n`;

    if (!Array.isArray(dataset)) {
        dataset = [dataset];
    }

    let valueFound = true;

    for (let i = 0; i < dataset.length; ++i) {
        let dataRow = '';

        for (const query of headerQueryMap.values()) {
            let temp = dataset[i];

            for (const queryPart of query) {
                if (temp[queryPart] === null || temp[queryPart] === undefined) {
                    valueFound = false;

                    // We break out of the for loop because there is no need to dig deeper into the object, the object is already undefined or null
                    break;
                }
                else {
                    temp = temp[queryPart];
                }
            }

            if (!valueFound) {
                valueFound = true;
            }
            else {
                temp = internals.dateToISOString(temp);
                dataRow += `"${internals.escapeQuotesInString(temp)}"`;
            }

            dataRow += internals.separator;
        }

        csv += `${dataRow}\n`;
    }

    return csv.trim();
};

internals.headerQueryMapToExcel = (headerQueryMap, dataset) => {

    const dataRows = [];

    if (!Array.isArray(dataset)) {
        dataset = [dataset];
    }

    for (let i = 0; i < dataset.length; ++i) {
        const dataRow = {};

        for (const key of headerQueryMap.keys()) {

            const query = headerQueryMap.get(key);
            let temp = dataset[i];

            if (temp) {
                for (const queryPart of query) {
                    temp = temp[queryPart];
                }

                temp = internals.dateToISOString(temp);
            }

            dataRow[key] = internals.escapeQuotesInString(temp);
        }

        dataRows.push(dataRow);
    }

    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(dataRows, headerQueryMap.keys()));
    return wb;
};

internals.escapeQuotesInString = (str) => {

    if (typeof str === 'string' || str instanceof String) {
        return str.replace(/"/g, '""');
    }

    return str;
};

internals.dateToISOString = (str) => {

    if (str instanceof Date) {
        return str.toISOString();
    }

    return str;
};

internals.createRouteMethodString = (route, method) => {

    return `${route}_${method}`;
};
<|endoftext|>


<commit_before>import L from 'leaflet'
import 'leaflet/dist/leaflet.css!'
import 'leaflet-loading'
import 'leaflet-loading/src/Control.Loading.css!'

import './leaflet-singleclick.js'

import * as CovJSON from 'covjson-reader'
import * as RestAPI from 'coverage-rest-client'
import * as C from 'leaflet-coverage'
import * as CovUtils from 'covutils'

import 'c3/c3.css!'

import CodeMirror from 'codemirror'

import FileMenu from './FileMenu.js'
import Editor from './Editor.js'

import './style.css!'

let mapEl = document.getElementsByClassName('map')[0]
let map = L.map(mapEl, {
  loadingControl: true,
  // initial center and zoom has to be set before layers can be added
  center: [10, 0],
  zoom: 2
})

L.control.scale().addTo(map)

let baseLayers = {
  'OSM':
    L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
       attribution: 'Map data © OpenStreetMap'
    })
}
baseLayers['OSM'].addTo(map)

let layerControl = L.control.layers([], [], {collapsed: false}).addTo(map)

// We use ParameterSync here so that multiple coverage layers that display the same
// parameter get synchronized in terms of their palette and extent.
// It also allows us to display a single legend only.
// Layers that don't have a single parameter get ignored automatically.
let paramSync = new C.ParameterSync({
  syncProperties: {
    palette: (p1, p2) => p1,
    paletteExtent: (e1, e2) => e1 && e2 ? [Math.min(e1[0], e2[0]), Math.max(e1[1], e2[1])] : null
  }
}).on('parameterAdd', e => {
    // The virtual sync layer proxies the synced palette, paletteExtent, and parameter.
    // The sync layer will fire a 'remove' event if all real layers for that parameter were removed.
    let layer = e.syncLayer
    if (layer.palette) {
      C.legend(layer, {
        position: 'bottomright'
      }).addTo(map)
    }
  })

let layersInControl = new Set()
let coverageLayersOnMap = new Set()

function removeLayers () {
  for (let layer of layersInControl) {
    layerControl.removeLayer(layer)
    if (map.hasLayer(layer)) {
      // FIXME leaflet's internal state breaks if layers or controls throw exceptions in onAdd()
      // -> could be prevented by linting CovJSON before-hand
      try {
        map.removeLayer(layer)
      } catch (e) {}
    }
  }
  layersInControl = new Set()
}

function loadCov (url, options = {}) {
  removeLayers()

  map.fire('dataloading')
  CovJSON.read(url)
    .then(cov => RestAPI.wrap(cov, {loader: CovJSON.read}))
    .then(cov => {

    if (CovUtils.isDomain(cov)) {
      cov = CovUtils.fromDomain(cov)
    }

    map.fire('dataload')
    console.log('Coverage loaded: ', cov)

    // add each parameter as a layer
    let firstLayer

    let layerClazz = C.dataLayerClass(cov)

    if (cov.coverages && !layerClazz) {
      // generic collection
      if (!cov.parameters) {
        throw new Error('Only coverage collections with a "parameters" property are supported')
      }

      for (let key of cov.parameters.keys()) {
        let layers = cov.coverages
          .filter(coverage => coverage.parameters.has(key))
          .map(coverage => createLayer(coverage, {keys: [key]}))
        layers.forEach(covlayer => map.fire('covlayercreate', {layer: covlayer}))
        let layer = L.layerGroup(layers)
        layersInControl.add(layer)

        layerControl.addOverlay(layer, key)
        if (!firstLayer) {
          firstLayer = layer

          // the following piece of code should be easier
          // TODO extend layer group class in leaflet-coverage (like PointCollection) to provide single 'add' event
          let addCount = 0
          for (let l of layers) {
            l.on('add', () => {
              coverageLayersOnMap.add(l)
              ++addCount
              if (addCount === layers.length) {
                zoomToLayers(layers)
                // FIXME is this the right place?? define event semantics!
                map.fire('covlayeradd', {layer: l})
              }
            })
          }
        }
      }
    } else if (layerClazz) {
      // single coverage or a coverage collection of a specific domain type

      for (let key of cov.parameters.keys()) {
        let opts = {keys: [key]}
        let layer = createLayer(cov, opts)
        map.fire('covlayercreate', {layer})
        layersInControl.add(layer)

        layerControl.addOverlay(layer, key)
        if (!firstLayer) {
          firstLayer = layer
          layer.on('add', () => {
            zoomToLayers([layer])
            if (!cov.coverages) {
              if (isVerticalProfile(cov) || isTimeSeries(cov)) {
                layer.openPopup()
              }
            }
          })
        }
        layer.on('add', () => {
          coverageLayersOnMap.add(layer)
          map.fire('covlayeradd', {layer})
        }).on('remove', () => {
          coverageLayersOnMap.delete(layer)
          map.fire('covlayerremove', {layer})
        })
      }
    } else {
      throw new Error('unsupported type')
    }
    if (options.display && firstLayer) {
      map.addLayer(firstLayer)
    }
    editor.clearErrors()
  }).catch(e => {
    map.fire('dataload')
    console.log(e)
    editor.addError(e.message)
  })
}

function zoomToLayers (layers) {
  let bnds = layers.map(l => l.getBounds())
  let bounds = L.latLngBounds(bnds)
  let opts
  if (bounds.getWest() === bounds.getEast() && bounds.getSouth() === bounds.getNorth()) {
    opts = { maxZoom: 5 }
  }
  map.fitBounds(bounds, opts)
}

function isVerticalProfile (cov) {
  return cov.domainType === C.COVJSON_VERTICALPROFILE
}

function isTimeSeries (cov) {
  return cov.domainType === C.COVJSON_POINTSERIES || cov.domainType === C.COVJSON_POLYGONSERIES
}

function createLayer(cov, opts) {
  let layer = C.dataLayer(cov, opts).on('add', e => {
    let covLayer = e.target
    console.log('layer added:', covLayer)

    // This registers the layer with the sync manager.
    // By doing that, the palette and extent get unified (if existing)
    // and an event gets fired if a new parameter was added.
    // See the code above where ParameterSync gets instantiated.
    paramSync.addLayer(covLayer)

    if (!cov.coverages) {
      if (covLayer.time) {
        new C.TimeAxis(covLayer).addTo(map)
      }
      if (covLayer.vertical) {
        new C.VerticalAxis(covLayer).addTo(map)
      }
    }
  }).on('dataLoading', () => map.fire('dataloading'))
    .on('dataLoad', () => map.fire('dataload'))

  if (cov.coverages) {
    if (isVerticalProfile(cov)) {
      layer.bindPopupEach(coverage => new C.VerticalProfilePlot(coverage))
    } else if (isTimeSeries(cov)) {
      layer.bindPopupEach(coverage => new C.TimeSeriesPlot(coverage))
    }
  } else {
    if (isVerticalProfile(cov)) {
      layer.bindPopup(new C.VerticalProfilePlot(cov))
    } else if (isTimeSeries(cov)) {
      layer.bindPopup(new C.TimeSeriesPlot(cov))
    }
  }

  return layer
}

let examples = [{
  title: 'Grid',
  url: 'coverages/grid.covjson'
}, {
  title: 'Grid (Categorical)',
  url: 'coverages/grid-categorical.covjson'
}, {
  title: 'Grid (Tiled)',
  url: 'coverages/grid-tiled.covjson'
}, {
  title: 'Trajectory',
  url: 'coverages/trajectory.covjson'
}, {
  title: 'Profile',
  url: 'coverages/profile.covjson'
}, {
  title: 'PointSeries',
  url: 'coverages/pointseries.covjson'
}, {
  title: 'Point',
  url: 'coverages/point.covjson'
}, {
  title: 'Point Collection',
  url: 'coverages/point-collection.covjson'
}, {
  title: 'Profile Collection',
  url: 'coverages/profile-collection.covjson'
}, {
  title: 'MultiPolygon',
  url: 'coverages/multipolygon.covjson'
}, {
  title: 'PolygonSeries',
  url: 'coverages/polygonseries.covjson'
}, {
  title: 'Grid (Domain)',
  url: 'coverages/grid-domain.covjson'
}]

let editor = new Editor({
  container: document.getElementsByClassName('right')[0]
}).on('change', e => {
  loadCov(e.obj, {display: true})
}).on('resize', () => {
  map.invalidateSize()
})

function loadFromHash () {
  let url = window.location.hash.substr(1)
  editor.load(url)
}

if (window.location.hash) {
  loadFromHash()
} else {
  editor.json = '{}'
}

window.addEventListener("hashchange", loadFromHash, false)

new FileMenu({
  container: document.getElementsByClassName('file-bar')[0],
  examples
}).on('requestload', ({url}) => editor.load(url))

window.api = {
    map,
    cm: editor.cm,
    CodeMirror,
    layers: coverageLayersOnMap
}

// Wire up coverage value popup
let valuePopup
function openValuePopup (latlng) {
  valuePopup = new C.DraggableValuePopup({
    className: 'leaflet-popup-draggable',
    layers: [...coverageLayersOnMap]
  }).setLatLng(latlng)
    .openOn(map)
}

map.on('singleclick', e => openValuePopup(e.latlng))
map.on('covlayercreate', createEvent => {
  createEvent.layer.on('click', clickEvent => {
    openValuePopup(clickEvent.latlng)
  })
})
map.on('covlayeradd', e => {
  if (valuePopup) {
    valuePopup.addCoverageLayer(e.layer)
  }
})
map.on('covlayerremove', e => {
  if (valuePopup) {
    valuePopup.removeCoverageLayer(e.layer)
  }
})

<commit_msg>popup bugfix<commit_after>import L from 'leaflet'
import 'leaflet/dist/leaflet.css!'
import 'leaflet-loading'
import 'leaflet-loading/src/Control.Loading.css!'

import './leaflet-singleclick.js'

import * as CovJSON from 'covjson-reader'
import * as RestAPI from 'coverage-rest-client'
import * as C from 'leaflet-coverage'
import * as CovUtils from 'covutils'

import 'c3/c3.css!'

import CodeMirror from 'codemirror'

import FileMenu from './FileMenu.js'
import Editor from './Editor.js'

import './style.css!'

let mapEl = document.getElementsByClassName('map')[0]
let map = L.map(mapEl, {
  loadingControl: true,
  // initial center and zoom has to be set before layers can be added
  center: [10, 0],
  zoom: 2
})

L.control.scale().addTo(map)

let baseLayers = {
  'OSM':
    L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
       attribution: 'Map data © OpenStreetMap'
    })
}
baseLayers['OSM'].addTo(map)

let layerControl = L.control.layers([], [], {collapsed: false}).addTo(map)

// We use ParameterSync here so that multiple coverage layers that display the same
// parameter get synchronized in terms of their palette and extent.
// It also allows us to display a single legend only.
// Layers that don't have a single parameter get ignored automatically.
let paramSync = new C.ParameterSync({
  syncProperties: {
    palette: (p1, p2) => p1,
    paletteExtent: (e1, e2) => e1 && e2 ? [Math.min(e1[0], e2[0]), Math.max(e1[1], e2[1])] : null
  }
}).on('parameterAdd', e => {
    // The virtual sync layer proxies the synced palette, paletteExtent, and parameter.
    // The sync layer will fire a 'remove' event if all real layers for that parameter were removed.
    let layer = e.syncLayer
    if (layer.palette) {
      C.legend(layer, {
        position: 'bottomright'
      }).addTo(map)
    }
  })

let layersInControl = new Set()
let coverageLayersOnMap = new Set()

function removeLayers () {
  for (let layer of layersInControl) {
    layerControl.removeLayer(layer)
    if (map.hasLayer(layer)) {
      // FIXME leaflet's internal state breaks if layers or controls throw exceptions in onAdd()
      // -> could be prevented by linting CovJSON before-hand
      try {
        map.removeLayer(layer)
      } catch (e) {}
    }
  }
  layersInControl = new Set()
}

function loadCov (url, options = {}) {
  removeLayers()

  map.fire('dataloading')
  CovJSON.read(url)
    .then(cov => RestAPI.wrap(cov, {loader: CovJSON.read}))
    .then(cov => {

    if (CovUtils.isDomain(cov)) {
      cov = CovUtils.fromDomain(cov)
    }

    map.fire('dataload')
    console.log('Coverage loaded: ', cov)

    // add each parameter as a layer
    let firstLayer

    let layerClazz = C.dataLayerClass(cov)

    if (cov.coverages && !layerClazz) {
      // generic collection
      if (!cov.parameters) {
        throw new Error('Only coverage collections with a "parameters" property are supported')
      }

      for (let key of cov.parameters.keys()) {
        let layers = cov.coverages
          .filter(coverage => coverage.parameters.has(key))
          .map(coverage => createLayer(coverage, {keys: [key]}))
        layers.forEach(covlayer => map.fire('covlayercreate', {layer: covlayer}))
        let layer = L.layerGroup(layers)
        layersInControl.add(layer)

        layerControl.addOverlay(layer, key)
        if (!firstLayer) {
          firstLayer = layer

          // the following piece of code should be easier
          // TODO extend layer group class in leaflet-coverage (like PointCollection) to provide single 'add' event
          let addCount = 0
          for (let l of layers) {
            l.on('add', () => {
              coverageLayersOnMap.add(l)
              ++addCount
              if (addCount === layers.length) {
                zoomToLayers(layers)
                // FIXME is this the right place?? define event semantics!
                map.fire('covlayeradd', {layer: l})
              }
            })
          }
        }
      }
    } else if (layerClazz) {
      // single coverage or a coverage collection of a specific domain type

      for (let key of cov.parameters.keys()) {
        let opts = {keys: [key]}
        let layer = createLayer(cov, opts)
        map.fire('covlayercreate', {layer})
        layersInControl.add(layer)

        layerControl.addOverlay(layer, key)
        if (!firstLayer) {
          firstLayer = layer
          layer.on('add', () => {
            zoomToLayers([layer])
            if (!cov.coverages) {
              if (isVerticalProfile(cov) || isTimeSeries(cov)) {
                layer.openPopup()
              }
            }
          })
        }
        layer.on('add', () => {
          coverageLayersOnMap.add(layer)
          map.fire('covlayeradd', {layer})
        }).on('remove', () => {
          coverageLayersOnMap.delete(layer)
          map.fire('covlayerremove', {layer})
        })
      }
    } else {
      throw new Error('unsupported type')
    }
    if (options.display && firstLayer) {
      map.addLayer(firstLayer)
    }
    editor.clearErrors()
  }).catch(e => {
    map.fire('dataload')
    console.log(e)
    editor.addError(e.message)
  })
}

function zoomToLayers (layers) {
  let bnds = layers.map(l => l.getBounds())
  let bounds = L.latLngBounds(bnds)
  let opts
  if (bounds.getWest() === bounds.getEast() && bounds.getSouth() === bounds.getNorth()) {
    opts = { maxZoom: 5 }
  }
  map.fitBounds(bounds, opts)
}

function isVerticalProfile (cov) {
  return cov.domainType === C.COVJSON_VERTICALPROFILE
}

function isTimeSeries (cov) {
  return cov.domainType === C.COVJSON_POINTSERIES || cov.domainType === C.COVJSON_POLYGONSERIES
}

function createLayer(cov, opts) {
  let layer = C.dataLayer(cov, opts).on('add', e => {
    let covLayer = e.target
    console.log('layer added:', covLayer)

    // This registers the layer with the sync manager.
    // By doing that, the palette and extent get unified (if existing)
    // and an event gets fired if a new parameter was added.
    // See the code above where ParameterSync gets instantiated.
    paramSync.addLayer(covLayer)

    if (!cov.coverages) {
      if (covLayer.time) {
        new C.TimeAxis(covLayer).addTo(map)
      }
      if (covLayer.vertical) {
        new C.VerticalAxis(covLayer).addTo(map)
      }
    }
  }).on('dataLoading', () => map.fire('dataloading'))
    .on('dataLoad', () => map.fire('dataload'))

  if (cov.coverages) {
    if (isVerticalProfile(cov)) {
      layer.bindPopupEach(coverage => new C.VerticalProfilePlot(coverage))
    } else if (isTimeSeries(cov)) {
      layer.bindPopupEach(coverage => new C.TimeSeriesPlot(coverage))
    }
  } else {
    if (isVerticalProfile(cov)) {
      layer.bindPopup(new C.VerticalProfilePlot(cov))
    } else if (isTimeSeries(cov)) {
      layer.bindPopup(new C.TimeSeriesPlot(cov))
    }
  }

  return layer
}

let examples = [{
  title: 'Grid',
  url: 'coverages/grid.covjson'
}, {
  title: 'Grid (Categorical)',
  url: 'coverages/grid-categorical.covjson'
}, {
  title: 'Grid (Tiled)',
  url: 'coverages/grid-tiled.covjson'
}, {
  title: 'Trajectory',
  url: 'coverages/trajectory.covjson'
}, {
  title: 'Profile',
  url: 'coverages/profile.covjson'
}, {
  title: 'PointSeries',
  url: 'coverages/pointseries.covjson'
}, {
  title: 'Point',
  url: 'coverages/point.covjson'
}, {
  title: 'Point Collection',
  url: 'coverages/point-collection.covjson'
}, {
  title: 'Profile Collection',
  url: 'coverages/profile-collection.covjson'
}, {
  title: 'MultiPolygon',
  url: 'coverages/multipolygon.covjson'
}, {
  title: 'PolygonSeries',
  url: 'coverages/polygonseries.covjson'
}, {
  title: 'Grid (Domain)',
  url: 'coverages/grid-domain.covjson'
}]

let editor = new Editor({
  container: document.getElementsByClassName('right')[0]
}).on('change', e => {
  loadCov(e.obj, {display: true})
}).on('resize', () => {
  map.invalidateSize()
})

function loadFromHash () {
  let url = window.location.hash.substr(1)
  editor.load(url)
}

if (window.location.hash) {
  loadFromHash()
} else {
  editor.json = '{}'
}

window.addEventListener("hashchange", loadFromHash, false)

new FileMenu({
  container: document.getElementsByClassName('file-bar')[0],
  examples
}).on('requestload', ({url}) => editor.load(url))

window.api = {
    map,
    cm: editor.cm,
    CodeMirror,
    layers: coverageLayersOnMap
}

// Wire up coverage value popup
let valuePopup
function openValuePopup (latlng) {
  valuePopup = new C.DraggableValuePopup({
    className: 'leaflet-popup-draggable',
    layers: [...coverageLayersOnMap]
  }).setLatLng(latlng)
    .openOn(map)
}

map.on('singleclick', e => openValuePopup(e.latlng))
map.on('covlayercreate', createEvent => {
  let layer = createEvent.layer
  // TODO can we make this more clever?
  if (layer instanceof C.VerticalProfileCollection) {
    // don't show a value popup as we already show a plot popup
    return
  }
  layer.on('click', clickEvent => {
    openValuePopup(clickEvent.latlng)
  })
})
map.on('covlayeradd', e => {
  if (valuePopup) {
    valuePopup.addCoverageLayer(e.layer)
  }
})
map.on('covlayerremove', e => {
  if (valuePopup) {
    valuePopup.removeCoverageLayer(e.layer)
  }
})

<|endoftext|>