Commit a7d9d536 authored by Reinhold Gschweicher's avatar Reinhold Gschweicher

Katex render and vscode output improvements for md

1. Fix output of jupyter notebooks saved in VSCode

VSCode uses the newer NBFormat Version 4.1 [1], which allows
the output text entry to be a simple string.

The function `rawCode` assumed NBFormat Version 3.2 [2], which requires
the output.text object to be an array.

This commit adds a special handling if the type of output.text is a
string, then we don't need to use `join()` and can just return the
string.

Fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/212837

[1] https://nbformat.readthedocs.io/en/latest/format_description.html
[2] https://ipython.org/ipython-doc/3/notebook/nbformat.html

2. Handle katex render error in ipython markdown

When a formula brings katex to its knees the whole cell is dropped.
Instead of just dropping continue on and leave the unrendered formula in
the cell.

Partially fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/216744

3. Render listitems with Katex in ipython markdown

Add a renderer callback for list items. Not only paragraphs should have
the honor to have math formulas rendered with Katex. Also listitems of
bullet points and enumerations should have this honor.

Fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/216741

4. Get the tick `'` back to pass it to Katex

When rendering IPython notebook markdown cells the formulas are passed
to Katex. Before that the raw string is passed to marked.js to handle
HTML forbidden characters. One of these characters is the `'`. This
character is often used in math formulas and the replacement `&#39` is
not well received by Katex.

Fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/216744
parent 73ffa9c3
...@@ -36,9 +36,9 @@ const katexRegexString = `( ...@@ -36,9 +36,9 @@ const katexRegexString = `(
.replace(/\s/g, '') .replace(/\s/g, '')
.trim(); .trim();
renderer.paragraph = t => { function renderKatex(t) {
let text = t; let text = t;
let inline = false; let numInline = 0; // number of successfull converted math formulas
if (typeof katex !== 'undefined') { if (typeof katex !== 'undefined') {
const katexString = text const katexString = text
...@@ -50,24 +50,40 @@ renderer.paragraph = t => { ...@@ -50,24 +50,40 @@ renderer.paragraph = t => {
const numberOfMatches = katexString.match(regex); const numberOfMatches = katexString.match(regex);
if (numberOfMatches && numberOfMatches.length !== 0) { if (numberOfMatches && numberOfMatches.length !== 0) {
let matches = regex.exec(katexString);
if (matchLocation > 0) { if (matchLocation > 0) {
let matches = regex.exec(katexString); numInline += 1;
inline = true;
while (matches !== null) { while (matches !== null) {
const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, '')); try {
text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; const renderedKatex = katex.renderToString(
matches[0].replace(/\$/g, '').replace(/'/g, "'"),
); // get the tick ' back again from HTMLified string
text = `${text.replace(matches[0], ` ${renderedKatex}`)}`;
} catch {
numInline -= 1;
}
matches = regex.exec(katexString); matches = regex.exec(katexString);
} }
} else { } else {
const matches = regex.exec(katexString); try {
text = katex.renderToString(matches[2]); text = katex.renderToString(matches[2].replace(/'/g, "'"));
} catch (error) {
numInline -= 1;
}
} }
} }
} }
return [text, numInline > 0];
}
renderer.paragraph = t => {
const [text, inline] = renderKatex(t);
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
}; };
renderer.listitem = t => {
const [text, inline] = renderKatex(t);
return `<li class="${inline ? 'inline-katex' : ''}">${text}</li>`;
};
marked.setOptions({ marked.setOptions({
renderer, renderer,
......
...@@ -63,6 +63,9 @@ export default { ...@@ -63,6 +63,9 @@ export default {
}, },
rawCode(output) { rawCode(output) {
if (output.text) { if (output.text) {
if (typeof output.text === 'string') {
return output.text;
}
return output.text.join(''); return output.text.join('');
} }
......
---
title: Katex render and vscode output improvements for markdown
merge_request: 31433
author: Reinhold Gschweicher <pyro4hell@gmail.com>
type: fixed
...@@ -53,16 +53,32 @@ describe('Code component', () => { ...@@ -53,16 +53,32 @@ describe('Code component', () => {
}); });
}); });
describe('with string for output', () => {
// NBFormat Version 4.1 allows outputs.text to be a string
beforeEach(() => {
const cell = json.cells[2];
cell.outputs[0].text = cell.outputs[0].text.join('');
vm = setupComponent(cell);
return vm.$nextTick();
});
it('does not render output prompt', () => {
expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
});
it('renders output cell', () => {
expect(vm.$el.querySelector('.output')).toBeDefined();
});
});
describe('with string for cell.source', () => { describe('with string for cell.source', () => {
beforeEach(done => { beforeEach(() => {
const cell = json.cells[0]; const cell = json.cells[0];
cell.source = cell.source.join(''); cell.source = cell.source.join('');
vm = setupComponent(cell); vm = setupComponent(cell);
return vm.$nextTick();
setImmediate(() => {
done();
});
}); });
it('renders the same input as when cell.source is an array', () => { it('renders the same input as when cell.source is an array', () => {
......
...@@ -11,7 +11,7 @@ describe('Markdown component', () => { ...@@ -11,7 +11,7 @@ describe('Markdown component', () => {
let cell; let cell;
let json; let json;
beforeEach(done => { beforeEach(() => {
json = getJSONFixture('blob/notebook/basic.json'); json = getJSONFixture('blob/notebook/basic.json');
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
...@@ -24,9 +24,7 @@ describe('Markdown component', () => { ...@@ -24,9 +24,7 @@ describe('Markdown component', () => {
}); });
vm.$mount(); vm.$mount();
setImmediate(() => { return vm.$nextTick();
done();
});
}); });
it('does not render promot', () => { it('does not render promot', () => {
...@@ -41,17 +39,15 @@ describe('Markdown component', () => { ...@@ -41,17 +39,15 @@ describe('Markdown component', () => {
expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
}); });
it('sanitizes output', done => { it('sanitizes output', () => {
Object.assign(cell, { Object.assign(cell, {
source: [ source: [
'[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n',
], ],
}); });
Vue.nextTick(() => { return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
done();
}); });
}); });
...@@ -60,45 +56,111 @@ describe('Markdown component', () => { ...@@ -60,45 +56,111 @@ describe('Markdown component', () => {
json = getJSONFixture('blob/notebook/math.json'); json = getJSONFixture('blob/notebook/math.json');
}); });
it('renders multi-line katex', done => { it('renders multi-line katex', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
cell: json.cells[0], cell: json.cells[0],
}, },
}).$mount(); }).$mount();
Vue.nextTick(() => { return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.katex')).not.toBeNull(); expect(vm.$el.querySelector('.katex')).not.toBeNull();
done();
}); });
}); });
it('renders inline katex', done => { it('renders inline katex', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
cell: json.cells[1], cell: json.cells[1],
}, },
}).$mount(); }).$mount();
Vue.nextTick(() => { return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
done();
}); });
}); });
it('renders multiple inline katex', done => { it('renders multiple inline katex', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
cell: json.cells[1], cell: json.cells[1],
}, },
}).$mount(); }).$mount();
Vue.nextTick(() => { return vm.$nextTick().then(() => {
expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4); expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4);
});
});
it('output cell in case of katex error', () => {
vm = new Component({
propsData: {
cell: {
cell_type: 'markdown',
metadata: {},
source: ['Some invalid $a & b$ inline formula $b & c$\n', '\n'],
},
},
}).$mount();
return vm.$nextTick().then(() => {
// expect one paragraph with no katex formula in it
expect(vm.$el.querySelectorAll('p').length).toBe(1);
expect(vm.$el.querySelectorAll('p .katex').length).toBe(0);
});
});
it('output cell and render remaining formula in case of katex error', () => {
vm = new Component({
propsData: {
cell: {
cell_type: 'markdown',
metadata: {},
source: ['An invalid $a & b$ inline formula and a vaild one $b = c$\n', '\n'],
},
},
}).$mount();
return vm.$nextTick().then(() => {
// expect one paragraph with no katex formula in it
expect(vm.$el.querySelectorAll('p').length).toBe(1);
expect(vm.$el.querySelectorAll('p .katex').length).toBe(1);
});
});
it('renders math formula in list object', () => {
vm = new Component({
propsData: {
cell: {
cell_type: 'markdown',
metadata: {},
source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
},
},
}).$mount();
return vm.$nextTick().then(() => {
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li').length).toBe(1);
expect(vm.$el.querySelectorAll('li .katex').length).toBe(2);
});
});
it("renders math formula with tick ' in it", () => {
vm = new Component({
propsData: {
cell: {
cell_type: 'markdown',
metadata: {},
source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
},
},
}).$mount();
done(); return vm.$nextTick().then(() => {
// expect one list with a katex formula in it
expect(vm.$el.querySelectorAll('li').length).toBe(1);
expect(vm.$el.querySelectorAll('li .katex').length).toBe(2);
}); });
}); });
}); });
......
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