From 80384c4670bc52c6373ff97c5fce72b74ce1f0fc Mon Sep 17 00:00:00 2001 From: Reinhold Gschweicher Date: Mon, 12 Apr 2021 13:50:24 +0200 Subject: [PATCH] IPython KaTeX rendering of comparison operators for markdown 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. Two of these characters are the less-than-operator `<` and the greater-than-operator `>`. These characters are often used in math formulas and the replacements `<` and `>` are not well received by Katex. For the tests use async/await syntax, and check length with `toHaveLength()` to be more explicit. Fixes: https://gitlab.com/gitlab-org/gitlab/-/issues/327450 --- .../javascripts/notebook/cells/markdown.vue | 11 +- .../ipython_katex_comparison_operators.yml | 5 + spec/frontend/notebook/cells/markdown_spec.js | 106 +++++++++++------- 3 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 changelogs/unreleased/ipython_katex_comparison_operators.yml diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index e4cde0d4ff345b..c09db6851e5846 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -37,6 +37,11 @@ const katexRegexString = `( .replace(/\s/g, '') .trim(); +function deHTMLify(t) { + // get some specific characters back, that are allowed for KaTex rendering + const text = t.replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>'); + return text; +} function renderKatex(t) { let text = t; let numInline = 0; // number of successfull converted math formulas @@ -57,9 +62,7 @@ function renderKatex(t) { while (matches !== null) { try { - const renderedKatex = katex.renderToString( - matches[0].replace(/\$/g, '').replace(/'/g, "'"), - ); // get the tick ' back again from HTMLified string + const renderedKatex = katex.renderToString(deHTMLify(matches[0].replace(/\$/g, ''))); text = `${text.replace(matches[0], ` ${renderedKatex}`)}`; } catch { numInline -= 1; @@ -68,7 +71,7 @@ function renderKatex(t) { } } else { try { - text = katex.renderToString(matches[2].replace(/'/g, "'")); + text = katex.renderToString(deHTMLify(matches[2])); } catch (error) { numInline -= 1; } diff --git a/changelogs/unreleased/ipython_katex_comparison_operators.yml b/changelogs/unreleased/ipython_katex_comparison_operators.yml new file mode 100644 index 00000000000000..8147bc91066067 --- /dev/null +++ b/changelogs/unreleased/ipython_katex_comparison_operators.yml @@ -0,0 +1,5 @@ +--- +title: IPython KaTeX rendering of comparison operators for markdown +merge_request: 59132 +author: Reinhold Gschweicher +type: fixed diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js index 4d6addaf47c6f1..219d74595bd966 100644 --- a/spec/frontend/notebook/cells/markdown_spec.js +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -39,16 +39,15 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); - it('sanitizes output', () => { + it('sanitizes output', async () => { Object.assign(cell, { source: [ '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', ], }); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); - }); + await vm.$nextTick(); + expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); }); describe('katex', () => { @@ -56,43 +55,40 @@ describe('Markdown component', () => { json = getJSONFixture('blob/notebook/math.json'); }); - it('renders multi-line katex', () => { + it('renders multi-line katex', async () => { vm = new Component({ propsData: { cell: json.cells[0], }, }).$mount(); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.katex')).not.toBeNull(); - }); + await vm.$nextTick(); + expect(vm.$el.querySelector('.katex')).not.toBeNull(); }); - it('renders inline katex', () => { + it('renders inline katex', async () => { vm = new Component({ propsData: { cell: json.cells[1], }, }).$mount(); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); - }); + await vm.$nextTick(); + expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull(); }); - it('renders multiple inline katex', () => { + it('renders multiple inline katex', async () => { vm = new Component({ propsData: { cell: json.cells[1], }, }).$mount(); - return vm.$nextTick().then(() => { - expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4); - }); + await vm.$nextTick(); + expect(vm.$el.querySelectorAll('p:nth-child(2) .katex')).toHaveLength(4); }); - it('output cell in case of katex error', () => { + it('output cell in case of katex error', async () => { vm = new Component({ propsData: { cell: { @@ -103,14 +99,13 @@ describe('Markdown component', () => { }, }).$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); - }); + await vm.$nextTick(); + // expect one paragraph with no katex formula in it + expect(vm.$el.querySelectorAll('p')).toHaveLength(1); + expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(0); }); - it('output cell and render remaining formula in case of katex error', () => { + it('output cell and render remaining formula in case of katex error', async () => { vm = new Component({ propsData: { cell: { @@ -121,14 +116,13 @@ describe('Markdown component', () => { }, }).$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); - }); + await vm.$nextTick(); + // expect one paragraph with no katex formula in it + expect(vm.$el.querySelectorAll('p')).toHaveLength(1); + expect(vm.$el.querySelectorAll('p .katex')).toHaveLength(1); }); - it('renders math formula in list object', () => { + it('renders math formula in list object', async () => { vm = new Component({ propsData: { cell: { @@ -139,14 +133,13 @@ describe('Markdown component', () => { }, }).$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); - }); + await vm.$nextTick(); + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li')).toHaveLength(1); + expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); }); - it("renders math formula with tick ' in it", () => { + it("renders math formula with tick ' in it", async () => { vm = new Component({ propsData: { cell: { @@ -157,11 +150,44 @@ describe('Markdown component', () => { }, }).$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); - }); + await vm.$nextTick(); + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li')).toHaveLength(1); + expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); + }); + + it('renders math formula with less-than-operator < in it', async () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ['- list with inline $a=2$ inline formula $a + b < c$\n', '\n'], + }, + }, + }).$mount(); + + await vm.$nextTick(); + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li')).toHaveLength(1); + expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); + }); + + it('renders math formula with greater-than-operator > in it', async () => { + vm = new Component({ + propsData: { + cell: { + cell_type: 'markdown', + metadata: {}, + source: ['- list with inline $a=2$ inline formula $a + b > c$\n', '\n'], + }, + }, + }).$mount(); + + await vm.$nextTick(); + // expect one list with a katex formula in it + expect(vm.$el.querySelectorAll('li')).toHaveLength(1); + expect(vm.$el.querySelectorAll('li .katex')).toHaveLength(2); }); }); }); -- GitLab