Commit 1eb3a47c authored by Justin Boyson's avatar Justin Boyson Committed by Olena Horal-Koretska

Add support LaTeX in jupyter notebooks

Add MathJax package as well
parent 8635c0cd
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import CodeOutput from '../code/index.vue'; import CodeOutput from '../code/index.vue';
import HtmlOutput from './html.vue'; import HtmlOutput from './html.vue';
import ImageOutput from './image.vue'; import ImageOutput from './image.vue';
import LatexOutput from './latex.vue';
export default { export default {
props: { props: {
...@@ -35,6 +36,8 @@ export default { ...@@ -35,6 +36,8 @@ export default {
return 'image/jpeg'; return 'image/jpeg';
} else if (output.data['text/html']) { } else if (output.data['text/html']) {
return 'text/html'; return 'text/html';
} else if (output.data['text/latex']) {
return 'text/latex';
} else if (output.data['image/svg+xml']) { } else if (output.data['image/svg+xml']) {
return 'image/svg+xml'; return 'image/svg+xml';
} }
...@@ -59,6 +62,8 @@ export default { ...@@ -59,6 +62,8 @@ export default {
return ImageOutput; return ImageOutput;
} else if (output.data['text/html']) { } else if (output.data['text/html']) {
return HtmlOutput; return HtmlOutput;
} else if (output.data['text/latex']) {
return LatexOutput;
} else if (output.data['image/svg+xml']) { } else if (output.data['image/svg+xml']) {
return HtmlOutput; return HtmlOutput;
} }
......
<script>
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
export default {
name: 'LatexOutput',
components: {
Prompt,
},
props: {
count: {
type: Number,
required: true,
},
rawCode: {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
code() {
// MathJax will not parse out the inline delimeters "$$" correctly
// so we remove them from the raw code itself
const parsedCode = this.rawCode.replace(/\$\$/g, '');
const svg = window.MathJax.tex2svg(parsedCode);
// NOTE: This is used with `v-html` and not `v-safe-html` due to an
// issue with dompurify stripping out xlink attributes from use tags
return svg.outerHTML;
},
},
};
</script>
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="index === 0" />
<!-- eslint-disable -->
<div ref="maths" v-html="code"></div>
</div>
</template>
---
title: Add LaTeX support for Jupyter Notebooks
merge_request: 49497
author:
type: fixed
...@@ -108,6 +108,7 @@ ...@@ -108,6 +108,7 @@
"katex": "^0.10.0", "katex": "^0.10.0",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"marked": "^0.3.12", "marked": "^0.3.12",
"mathjax": "3",
"mermaid": "^8.5.2", "mermaid": "^8.5.2",
"mersenne-twister": "1.1.0", "mersenne-twister": "1.1.0",
"minimatch": "^3.0.4", "minimatch": "^3.0.4",
......
...@@ -18,12 +18,14 @@ describe('Output component', () => { ...@@ -18,12 +18,14 @@ describe('Output component', () => {
}; };
beforeEach(() => { beforeEach(() => {
// This is the output after rendering a jupyter notebook
json = getJSONFixture('blob/notebook/basic.json'); json = getJSONFixture('blob/notebook/basic.json');
}); });
describe('text output', () => { describe('text output', () => {
beforeEach((done) => { beforeEach((done) => {
createComponent(json.cells[2].outputs[0]); const textType = json.cells[2];
createComponent(textType.outputs[0]);
setImmediate(() => { setImmediate(() => {
done(); done();
...@@ -41,7 +43,8 @@ describe('Output component', () => { ...@@ -41,7 +43,8 @@ describe('Output component', () => {
describe('image output', () => { describe('image output', () => {
beforeEach((done) => { beforeEach((done) => {
createComponent(json.cells[3].outputs[0]); const imageType = json.cells[3];
createComponent(imageType.outputs[0]);
setImmediate(() => { setImmediate(() => {
done(); done();
...@@ -55,23 +58,42 @@ describe('Output component', () => { ...@@ -55,23 +58,42 @@ describe('Output component', () => {
describe('html output', () => { describe('html output', () => {
it('renders raw HTML', () => { it('renders raw HTML', () => {
createComponent(json.cells[4].outputs[0]); const htmlType = json.cells[4];
createComponent(htmlType.outputs[0]);
expect(vm.$el.querySelector('p')).not.toBeNull(); expect(vm.$el.querySelector('p')).not.toBeNull();
expect(vm.$el.querySelectorAll('p').length).toBe(1); expect(vm.$el.querySelectorAll('p')).toHaveLength(1);
expect(vm.$el.textContent.trim()).toContain('test'); expect(vm.$el.textContent.trim()).toContain('test');
}); });
it('renders multiple raw HTML outputs', () => { it('renders multiple raw HTML outputs', () => {
createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]); const htmlType = json.cells[4];
createComponent([htmlType.outputs[0], htmlType.outputs[0]]);
expect(vm.$el.querySelectorAll('p').length).toBe(2); expect(vm.$el.querySelectorAll('p')).toHaveLength(2);
});
});
describe('LaTeX output', () => {
it('renders LaTeX', () => {
const output = {
data: {
'text/latex': ['$$F(k) = \\int_{-\\infty}^{\\infty} f(x) e^{2\\pi i k} dx$$'],
'text/plain': ['<IPython.core.display.Latex object>'],
},
metadata: {},
output_type: 'display_data',
};
createComponent(output);
expect(vm.$el.querySelector('.MathJax')).not.toBeNull();
}); });
}); });
describe('svg output', () => { describe('svg output', () => {
beforeEach((done) => { beforeEach((done) => {
createComponent(json.cells[5].outputs[0]); const svgType = json.cells[5];
createComponent(svgType.outputs[0]);
setImmediate(() => { setImmediate(() => {
done(); done();
...@@ -85,7 +107,8 @@ describe('Output component', () => { ...@@ -85,7 +107,8 @@ describe('Output component', () => {
describe('default to plain text', () => { describe('default to plain text', () => {
beforeEach((done) => { beforeEach((done) => {
createComponent(json.cells[6].outputs[0]); const unknownType = json.cells[6];
createComponent(unknownType.outputs[0]);
setImmediate(() => { setImmediate(() => {
done(); done();
...@@ -102,7 +125,8 @@ describe('Output component', () => { ...@@ -102,7 +125,8 @@ describe('Output component', () => {
}); });
it("renders as plain text when doesn't recognise other types", (done) => { it("renders as plain text when doesn't recognise other types", (done) => {
createComponent(json.cells[7].outputs[0]); const unknownType = json.cells[7];
createComponent(unknownType.outputs[0]);
setImmediate(() => { setImmediate(() => {
expect(vm.$el.querySelector('pre')).not.toBeNull(); expect(vm.$el.querySelector('pre')).not.toBeNull();
......
import { shallowMount } from '@vue/test-utils';
import LatexOutput from '~/notebook/cells/output/latex.vue';
import Prompt from '~/notebook/cells/prompt.vue';
describe('LaTeX output cell', () => {
beforeEach(() => {
window.MathJax = {
tex2svg: jest.fn((code) => ({ outerHTML: code })),
};
});
const inlineLatex = '$$F(k) = \\int_{-\\infty}^{\\infty} f(x) e^{2\\pi i k} dx$$';
const count = 12345;
const createComponent = (rawCode, index) =>
shallowMount(LatexOutput, {
propsData: {
count,
index,
rawCode,
},
});
it.each`
index | expectation
${0} | ${true}
${1} | ${false}
`('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
const wrapper = createComponent(inlineLatex, index);
const prompt = wrapper.find(Prompt);
expect(prompt.props().count).toEqual(count);
expect(prompt.props().showOutput).toEqual(expectation);
});
it('strips the `$$` delimter from LaTeX', () => {
createComponent(inlineLatex, 0);
expect(window.MathJax.tex2svg).toHaveBeenCalledWith(expect.not.stringContaining('$$'));
});
});
...@@ -3057,7 +3057,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: ...@@ -3057,7 +3057,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
commander@2, commander@^2.10.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
version "2.20.0" version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
...@@ -7476,11 +7476,11 @@ karma@^4.2.0: ...@@ -7476,11 +7476,11 @@ karma@^4.2.0:
useragent "2.3.0" useragent "2.3.0"
katex@^0.10.0: katex@^0.10.0:
version "0.10.0" version "0.10.2"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.0.tgz#da562e5d0d5cc3aa602e27af8a9b8710bfbce765" resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.2.tgz#39973edbb65eda5b6f9e7f41648781e557dd4932"
integrity sha512-/WRvx+L1eVBrLwX7QzKU1dQuaGnE7E8hDvx3VWfZh9HbMiCfsKWJNnYZ0S8ZMDAfAyDSofdyXIrH/hujF1fYXg== integrity sha512-cQOmyIRoMloCoSIOZ1+gEwsksdJZ1EW4SWm3QzxSza/QsnZr6D4U1V9S4q+B/OLm2OQ8TCBecQ8MaIfnScI7cw==
dependencies: dependencies:
commander "^2.16.0" commander "^2.19.0"
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"
...@@ -8054,6 +8054,11 @@ marked@^0.3.12, marked@~0.3.6: ...@@ -8054,6 +8054,11 @@ marked@^0.3.12, marked@~0.3.6:
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg== integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
mathjax@3:
version "3.1.2"
resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-3.1.2.tgz#95c0d45ce2330ef7b6a815cebe7d61ecc26bbabd"
integrity sha512-BojKspBv4nNWzO1wC6VEI+g9gHDOhkaGHGgLxXkasdU4pwjdO5AXD5M/wcLPkXYPjZ/N+6sU8rjQTlyvN2cWiQ==
mathml-tag-names@^2.1.0: mathml-tag-names@^2.1.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc" resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
......
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