diff --git a/app/assets/javascripts/blob/xlsx/components/table.vue b/app/assets/javascripts/blob/xlsx/components/table.vue
new file mode 100644
index 0000000000000000000000000000000000000000..81227fefaea8071f193d92338da4b0786771f72d
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx/components/table.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/blob/xlsx/components/tabs.vue b/app/assets/javascripts/blob/xlsx/components/tabs.vue
new file mode 100644
index 0000000000000000000000000000000000000000..88375615e1715de788525d767e6234991d4d70ed
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx/components/tabs.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/blob/xlsx/eventhub.js b/app/assets/javascripts/blob/xlsx/eventhub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/blob/xlsx/index.vue b/app/assets/javascripts/blob/xlsx/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d841066cc87ffd92b94447d21299c4b79874f47a
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx/index.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
diff --git a/app/assets/javascripts/blob/xlsx/service.js b/app/assets/javascripts/blob/xlsx/service.js
new file mode 100644
index 0000000000000000000000000000000000000000..9135e8edc58aa346a01bdc944779379bb3ca0a00
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx/service.js
@@ -0,0 +1,93 @@
+/* eslint-disable class-methods-use-this */
+import xlsx from 'xlsx';
+
+export default class XlsxService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ getData() {
+ return this.loadFile()
+ .then(workbook => this.processWorkbook(workbook));
+ }
+
+ loadFile() {
+ return new Promise((resolve) => {
+ const request = new XMLHttpRequest();
+ request.open('GET', this.endpoint, true);
+ request.responseType = 'arraybuffer';
+
+ request.onload = () => {
+ const arraybuffer = request.response;
+ const data = new Uint8Array(arraybuffer);
+ const arr = [];
+ data.forEach((d) => {
+ arr.push(String.fromCharCode(d));
+ });
+ const bstr = arr.join('');
+ const workbook = xlsx.read(bstr, {
+ type: 'binary',
+ });
+
+ resolve(workbook);
+ };
+
+ request.send();
+ });
+ }
+
+ processWorkbook(workbook) {
+ return new Promise((resolve) => {
+ const sheets = workbook.Sheets;
+ const data = {};
+
+ workbook.SheetNames.forEach((sheetName) => {
+ const sheet = sheets[sheetName];
+ const columns = this.getColumns(sheet);
+ const rows = xlsx.utils.sheet_to_json(sheet, {
+ raw: true,
+ }).map((row) => {
+ const arr = [];
+
+ columns.forEach((col) => {
+ const val = row[col];
+
+ if (typeof val !== 'undefined') {
+ arr.push(val);
+ } else {
+ arr.push('');
+ }
+ });
+
+ return arr;
+ });
+
+ data[sheetName] = {
+ columns,
+ rows,
+ };
+ });
+
+ resolve(data);
+ });
+ }
+
+ getColumns(sheet) {
+ if (!sheet['!ref']) return [];
+
+ const range = xlsx.utils.decode_range(sheet['!ref']);
+ const columnHeaders = [];
+ for (let c = range.s.c; c <= range.e.c; c += 1) {
+ const val = sheet[xlsx.utils.encode_cell({
+ c,
+ r: range.s.r,
+ })];
+
+ if (val) {
+ columnHeaders.push(val.v);
+ }
+ }
+
+ return columnHeaders;
+ }
+}
diff --git a/app/assets/javascripts/blob/xlsx_viewer.js b/app/assets/javascripts/blob/xlsx_viewer.js
new file mode 100644
index 0000000000000000000000000000000000000000..511510c0c4f97ab9d0d58a273ec2802d988f074b
--- /dev/null
+++ b/app/assets/javascripts/blob/xlsx_viewer.js
@@ -0,0 +1,24 @@
+/* eslint-disable no-new */
+import Vue from 'vue';
+import xlsxTable from './xlsx/index.vue';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new Vue({
+ el: document.getElementById('js-xlsx-viewer'),
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ };
+ },
+ components: {
+ xlsxTable,
+ },
+ render(createElement) {
+ return createElement('xlsx-table', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/stylesheets/pages/xlsx.scss b/app/assets/stylesheets/pages/xlsx.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d34a001e3f06eebac2f3b2085783c3ad1945baa0
--- /dev/null
+++ b/app/assets/stylesheets/pages/xlsx.scss
@@ -0,0 +1,30 @@
+$hll-bg: #f8eec7;
+
+.hll {
+ > td {
+ background-color: $hll-bg;
+ }
+}
+
+.xlsx-table {
+ border: 0;
+
+ > thead > tr > th {
+ &:first-of-type {
+ border-left: 0;
+ }
+
+ &:last-of-type {
+ border-right: 0;
+ }
+ }
+
+ > tbody > tr > td {
+ &:first-of-type {
+ background-color: $gray-light;
+ border-top: 0;
+ border-bottom: 0;
+ vertical-align: middle;
+ }
+ }
+}
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 55872acef5101bfffa65d4fd890aa985ea9f409f..51aaf978192af62c59f55ad8468979388d4596e7 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -54,6 +54,10 @@ def pdf?
extension == 'pdf'
end
+ def xlsx?
+ binary? && name && File.extname(name) == '.xlsx'
+ end
+
def ipython_notebook?
text? && language&.name == 'Jupyter Notebook'
end
@@ -109,6 +113,8 @@ def to_partial_path(project)
else
'text'
end
+ elsif xlsx?
+ 'xlsx'
else
'download'
end
diff --git a/app/views/projects/blob/_xlsx.html.haml b/app/views/projects/blob/_xlsx.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d0b3234d056139c3525ccf1b01aaaab7b6b9f0c0
--- /dev/null
+++ b/app/views/projects/blob/_xlsx.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('xlsx_viewer')
+
+.file-content#js-xlsx-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
diff --git a/changelogs/unreleased/xlsx-renderer.yml b/changelogs/unreleased/xlsx-renderer.yml
new file mode 100644
index 0000000000000000000000000000000000000000..21966ba43922cf54b79399064fab643e6fa4b56b
--- /dev/null
+++ b/changelogs/unreleased/xlsx-renderer.yml
@@ -0,0 +1,4 @@
+---
+title: XLSX files render in the browser
+merge_request:
+author:
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 59c7050a14de5dfb4f26b5946a268a9aa5a41131..a21463f4679fbdfbc017a7ad11aef44fb1c114b6 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -398,3 +398,15 @@
:why: https://github.com/remy/undefsafe/blob/master/LICENSE
:versions: []
:when: 2017-04-10 06:30:00.002555000 Z
+- - :approve
+ - printj
+ - :who: Phil Hughes
+ :why: https://github.com/SheetJS/printj/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-11 15:04:29.316170000 Z
+- - :approve
+ - voc
+ - :who: Phil Hughes
+ :why: https://github.com/SheetJS/voc/blob/master/LICENSE
+ :versions: []
+ :when: 2017-04-11 15:05:17.527393000 Z
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0fea3f8222b83cd1634e27d399c36ade718c8c0f..9731c000d635b91e1f5c496d3c8df40c08164025 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -40,6 +40,7 @@ var config = {
notebook_viewer: './blob/notebook_viewer.js',
sketch_viewer: './blob/sketch_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
+ xlsx_viewer: './blob/xlsx_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags',
@@ -123,6 +124,7 @@ var config = {
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
+ 'xlsx_viewer',
'vue_pipelines',
],
minChunks: function(module, count) {
diff --git a/package.json b/package.json
index a17399ddb8f614b897e3d2af9a2e97849ccb97b3..dc42b22dd3c5d4a74429acea05bb2f924efb06dc 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,8 @@
"vue-resource": "^0.9.3",
"vue-template-compiler": "^2.2.6",
"webpack": "^2.3.3",
- "webpack-bundle-analyzer": "^2.3.0"
+ "webpack-bundle-analyzer": "^2.3.0",
+ "xlsx": "^0.9.9"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
diff --git a/spec/javascripts/blob/xlsx/components/table_spec.js b/spec/javascripts/blob/xlsx/components/table_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ccef4b9c22d758ebe8b398b984cd250d7110ceae
--- /dev/null
+++ b/spec/javascripts/blob/xlsx/components/table_spec.js
@@ -0,0 +1,108 @@
+import Vue from 'vue';
+import table from '~/blob/xlsx/components/table.vue';
+
+describe('XLSX table', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const TableComponent = Vue.extend(table);
+
+ vm = new TableComponent({
+ propsData: {
+ sheet: {
+ columns: ['test', 'test 2'],
+ rows: [
+ ['test 3', 'test 4'],
+ ['test 6', 'test 5'],
+ ],
+ },
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ location.hash = '';
+ });
+
+ describe('linePath', () => {
+ it('returns linePath with just the number when hash is empty', () => {
+ expect(
+ vm.linePath(0),
+ ).toBe('#L1');
+ });
+
+ it('returns linePath with just the number when hash has a value', () => {
+ location.hash = 'test';
+
+ expect(
+ vm.linePath(0),
+ ).toBe('#test-L1');
+ });
+ });
+
+ describe('getCurrentLineNumberFromUrl', () => {
+ it('gets line number', () => {
+ location.hash = 'L1';
+ vm.getCurrentLineNumberFromUrl();
+
+ expect(
+ vm.currentLineNumber,
+ ).toBe(1);
+ });
+
+ it('gets line number when hash has sheet name', () => {
+ location.hash = 'test-L1';
+ vm.getCurrentLineNumberFromUrl();
+
+ expect(
+ vm.currentLineNumber,
+ ).toBe(1);
+ });
+ });
+
+ it('renders column names', () => {
+ expect(
+ vm.$el.querySelector('th:nth-child(2)').textContent.trim(),
+ ).toBe('test');
+
+ expect(
+ vm.$el.querySelector('th:nth-child(3)').textContent.trim(),
+ ).toBe('test 2');
+ });
+
+ describe('row rendering', () => {
+ it('renders row numbers', () => {
+ expect(
+ vm.$el.querySelector('td:first-child').textContent.trim(),
+ ).toBe('1');
+ });
+
+ it('updates hash when clicking line number', (done) => {
+ vm.$el.querySelector('td:first-child a').click();
+
+ Vue.nextTick(() => {
+ expect(
+ location.hash,
+ ).toBe('#L1');
+
+ done();
+ });
+ });
+
+ it('calls updateCurrentLineNumber when clicking line number', (done) => {
+ spyOn(vm, 'updateCurrentLineNumber');
+
+ vm.$el.querySelector('td:first-child a').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.updateCurrentLineNumber,
+ ).toHaveBeenCalledWith(0);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/blob/xlsx/components/tabs_spec.js b/spec/javascripts/blob/xlsx/components/tabs_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..031c662471bb2eebe8ac0d890c3b237b0922e4fa
--- /dev/null
+++ b/spec/javascripts/blob/xlsx/components/tabs_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import tabs from '~/blob/xlsx/components/tabs.vue';
+import eventHub from '~/blob/xlsx/eventhub';
+
+describe('XLSX tabs', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const TabsComponent = Vue.extend(tabs);
+
+ vm = new TabsComponent({
+ propsData: {
+ currentSheetName: 'test 1',
+ sheetNames: ['test 1', 'test 2'],
+ },
+ }).$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('changes hash to sheet name', (done) => {
+ eventHub.$on('update-sheet', (name) => {
+ expect(
+ name,
+ ).toBe('test 2');
+
+ done();
+ });
+
+ vm.changeSheet('test 2');
+ });
+
+ it('selects current sheet name', () => {
+ expect(
+ vm.$el.querySelector('li:first-child'),
+ ).toHaveClass('active');
+
+ expect(
+ vm.$el.querySelector('li:nth-child(2)'),
+ ).not.toHaveClass('active');
+ });
+
+ it('getTabPath returns encoded path', () => {
+ expect(
+ vm.getTabPath('test 2'),
+ ).toBe(`#${encodeURIComponent('test 2')}`);
+ });
+});
diff --git a/spec/javascripts/blob/xlsx/index_spec.js b/spec/javascripts/blob/xlsx/index_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..68f0d58f449b0d85ac065aee908b170bee2f0eb0
--- /dev/null
+++ b/spec/javascripts/blob/xlsx/index_spec.js
@@ -0,0 +1,83 @@
+import Vue from 'vue';
+import Service from '~/blob/xlsx/service';
+import component from '~/blob/xlsx/index.vue';
+import eventHub from '~/blob/xlsx/eventhub';
+
+describe('XLSX Renderer', () => {
+ let vm;
+
+ beforeEach((done) => {
+ const RendererComponent = Vue.extend(component);
+
+ spyOn(Service.prototype, 'getData').and.callFake(() => new Promise((resolve) => {
+ resolve({
+ test: {
+ columns: 1,
+ },
+ 'test 1': {
+ columns: 2,
+ },
+ });
+
+ setTimeout(done, 0);
+ }));
+
+ spyOn(eventHub, '$off');
+
+ vm = new RendererComponent({
+ propsData: {
+ endpoint: '/',
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ location.hash = '';
+ });
+
+ it('sheetNames returns array of sheet names', () => {
+ expect(
+ vm.sheetNames,
+ ).toEqual(['test', 'test 1']);
+ });
+
+ it('sheet returns currently selected sheet', () => {
+ expect(
+ vm.sheet,
+ ).toEqual({
+ columns: 1,
+ });
+ });
+
+ describe('getInitialSheet', () => {
+ it('defaults to first sheet', () => {
+ expect(
+ vm.currentSheetName,
+ ).toBe('test');
+ });
+
+ it('uses hash for currentSheetName', () => {
+ location.hash = 'test 1';
+
+ expect(
+ vm.getInitialSheet(),
+ ).toBe('test 1');
+ });
+
+ it('defaults to first sheet if hash is not found in sheetNames', () => {
+ location.hash = 'test 2';
+
+ expect(
+ vm.getInitialSheet(),
+ ).toBe('test');
+ });
+ });
+
+ it('removes eventHub listener on destroy', () => {
+ vm.$destroy();
+
+ expect(
+ eventHub.$off,
+ ).toHaveBeenCalledWith('update-sheet', vm.updateSheetName);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index e16cd9c36730167a9ef8534fd0e2a393be13c11d..8c95b49196a9a0a7507bd9a16b5b79a2e12d5983 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -37,6 +37,14 @@ acorn@^4.0.11, acorn@^4.0.3, acorn@^4.0.4:
version "4.0.11"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.11.tgz#edcda3bd937e7556410d42ed5860f67399c794c0"
+adler-32@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.0.0.tgz#28728a71756f629666dd1653cd80793a9df18651"
+ dependencies:
+ concat-stream ""
+ exit-on-epipe ""
+ printj ""
+
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -1068,6 +1076,12 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
+cfb@~0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/cfb/-/cfb-0.11.1.tgz#a96db8f272a6c3fb99dbbb23ef41223f48be1ea7"
+ dependencies:
+ commander ""
+
chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
@@ -1153,6 +1167,15 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+codepage@~1.8.0:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.8.1.tgz#f1a009d5261dc2754628bacb6fbbf0e6e2abffaa"
+ dependencies:
+ commander ""
+ concat-stream ""
+ exit-on-epipe ""
+ voc ""
+
color-convert@^1.3.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
@@ -1185,6 +1208,10 @@ colormin@^1.0.5:
css-color-names "0.0.4"
has "^1.0.1"
+colors@0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
+
colors@^1.1.0, colors@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
@@ -1201,7 +1228,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.8.1, commander@^2.9.0:
+commander@, commander@^2.8.1, commander@^2.9.0, commander@~2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
@@ -1257,6 +1284,14 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+concat-stream@, concat-stream@^1.4.6:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
concat-stream@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611"
@@ -1265,14 +1300,6 @@ concat-stream@1.5.0:
readable-stream "~2.0.0"
typedarray "~0.0.5"
-concat-stream@^1.4.6:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
- dependencies:
- inherits "^2.0.3"
- readable-stream "^2.2.2"
- typedarray "^0.0.6"
-
config-chain@~1.1.5:
version "1.1.11"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2"
@@ -1373,6 +1400,14 @@ cosmiconfig@^2.1.0, cosmiconfig@^2.1.1:
parse-json "^2.2.0"
require-from-string "^1.1.0"
+crc-32@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.0.1.tgz#2efbddb7ccbb7beb4d181803b10e33a29c9fd214"
+ dependencies:
+ concat-stream ""
+ exit-on-epipe ""
+ printj ""
+
create-ecdh@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -2116,6 +2151,10 @@ exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+exit-on-epipe@, exit-on-epipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.0.tgz#f6e0579c8214d33a08109fd6e2e5c1dbc70463fc"
+
expand-braces@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
@@ -2362,6 +2401,10 @@ forwarded@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
+frac@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/frac/-/frac-0.3.1.tgz#577677b7fdcbe6faf7c461f1801d34137cda4354"
+
fresh@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.3.0.tgz#651f838e22424e7566de161d8358caa199f83d4f"
@@ -4411,6 +4454,10 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+printj@:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/printj/-/printj-1.0.0.tgz#5c37de6c5772a3fed8468399c2063b5b22528867"
+
private@^0.1.6:
version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -5137,6 +5184,14 @@ sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ssf@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.9.0.tgz#eab445edc8c85bc1b637eaa62c3cf0ace92b7148"
+ dependencies:
+ colors "0.6.2"
+ frac "0.3.1"
+ voc ""
+
sshpk@^1.7.0:
version "1.10.2"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa"
@@ -5585,6 +5640,10 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
+voc@:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/voc/-/voc-0.5.0.tgz#be6ca7c76e4a57d930cc80f6b31fbd80ca86045c"
+
void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
@@ -5823,6 +5882,18 @@ xdg-basedir@^2.0.0:
dependencies:
os-homedir "^1.0.0"
+xlsx@^0.9.9:
+ version "0.9.10"
+ resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.9.10.tgz#86434dd92fc743d8fced728c3e1e373b22ca2820"
+ dependencies:
+ adler-32 "~1.0.0"
+ cfb "~0.11.1"
+ codepage "~1.8.0"
+ commander "~2.9.0"
+ crc-32 "~1.0.0"
+ exit-on-epipe "~1.0.0"
+ ssf "~0.9.0"
+
xmlhttprequest-ssl@1.5.3:
version "1.5.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"