Skip to content
Snippets Groups Projects
Commit 640e8093 authored by Jacob Schatz's avatar Jacob Schatz
Browse files

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

Moved NotebookLab assets into repo

See merge request !10630
parents 3ac4ef22 1d99c775
No related branches found
No related tags found
No related merge requests found
Showing
with 897 additions and 3 deletions
/* 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');
Loading
Loading
@@ -19,6 +18,9 @@ export default () => {
json: {},
};
},
components: {
notebookLab,
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
Loading
Loading
<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;
Loading
Loading
@@ -32,8 +32,10 @@
"js-cookie": "^2.1.3",
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
"marked": "^0.3.6",
"mousetrap": "^1.4.6",
"pikaday": "^1.5.1",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
"raw-loader": "^0.5.1",
"react-dev-utils": "^0.5.2",
Loading
Loading
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();
});
});
Loading
Loading
@@ -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
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment