Commit 640e8093 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'notebooklab-in-repo' into 'master'

Moved NotebookLab assets into repo

See merge request !10630
parents 3ac4ef22 1d99c775
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab';
import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
......@@ -19,6 +18,9 @@ export default () => {
json: {},
};
},
components: {
notebookLab,
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
......
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue';
export default {
components: {
'code-cell': CodeCell,
'output-cell': OutputCell,
},
props: {
cell: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
computed: {
rawInputCode() {
if (this.cell.source) {
return this.cell.source.join('');
}
return '';
},
hasOutput() {
return this.cell.outputs.length;
},
output() {
return this.cell.outputs[0];
},
},
};
</script>
<style scoped>
.cell {
flex-direction: column;
}
</style>
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<script>
import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
components: {
prompt: Prompt,
},
props: {
count: {
type: Number,
required: false,
default: 0,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
type: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
computed: {
code() {
return this.rawCode;
},
promptType() {
const type = this.type.split('put')[0];
return type.charAt(0).toUpperCase() + type.slice(1);
},
},
mounted() {
Prism.highlightElement(this.$refs.code);
},
};
</script>
export { default as MarkdownCell } from './markdown.vue';
export { default as CodeCell } from './code.vue';
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<script>
/* global katex */
import marked from 'marked';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
/*
Regex to match KaTex blocks.
Supports the following:
\begin{equation}<math>\end{equation}
$$<math>$$
inline $<math>$
The matched text then goes through the KaTex renderer & then outputs the HTML
*/
const katexRegexString = `(
^\\\\begin{[a-zA-Z]+}\\s
|
^\\$\\$
|
\\s\\$(?!\\$)
)
(.+?)
(
\\s\\\\end{[a-zA-Z]+}$
|
\\$\\$$
|
\\$
)
`.replace(/\s/g, '').trim();
renderer.paragraph = (t) => {
let text = t;
let inline = false;
if (typeof katex !== 'undefined') {
const katexString = text.replace(/\\/g, '\\');
const matches = new RegExp(katexRegexString, 'gi').exec(katexString);
if (matches && matches.length > 0) {
if (matches[1].trim() === '$' && matches[3].trim() === '$') {
inline = true;
text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`;
} else {
text = katex.renderToString(matches[2]);
}
}
}
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
};
marked.setOptions({
sanitize: true,
renderer,
});
export default {
components: {
prompt: Prompt,
},
props: {
cell: {
type: Object,
required: true,
},
},
computed: {
markdown() {
return marked(this.cell.source.join(''));
},
},
};
</script>
<style>
.markdown .katex {
display: block;
text-align: center;
}
.markdown .inline-katex .katex {
display: inline;
text-align: initial;
}
</style>
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<script>
import Prompt from '../prompt.vue';
export default {
props: {
outputType: {
type: String,
required: true,
},
rawCode: {
type: String,
required: true,
},
},
components: {
prompt: Prompt,
},
};
</script>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
import Image from './image.vue';
export default {
props: {
codeCssClass: {
type: String,
required: false,
default: '',
},
count: {
type: Number,
required: false,
default: 0,
},
output: {
type: Object,
requred: true,
},
},
components: {
'code-cell': CodeCell,
'html-output': Html,
'image-output': Image,
},
data() {
return {
outputType: '',
};
},
computed: {
componentName() {
if (this.output.text) {
return 'code-cell';
} else if (this.output.data['image/png']) {
this.outputType = 'image/png';
return 'image-output';
} else if (this.output.data['text/html']) {
this.outputType = 'text/html';
return 'html-output';
} else if (this.output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml';
return 'html-output';
}
this.outputType = 'text/plain';
return 'code-cell';
},
rawCode() {
if (this.output.text) {
return this.output.text.join('');
}
return this.dataForType(this.outputType);
},
},
methods: {
dataForType(type) {
let data = this.output.data[type];
if (typeof data === 'object') {
data = data.join('');
}
return data;
},
},
};
</script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
required: false,
},
count: {
type: Number,
required: false,
},
},
};
</script>
<style scoped>
.prompt {
padding: 0 10px;
min-width: 7em;
font-family: monospace;
}
</style>
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import {
MarkdownCell,
CodeCell,
} from './cells';
export default {
components: {
'code-cell': CodeCell,
'markdown-cell': MarkdownCell,
},
props: {
notebook: {
type: Object,
required: true,
},
codeCssClass: {
type: String,
required: false,
default: '',
},
},
methods: {
cellType(type) {
return `${type}-cell`;
},
},
computed: {
cells() {
if (this.notebook.worksheets) {
const data = {
cells: [],
};
return this.notebook.worksheets.reduce((cellData, sheet) => {
const cellDataCopy = cellData;
cellDataCopy.cells = cellDataCopy.cells.concat(sheet.cells);
return cellDataCopy;
}, data).cells;
}
return this.notebook.cells;
},
hasNotebook() {
return Object.keys(this.notebook).length;
},
},
};
</script>
<style>
.cell,
.input,
.output {
display: flex;
width: 100%;
margin-bottom: 10px;
}
.cell pre {
margin: 0;
width: 100%;
}
</style>
import Prism from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/plugins/custom-class/prism-custom-class';
Prism.plugins.customClass.map({
comment: 'c',
error: 'err',
operator: 'o',
constant: 'kc',
namespace: 'kn',
keyword: 'k',
string: 's',
number: 'm',
'attr-name': 'na',
builtin: 'nb',
entity: 'ni',
function: 'nf',
tag: 'nt',
variable: 'nv',
});
export default Prism;
require 'spec_helper'
describe 'Raw files', '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'raw-project') }
before(:all) do
clean_frontend_fixtures('blob/notebook/')
end
it 'blob/notebook/basic.json' do |example|
blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
store_frontend_fixture(blob.data, example.description)
end
it 'blob/notebook/worksheets.json' do |example|
blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb')
store_frontend_fixture(blob.data, example.description)
end
end
import Vue from 'vue';
import CodeComponent from '~/notebook/cells/code.vue';
const Component = Vue.extend(CodeComponent);
describe('Code component', () => {
let vm;
let json;
beforeEach(() => {
json = getJSONFixture('blob/notebook/basic.json');
});
describe('without output', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
cell: json.cells[0],
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('does not render output prompt', () => {
expect(vm.$el.querySelectorAll('.prompt').length).toBe(1);
});
});
describe('with output', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
cell: json.cells[2],
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('does not render output prompt', () => {
expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
});
it('renders output cell', () => {
expect(vm.$el.querySelector('.output')).toBeDefined();
});
});
});
import Vue from 'vue';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
const Component = Vue.extend(MarkdownComponent);
describe('Markdown component', () => {
let vm;
let cell;
let json;
beforeEach((done) => {
json = getJSONFixture('blob/notebook/basic.json');
cell = json.cells[1];
vm = new Component({
propsData: {
cell,
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('does not render promot', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
it('does not render the markdown text', () => {
expect(
vm.$el.querySelector('.markdown').innerHTML.trim(),
).not.toEqual(cell.source.join(''));
});
it('renders the markdown HTML', () => {
expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
});
import Vue from 'vue';
import CodeComponent from '~/notebook/cells/output/index.vue';
const Component = Vue.extend(CodeComponent);
describe('Output component', () => {
let vm;
let json;
const createComponent = (output) => {
vm = new Component({
propsData: {
output,
count: 1,
},
});
vm.$mount();
};
beforeEach(() => {
json = getJSONFixture('blob/notebook/basic.json');
});
describe('text output', () => {
beforeEach((done) => {
createComponent(json.cells[2].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders as plain text', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('renders promot', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
});
describe('image output', () => {
beforeEach((done) => {
createComponent(json.cells[3].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders as an image', () => {
expect(vm.$el.querySelector('img')).not.toBeNull();
});
it('does not render the prompt', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
});
describe('html output', () => {
beforeEach((done) => {
createComponent(json.cells[4].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders raw HTML', () => {
expect(vm.$el.querySelector('p')).not.toBeNull();
expect(vm.$el.textContent.trim()).toBe('test');
});
it('does not render the prompt', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
});
describe('svg output', () => {
beforeEach((done) => {
createComponent(json.cells[5].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders as an svg', () => {
expect(vm.$el.querySelector('svg')).not.toBeNull();
});
it('does not render the prompt', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
});
describe('default to plain text', () => {
beforeEach((done) => {
createComponent(json.cells[6].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders as plain text', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
expect(vm.$el.textContent.trim()).toContain('testing');
});
it('renders promot', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
it('renders as plain text when doesn\'t recognise other types', (done) => {
createComponent(json.cells[7].outputs[0]);
setTimeout(() => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
expect(vm.$el.textContent.trim()).toContain('testing');
done();
});
});
});
});
import Vue from 'vue';
import PromptComponent from '~/notebook/cells/prompt.vue';
const Component = Vue.extend(PromptComponent);
describe('Prompt component', () => {
let vm;
describe('input', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
type: 'In',
count: 1,
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('renders in label', () => {
expect(vm.$el.textContent.trim()).toContain('In');
});
it('renders count', () => {
expect(vm.$el.textContent.trim()).toContain('1');
});
});
describe('output', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
type: 'Out',
count: 1,
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('renders in label', () => {
expect(vm.$el.textContent.trim()).toContain('Out');
});
it('renders count', () => {
expect(vm.$el.textContent.trim()).toContain('1');
});
});
});
import Vue from 'vue';
import Notebook from '~/notebook/index.vue';
const Component = Vue.extend(Notebook);
describe('Notebook component', () => {
let vm;
let json;
let jsonWithWorksheet;
beforeEach(() => {
json = getJSONFixture('blob/notebook/basic.json');
jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
});
describe('without JSON', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
notebook: {},
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('does not render', () => {
expect(vm.$el.tagName).toBeUndefined();
});
});
describe('with JSON', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
notebook: json,
codeCssClass: 'js-code-class',
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('renders cells', () => {
expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length);
});
it('renders markdown cell', () => {
expect(vm.$el.querySelector('.markdown')).not.toBeNull();
});
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('add code class to code blocks', () => {
expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
});
});
describe('with worksheets', () => {
beforeEach((done) => {
vm = new Component({
propsData: {
notebook: jsonWithWorksheet,
codeCssClass: 'js-code-class',
},
});
vm.$mount();
setTimeout(() => {
done();
});
});
it('renders cells', () => {
expect(vm.$el.querySelectorAll('.cell').length).toBe(jsonWithWorksheet.worksheets[0].cells.length);
});
it('renders markdown cell', () => {
expect(vm.$el.querySelector('.markdown')).not.toBeNull();
});
it('renders code cell', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
it('add code class to code blocks', () => {
expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
});
});
});
import Prism from '~/notebook/lib/highlight';
describe('Highlight library', () => {
it('imports python language', () => {
expect(Prism.languages.python).toBeDefined();
});
it('uses custom CSS classes', () => {
const el = document.createElement('div');
el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript);
expect(el.querySelector('.s')).not.toBeNull();
expect(el.querySelector('.nf')).not.toBeNull();
});
});
......@@ -38,7 +38,8 @@ module TestEnv
'deleted-image-test' => '6c17798',
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3'
'v1.1.0' => 'b83d6e3',
'add-ipython-files' => '6d85bb69'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
......
This diff is collapsed.
......@@ -1119,6 +1119,14 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
clipboard@^1.5.5:
version "1.6.1"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
dependencies:
good-listener "^1.2.0"
select "^1.1.2"
tiny-emitter "^1.0.0"
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
......@@ -1596,6 +1604,10 @@ delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
delegate@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
......@@ -2497,6 +2509,12 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
good-listener@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
delegate "^3.1.2"
got@^3.2.0:
version "3.3.1"
resolved "https://registry.yarnpkg.com/got/-/got-3.3.1.tgz#e5d0ed4af55fc3eef4d56007769d98192bcb2eca"
......@@ -3554,6 +3572,10 @@ map-stream@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
marked@^0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
math-expression-evaluator@^1.2.14:
version "1.2.16"
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.16.tgz#b357fa1ca9faefb8e48d10c14ef2bcb2d9f0a7c9"
......@@ -4415,6 +4437,12 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prismjs@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365"
optionalDependencies:
clipboard "^1.5.5"
private@^0.1.6:
version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
......@@ -4873,6 +4901,10 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
......@@ -5368,6 +5400,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
tmp@0.0.28, tmp@0.0.x:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment