// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. const sampleData = require('../../../examples/filebrowser/sample.md'); import { defaultSanitizer } from '@jupyterlab/apputils'; import { JSONObject, JSONValue } from '@lumino/coreutils'; import { Widget } from '@lumino/widgets'; import { htmlRendererFactory, imageRendererFactory, IRenderMime, latexRendererFactory, markdownRendererFactory, MimeModel, svgRendererFactory, textRendererFactory } from '../src'; function createModel( mimeType: string, source: JSONValue, trusted = false ): IRenderMime.IMimeModel { const data: JSONObject = {}; data[mimeType] = source; return new MimeModel({ data, trusted }); } function encodeChars(txt: string): string { return txt.replace(/&/g, '&').replace(//g, '>'); } const sanitizer = defaultSanitizer; const defaultOptions: any = { sanitizer, linkHandler: null, resolver: null }; describe('rendermime/factories', () => { describe('textRendererFactory', () => { describe('#mimeTypes', () => { it('should have text related mimeTypes', () => { const mimeTypes = [ 'text/plain', 'application/vnd.jupyter.stdout', 'application/vnd.jupyter.stderr' ]; expect(textRendererFactory.mimeTypes).toEqual(mimeTypes); }); }); describe('#safe', () => { it('should be safe', () => { expect(textRendererFactory.safe).toBe(true); }); }); describe('#createRenderer()', () => { it('should output the correct HTML', async () => { const f = textRendererFactory; const mimeType = 'text/plain'; const model = createModel(mimeType, 'x = 2 ** a'); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe('
x = 2 ** a'); }); it('should be re-renderable', async () => { const f = textRendererFactory; const mimeType = 'text/plain'; const model = createModel(mimeType, 'x = 2 ** a'); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); await w.renderModel(model); expect(w.node.innerHTML).toBe('
x = 2 ** a'); }); it.each([ [ 'There is no text but \x1b[01;41;32mtext\x1b[00m.\nWoo.', '
There is no text but text.\nWoo.
'
],
[
'\x1b[48;2;185;0;129mwww.example.\x1b[0m\x1b[48;2;113;0;119mcom\x1b[0m',
'www.example.com' ] ])( 'should output the correct HTML with ansi colors', async (source, expected) => { const f = textRendererFactory; const mimeType = 'application/vnd.jupyter.console-text'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe(expected); } ); it('should escape inline html', async () => { const f = textRendererFactory; const source = 'There is no text but \x1b[01;41;32mtext\x1b[00m.\nWoo.'; const mimeType = 'application/vnd.jupyter.console-text'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe( '
There is no text <script>window.x=1</script> but text.\nWoo.
'
);
});
it('should autolink single URL', async () => {
const f = textRendererFactory;
const urls = [
['https://example.com', '', ''],
['https://example.com#', '', ''],
['https://example.com/', '', ''],
['www.example.com/', '', ''],
['http://www.quotes.com/foo/', '"', '"'],
['http://www.quotes.com/foo/', "'", "'"],
['http://www.brackets.com/foo', '(', ')'],
['http://www.brackets.com/foo', '{', '}'],
['http://www.brackets.com/foo', '[', ']'],
['http://www.brackets.com/foo', '<', '>'],
['https://ends.with/>', '', ''],
['http://www.brackets.com/inv', ')', '('],
['http://www.brackets.com/inv', '}', '{'],
['http://www.brackets.com/inv', ']', '['],
['http://www.brackets.com/inv', '>', '<'],
['https://ends.with/<', '', ''],
['http://www.punctuation.com', '', ','],
['http://www.punctuation.com', '', ':'],
['http://www.punctuation.com', '', ';'],
['http://www.punctuation.com', '', '.'],
['http://www.punctuation.com', '', '!'],
['http://www.punctuation.com', '', '?'],
['https://example.com#anchor', '', ''],
['http://localhost:9090/app', '', ''],
['http://localhost:9090/app/', '', ''],
['http://127.0.0.1/test?query=string', '', ''],
['http://127.0.0.1/test?query=string¶m=42', '', '']
];
await Promise.all(
urls.map(async u => {
const [url, before, after] = u;
const source = `Text with the URL ${before}${url}${after} inside.`;
const mimeType = 'text/plain';
const model = createModel(mimeType, source);
const w = f.createRenderer({ mimeType, ...defaultOptions });
const [urlEncoded, beforeEncoded, afterEncoded] = [
url,
before,
after
].map(encodeChars);
const prefixedUrl = urlEncoded.startsWith('www.')
? 'https://' + urlEncoded
: urlEncoded;
await w.renderModel(model);
expect(w.node.innerHTML).toBe(
`Text with the URL ${beforeEncoded}${urlEncoded}${afterEncoded} inside.` ); }) ); }); }); it('should autolink multiple URLs', async () => { const source = 'www.example.com\nwww.python.org'; const expected = '
www.example.com\nwww.python.org'; const f = textRendererFactory; const mimeType = 'text/plain'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe(expected); }); }); describe('latexRendererFactory', () => { describe('#mimeTypes', () => { it('should have the text/latex mimeType', () => { expect(latexRendererFactory.mimeTypes).toEqual(['text/latex']); }); }); describe('#safe', () => { it('should be safe', () => { expect(latexRendererFactory.safe).toBe(true); }); }); describe('#createRenderer()', () => { it('should set the textContent of the widget', async () => { const source = 'sumlimits_{i=0}^{infty} \frac{1}{n^2}'; const f = latexRendererFactory; const mimeType = 'text/latex'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.textContent).toBe(source); }); it('should be re-renderable', async () => { const source = 'sumlimits_{i=0}^{infty} \frac{1}{n^2}'; const f = latexRendererFactory; const mimeType = 'text/latex'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); await w.renderModel(model); expect(w.node.textContent).toBe(source); }); }); }); describe('svgRendererFactory', () => { describe('#mimeTypes', () => { it('should have the image/svg+xml mimeType', () => { expect(svgRendererFactory.mimeTypes).toEqual(['image/svg+xml']); }); }); describe('#safe', () => { it('should not be safe', () => { expect(svgRendererFactory.safe).toBe(false); }); }); describe('#createRenderer()', () => { it('should create an img element with the uri encoded svg inline', async () => { const source = ''; const displaySource = ''; const f = svgRendererFactory; const mimeType = 'image/svg+xml'; const model = createModel(mimeType, source, true); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); const imgEl = w.node.getElementsByTagName('img')[0]; expect(imgEl).toBeTruthy(); expect(imgEl.src).toContain(encodeURIComponent(displaySource)); }); }); }); describe('markdownRendererFactory', () => { describe('#mimeTypes', () => { it('should have the text/markdown mimeType', function () { expect(markdownRendererFactory.mimeTypes).toEqual(['text/markdown']); }); }); describe('#safe', () => { it('should be safe', () => { expect(markdownRendererFactory.safe).toBe(true); }); }); describe('#createRenderer()', () => { it('should set the inner html', async () => { const f = markdownRendererFactory; const source = '
hello
'; const mimeType = 'text/markdown'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toBe(source); }); it('should be re-renderable', async () => { const f = markdownRendererFactory; const source = 'hello
'; const mimeType = 'text/markdown'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); await w.renderModel(model); expect(w.node.innerHTML).toBe(source); }); it('should add header anchors', async () => { const f = markdownRendererFactory; const mimeType = 'text/markdown'; const model = createModel(mimeType, sampleData); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); Widget.attach(w, document.body); const node = document.getElementById('Title-third-level')!; expect(node.localName).toBe('h3'); const anchor = node.firstChild!.nextSibling as HTMLAnchorElement; expect(anchor.href).toContain('#Title-third-level'); expect(anchor.target).toBe('_self'); expect(anchor.className).toContain('jp-InternalAnchorLink'); expect(anchor.textContent).toBe('ΒΆ'); Widget.detach(w); }); it('should sanitize the html', async () => { const f = markdownRendererFactory; const source = 'hello
'; const mimeType = 'text/markdown'; const model = createModel(mimeType, source); const w = f.createRenderer({ mimeType, ...defaultOptions }); await w.renderModel(model); expect(w.node.innerHTML).toEqual( expect.not.arrayContaining(['script']) ); }); }); }); describe('htmlRendererFactory', () => { describe('#mimeTypes', () => { it('should have the text/html mimeType', () => { expect(htmlRendererFactory.mimeTypes).toEqual(['text/html']); }); }); describe('#safe', () => { it('should be safe', () => { expect(htmlRendererFactory.safe).toBe(true); }); }); describe('#createRenderer()', () => { it('should set the inner HTML', async () => { const f = htmlRendererFactory; const source = '