diff --git a/packages/rendermime/src/renderers.ts b/packages/rendermime/src/renderers.ts index 65c2a31331b5..b608c723fdd0 100644 --- a/packages/rendermime/src/renderers.ts +++ b/packages/rendermime/src/renderers.ts @@ -796,6 +796,19 @@ export function renderText(options: renderText.IRenderOptions): Promise { return Promise.resolve(undefined); } +/** + * Sanitize HTML out using native browser sanitizer. + * + * Compared to the `ISanitizer.sanitize` this does not allow to selectively + * allow to keep certain tags but escapes everything; on the other hand + * it is much faster as it uses platform-optimized code. + */ +function nativeSanitize(source: string): string { + const el = document.createElement('span'); + el.textContent = source; + return el.innerHTML; +} + /** * Render the textual representation into a host node. * @@ -808,10 +821,16 @@ function renderTextual( // Unpack the options. const { host, sanitizer, source } = options; - // Create the HTML content. - const content = sanitizer.sanitize(Private.ansiSpan(source), { - allowedTags: ['span'] - }); + const ansiPrefixRe = /\x1b/; // eslint-disable-line no-control-regex + const hasAnsiPrefix = ansiPrefixRe.test(source); + + // Create the HTML content: + // If no ANSI codes are present use a fast path for escaping. + const content = hasAnsiPrefix + ? sanitizer.sanitize(Private.ansiSpan(source), { + allowedTags: ['span'] + }) + : nativeSanitize(source); // Set the sanitized content for the host node. const pre = document.createElement('pre'); diff --git a/packages/rendermime/test/factories.spec.ts b/packages/rendermime/test/factories.spec.ts index d9d3db2463e2..0cb732a2578c 100644 --- a/packages/rendermime/test/factories.spec.ts +++ b/packages/rendermime/test/factories.spec.ts @@ -630,6 +630,48 @@ describe('rendermime/factories', () => { expect(end - start).toBeLessThan(timeout); }); + it('should use a fast path when no ANSI codes are present', async () => { + const mimeType = 'application/vnd.jupyter.stderr'; + + const ansiEscape = '\x1b[01;41;32mtext\x1b[00m'; + const notAnsiEscape = '\x1a[01;41;32mtext\x1a[00m'; + + // We cannot just compare times here because: + // a) tests are run in jsdom thus "native" sanitizer is not much faster + // b) `Private.ansiSpan` has much higher cost when ANSI escapes are present + + const testSource = ''; + const spy = jest.spyOn(sanitizer, 'sanitize'); + + // Initialize slow path scenario + let model = createModel(mimeType, testSource + ansiEscape); + let w = errorRendererFactory.createRenderer({ mimeType, ...options }); + expect(spy).toHaveBeenCalledTimes(0); + + // Test slow path + await w.renderModel(model); + // Sanitizer.sanitize should have been called + expect(spy).toHaveBeenCalledTimes(1); + + const escapedSlow = w.node.querySelector('pre')!.innerHTML; + + // Initialize fast path scenario. + model = createModel(mimeType, testSource + notAnsiEscape); + w = errorRendererFactory.createRenderer({ mimeType, ...options }); + + // Test fast path + await w.renderModel(model); + // Sanitizer.sanitize should not have been called + expect(spy).toHaveBeenCalledTimes(1); + + const escapedFast = w.node.querySelector('pre')!.innerHTML; + + // Disregarding the suffix the escaped code should be the same. + expect(escapedFast.slice(0, testSource.length)).toEqual( + escapedSlow.slice(0, testSource.length) + ); + }); + it.each([ ['arrives in a new line', 'www.example.com', '\n a new line of text'], ['arrives after a new line', 'www.example.com\n', 'a new line of text'],