Path: blob/main/src/vs/base/test/browser/markdownRenderer.test.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { fillInIncompleteTokens, renderMarkdown, renderAsPlaintext } from '../../browser/markdownRenderer.js';7import { IMarkdownString, MarkdownString } from '../../common/htmlContent.js';8import * as marked from '../../common/marked/marked.js';9import { parse } from '../../common/marshalling.js';10import { isWeb } from '../../common/platform.js';11import { URI } from '../../common/uri.js';12import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js';1314function strToNode(str: string): HTMLElement {15return new DOMParser().parseFromString(str, 'text/html').body.firstChild as HTMLElement;16}1718function assertNodeEquals(actualNode: HTMLElement, expectedHtml: string) {19const expectedNode = strToNode(expectedHtml);20assert.ok(21actualNode.isEqualNode(expectedNode),22`Expected: ${expectedNode.outerHTML}\nActual: ${actualNode.outerHTML}`);23}2425suite('MarkdownRenderer', () => {2627const store = ensureNoDisposablesAreLeakedInTestSuite();2829suite('Sanitization', () => {30test('Should not render images with unknown schemes', () => {31const markdown = { value: `` };32const result: HTMLElement = store.add(renderMarkdown(markdown)).element;33assert.strictEqual(result.innerHTML, '<p><img alt="image"></p>');34});35});3637suite('Images', () => {38test('image rendering conforms to default', () => {39const markdown = { value: `` };40const result: HTMLElement = store.add(renderMarkdown(markdown)).element;41assertNodeEquals(result, '<div><p><img title="caption" alt="image" src="http://example.com/cat.gif"></p></div>');42});4344test('image rendering conforms to default without title', () => {45const markdown = { value: `` };46const result: HTMLElement = store.add(renderMarkdown(markdown)).element;47assertNodeEquals(result, '<div><p><img alt="image" src="http://example.com/cat.gif"></p></div>');48});4950test('image width from title params', () => {51const result: HTMLElement = store.add(renderMarkdown({ value: `` })).element;52assertNodeEquals(result, `<div><p><img width="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);53});5455test('image height from title params', () => {56const result: HTMLElement = store.add(renderMarkdown({ value: `` })).element;57assertNodeEquals(result, `<div><p><img height="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);58});5960test('image width and height from title params', () => {61const result: HTMLElement = store.add(renderMarkdown({ value: `` })).element;62assertNodeEquals(result, `<div><p><img height="200" width="100" title="caption" alt="image" src="http://example.com/cat.gif"></p></div>`);63});6465test('image with file uri should render as same origin uri', () => {66if (isWeb) {67return;68}69const result: HTMLElement = store.add(renderMarkdown({ value: `` })).element;70assertNodeEquals(result, '<div><p><img src="vscode-file://vscode-app/images/cat.gif" alt="image"></p></div>');71});72});7374suite('Code block renderer', () => {75const simpleCodeBlockRenderer = (lang: string, code: string): Promise<HTMLElement> => {76const element = document.createElement('code');77element.textContent = code;78return Promise.resolve(element);79};8081test('asyncRenderCallback should be invoked for code blocks', () => {82const markdown = { value: '```js\n1 + 1;\n```' };83return new Promise<void>(resolve => {84store.add(renderMarkdown(markdown, {85asyncRenderCallback: resolve,86codeBlockRenderer: simpleCodeBlockRenderer87}));88});89});9091test('asyncRenderCallback should not be invoked if result is immediately disposed', () => {92const markdown = { value: '```js\n1 + 1;\n```' };93return new Promise<void>((resolve, reject) => {94const result = renderMarkdown(markdown, {95asyncRenderCallback: reject,96codeBlockRenderer: simpleCodeBlockRenderer97});98result.dispose();99setTimeout(resolve, 10);100});101});102103test('asyncRenderCallback should not be invoked if dispose is called before code block is rendered', () => {104const markdown = { value: '```js\n1 + 1;\n```' };105return new Promise<void>((resolve, reject) => {106let resolveCodeBlockRendering: (x: HTMLElement) => void;107const result = renderMarkdown(markdown, {108asyncRenderCallback: reject,109codeBlockRenderer: () => {110return new Promise(resolve => {111resolveCodeBlockRendering = resolve;112});113}114});115setTimeout(() => {116result.dispose();117resolveCodeBlockRendering(document.createElement('code'));118setTimeout(resolve, 10);119}, 10);120});121});122123test('Code blocks should use leading language id (#157793)', async () => {124const markdown = { value: '```js some other stuff\n1 + 1;\n```' };125const lang = await new Promise<string>(resolve => {126store.add(renderMarkdown(markdown, {127codeBlockRenderer: async (lang, value) => {128resolve(lang);129return simpleCodeBlockRenderer(lang, value);130}131}));132});133assert.strictEqual(lang, 'js');134});135});136137suite('ThemeIcons Support On', () => {138139test('render appendText', () => {140const mds = new MarkdownString(undefined, { supportThemeIcons: true });141mds.appendText('$(zap) $(not a theme icon) $(add)');142143const result: HTMLElement = store.add(renderMarkdown(mds)).element;144assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) $(add)</p>`);145});146147test('render appendMarkdown', () => {148const mds = new MarkdownString(undefined, { supportThemeIcons: true });149mds.appendMarkdown('$(zap) $(not a theme icon) $(add)');150151const result: HTMLElement = store.add(renderMarkdown(mds)).element;152assert.strictEqual(result.innerHTML, `<p><span class="codicon codicon-zap"></span> $(not a theme icon) <span class="codicon codicon-add"></span></p>`);153});154155test('render appendMarkdown with escaped icon', () => {156const mds = new MarkdownString(undefined, { supportThemeIcons: true });157mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)');158159const result: HTMLElement = store.add(renderMarkdown(mds)).element;160assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) <span class="codicon codicon-add"></span></p>`);161});162163test('render icon in link', () => {164const mds = new MarkdownString(undefined, { supportThemeIcons: true });165mds.appendMarkdown(`[$(zap)-link](#link)`);166167const result: HTMLElement = store.add(renderMarkdown(mds)).element;168assert.strictEqual(result.innerHTML, `<p><a href="" title="#link" draggable="false" data-href="#link"><span class="codicon codicon-zap"></span>-link</a></p>`);169});170171test('render icon in table', () => {172const mds = new MarkdownString(undefined, { supportThemeIcons: true });173mds.appendMarkdown(`174| text | text |175|--------|----------------------|176| $(zap) | [$(zap)-link](#link) |`);177178const result: HTMLElement = store.add(renderMarkdown(mds)).element;179assert.strictEqual(result.innerHTML, `<table>180<thead>181<tr>182<th>text</th>183<th>text</th>184</tr>185</thead>186<tbody><tr>187<td><span class="codicon codicon-zap"></span></td>188<td><a href="" title="#link" draggable="false" data-href="#link"><span class="codicon codicon-zap"></span>-link</a></td>189</tr>190</tbody></table>191`);192});193194test('render icon in <a> without href (#152170)', () => {195const mds = new MarkdownString(undefined, { supportThemeIcons: true, supportHtml: true });196mds.appendMarkdown(`<a>$(sync)</a>`);197198const result: HTMLElement = store.add(renderMarkdown(mds)).element;199assert.strictEqual(result.innerHTML, `<p><span class="codicon codicon-sync"></span></p>`);200});201});202203suite('ThemeIcons Support Off', () => {204205test('render appendText', () => {206const mds = new MarkdownString(undefined, { supportThemeIcons: false });207mds.appendText('$(zap) $(not a theme icon) $(add)');208209const result: HTMLElement = store.add(renderMarkdown(mds)).element;210assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) $(add)</p>`);211});212213test('render appendMarkdown with escaped icon', () => {214const mds = new MarkdownString(undefined, { supportThemeIcons: false });215mds.appendMarkdown('\\$(zap) $(not a theme icon) $(add)');216217const result: HTMLElement = store.add(renderMarkdown(mds)).element;218assert.strictEqual(result.innerHTML, `<p>$(zap) $(not a theme icon) $(add)</p>`);219});220});221222suite('Alerts', () => {223test('Should render alert with data-severity attribute and icon', () => {224const markdown = new MarkdownString('> [!NOTE]\n> This is a note alert', { supportAlertSyntax: true });225const result = store.add(renderMarkdown(markdown)).element;226227const blockquote = result.querySelector('blockquote[data-severity="note"]');228assert.ok(blockquote, 'Should have blockquote with data-severity="note"');229assert.ok(result.innerHTML.includes('This is a note alert'), 'Should contain alert text');230assert.ok(result.innerHTML.includes('codicon-info'), 'Should contain info icon');231});232233test('Should render regular blockquote when supportAlertSyntax is disabled', () => {234const markdown = new MarkdownString('> [!NOTE]\n> This should be a regular blockquote');235const result = store.add(renderMarkdown(markdown)).element;236237const blockquote = result.querySelector('blockquote');238assert.ok(blockquote, 'Should have blockquote');239assert.strictEqual(blockquote?.getAttribute('data-severity'), null, 'Should not have data-severity attribute');240assert.ok(result.innerHTML.includes('[!NOTE]'), 'Should contain literal [!NOTE] text');241});242243test('Should not transform blockquotes without alert syntax', () => {244const markdown = new MarkdownString('> This is a regular blockquote', { supportAlertSyntax: true });245const result = store.add(renderMarkdown(markdown)).element;246247const blockquote = result.querySelector('blockquote');248assert.strictEqual(blockquote?.getAttribute('data-severity'), null, 'Should not have data-severity attribute');249});250});251252test('npm Hover Run Script not working #90855', function () {253254const md: IMarkdownString = JSON.parse('{"value":"[Run Script](command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D \\"Run the script as a task\\")","supportThemeIcons":false,"isTrusted":true,"uris":{"__uri_e49443":{"$mid":1,"fsPath":"c:\\\\Users\\\\jrieken\\\\Code\\\\_sample\\\\foo\\\\package.json","_sep":1,"external":"file:///c%3A/Users/jrieken/Code/_sample/foo/package.json","path":"/c:/Users/jrieken/Code/_sample/foo/package.json","scheme":"file"},"command:npm.runScriptFromHover?%7B%22documentUri%22%3A%7B%22%24mid%22%3A1%2C%22fsPath%22%3A%22c%3A%5C%5CUsers%5C%5Cjrieken%5C%5CCode%5C%5C_sample%5C%5Cfoo%5C%5Cpackage.json%22%2C%22_sep%22%3A1%2C%22external%22%3A%22file%3A%2F%2F%2Fc%253A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22path%22%3A%22%2Fc%3A%2FUsers%2Fjrieken%2FCode%2F_sample%2Ffoo%2Fpackage.json%22%2C%22scheme%22%3A%22file%22%7D%2C%22script%22%3A%22echo%22%7D":{"$mid":1,"path":"npm.runScriptFromHover","scheme":"command","query":"{\\"documentUri\\":\\"__uri_e49443\\",\\"script\\":\\"echo\\"}"}}}');255const element = store.add(renderMarkdown(md)).element;256257const anchor = element.querySelector('a')!;258assert.ok(anchor);259assert.ok(anchor.dataset['href']);260261const uri = URI.parse(anchor.dataset['href']!);262263const data = <{ script: string; documentUri: URI }>parse(decodeURIComponent(uri.query));264assert.ok(data);265assert.strictEqual(data.script, 'echo');266assert.ok(data.documentUri.toString().startsWith('file:///c%3A/'));267});268269test('Should not render command links by default', () => {270const md = new MarkdownString(`[command1](command:doFoo) <a href="command:doFoo">command2</a>`, {271supportHtml: true272});273274const result: HTMLElement = store.add(renderMarkdown(md)).element;275assert.strictEqual(result.innerHTML, `<p>command1 command2</p>`);276});277278test('Should render command links in trusted strings', () => {279const md = new MarkdownString(`[command1](command:doFoo) <a href="command:doFoo">command2</a>`, {280isTrusted: true,281supportHtml: true,282});283284const result: HTMLElement = store.add(renderMarkdown(md)).element;285assert.strictEqual(result.innerHTML, `<p><a href="" title="command:doFoo" draggable="false" data-href="command:doFoo">command1</a> <a href="" data-href="command:doFoo">command2</a></p>`);286});287288test('Should remove relative links if there is no base url', () => {289const md = new MarkdownString(`[text](./foo) <a href="./bar">bar</a>`, {290isTrusted: true,291supportHtml: true,292});293294const result = store.add(renderMarkdown(md)).element;295assert.strictEqual(result.innerHTML, `<p>text bar</p>`);296});297298test('Should support relative links if baseurl is set', () => {299const md = new MarkdownString(`[text](./foo) <a href="./bar">bar</a> <img src="cat.gif">`, {300isTrusted: true,301supportHtml: true,302});303md.baseUri = URI.parse('https://example.com/path/');304305const result = store.add(renderMarkdown(md)).element;306assert.strictEqual(result.innerHTML, `<p><a href="" title="./foo" draggable="false" data-href="https://example.com/path/foo">text</a> <a href="" data-href="https://example.com/path/bar">bar</a> <img src="https://example.com/path/cat.gif"></p>`);307});308309suite('PlaintextMarkdownRender', () => {310311test('test code, blockquote, heading, list, listitem, paragraph, table, tablerow, tablecell, strong, em, br, del, text are rendered plaintext', () => {312const markdown = { value: '`code`\n>quote\n# heading\n- list\n\ntable | table2\n--- | --- \none | two\n\n\nbo**ld**\n_italic_\n~~del~~\nsome text' };313const expected = 'code\nquote\nheading\nlist\n\ntable table2\none two\nbold\nitalic\ndel\nsome text';314const result: string = renderAsPlaintext(markdown);315assert.strictEqual(result, expected);316});317318test('test html, hr, image, link are rendered plaintext', () => {319const markdown = { value: '<div>html</div>\n\n---\n\n[text](textLink)' };320const expected = 'text';321const result: string = renderAsPlaintext(markdown);322assert.strictEqual(result, expected);323});324325test(`Should not remove html inside of code blocks`, () => {326const markdown = {327value: [328'```html',329'<form>html</form>',330'```',331].join('\n')332};333const expected = [334'```',335'<form>html</form>',336'```',337].join('\n');338const result: string = renderAsPlaintext(markdown, { includeCodeBlocksFences: true });339assert.strictEqual(result, expected);340});341});342343suite('supportHtml', () => {344test('supportHtml is disabled by default', () => {345const mds = new MarkdownString(undefined, {});346mds.appendMarkdown('a<b>b</b>c');347348const result = store.add(renderMarkdown(mds)).element;349assert.strictEqual(result.innerHTML, `<p>abc</p>`);350});351352test('Renders html when supportHtml=true', () => {353const mds = new MarkdownString(undefined, { supportHtml: true });354mds.appendMarkdown('a<b>b</b>c');355356const result = store.add(renderMarkdown(mds)).element;357assert.strictEqual(result.innerHTML, `<p>a<b>b</b>c</p>`);358});359360test('Should not include scripts even when supportHtml=true', () => {361const mds = new MarkdownString(undefined, { supportHtml: true });362mds.appendMarkdown('a<b onclick="alert(1)">b</b><script>alert(2)</script>c');363364const result = store.add(renderMarkdown(mds)).element;365assert.strictEqual(result.innerHTML, `<p>a<b>b</b>c</p>`);366});367368test('Should not render html appended as text', () => {369const mds = new MarkdownString(undefined, { supportHtml: true });370mds.appendText('a<b>b</b>c');371372const result = store.add(renderMarkdown(mds)).element;373assert.strictEqual(result.innerHTML, `<p>a<b>b</b>c</p>`);374});375376test('Should render html images', () => {377if (isWeb) {378return;379}380381const mds = new MarkdownString(undefined, { supportHtml: true });382mds.appendMarkdown(`<img src="http://example.com/cat.gif">`);383384const result = store.add(renderMarkdown(mds)).element;385assert.strictEqual(result.innerHTML, `<img src="http://example.com/cat.gif">`);386});387388test('Should render html images with file uri as same origin uri', () => {389if (isWeb) {390return;391}392393const mds = new MarkdownString(undefined, { supportHtml: true });394mds.appendMarkdown(`<img src="file:///images/cat.gif">`);395396const result = store.add(renderMarkdown(mds)).element;397assert.strictEqual(result.innerHTML, `<img src="vscode-file://vscode-app/images/cat.gif">`);398});399400test('Should only allow checkbox inputs', () => {401const mds = new MarkdownString(402'text: <input type="text">\ncheckbox:<input type="checkbox">',403{ supportHtml: true });404405const result = store.add(renderMarkdown(mds)).element;406407// Inputs should always be disabled too408assert.strictEqual(result.innerHTML, `<p>text: \ncheckbox:<input type="checkbox" disabled=""></p>`);409});410});411412suite('fillInIncompleteTokens', () => {413function ignoreRaw(...tokenLists: marked.Token[][]): void {414tokenLists.forEach(tokens => {415tokens.forEach(t => t.raw = '');416});417}418419const completeTable = '| a | b |\n| --- | --- |';420421suite('table', () => {422test('complete table', () => {423const tokens = marked.marked.lexer(completeTable);424const newTokens = fillInIncompleteTokens(tokens);425assert.equal(newTokens, tokens);426});427428test('full header only', () => {429const incompleteTable = '| a | b |';430const tokens = marked.marked.lexer(incompleteTable);431const completeTableTokens = marked.marked.lexer(completeTable);432433const newTokens = fillInIncompleteTokens(tokens);434assert.deepStrictEqual(newTokens, completeTableTokens);435});436437test('full header only with trailing space', () => {438const incompleteTable = '| a | b | ';439const tokens = marked.marked.lexer(incompleteTable);440const completeTableTokens = marked.marked.lexer(completeTable);441442const newTokens = fillInIncompleteTokens(tokens);443if (newTokens) {444ignoreRaw(newTokens, completeTableTokens);445}446assert.deepStrictEqual(newTokens, completeTableTokens);447});448449test('incomplete header', () => {450const incompleteTable = '| a | b';451const tokens = marked.marked.lexer(incompleteTable);452const completeTableTokens = marked.marked.lexer(completeTable);453454const newTokens = fillInIncompleteTokens(tokens);455456if (newTokens) {457ignoreRaw(newTokens, completeTableTokens);458}459assert.deepStrictEqual(newTokens, completeTableTokens);460});461462test('incomplete header one column', () => {463const incompleteTable = '| a ';464const tokens = marked.marked.lexer(incompleteTable);465const completeTableTokens = marked.marked.lexer(incompleteTable + '|\n| --- |');466467const newTokens = fillInIncompleteTokens(tokens);468469if (newTokens) {470ignoreRaw(newTokens, completeTableTokens);471}472assert.deepStrictEqual(newTokens, completeTableTokens);473});474475test('full header with extras', () => {476const incompleteTable = '| a **bold** | b _italics_ |';477const tokens = marked.marked.lexer(incompleteTable);478const completeTableTokens = marked.marked.lexer(incompleteTable + '\n| --- | --- |');479480const newTokens = fillInIncompleteTokens(tokens);481assert.deepStrictEqual(newTokens, completeTableTokens);482});483484test('full header with leading text', () => {485// Parsing this gives one token and one 'text' subtoken486const incompleteTable = 'here is a table\n| a | b |';487const tokens = marked.marked.lexer(incompleteTable);488const completeTableTokens = marked.marked.lexer(incompleteTable + '\n| --- | --- |');489490const newTokens = fillInIncompleteTokens(tokens);491assert.deepStrictEqual(newTokens, completeTableTokens);492});493494test('full header with leading other stuff', () => {495// Parsing this gives one token and one 'text' subtoken496const incompleteTable = '```js\nconst xyz = 123;\n```\n| a | b |';497const tokens = marked.marked.lexer(incompleteTable);498const completeTableTokens = marked.marked.lexer(incompleteTable + '\n| --- | --- |');499500const newTokens = fillInIncompleteTokens(tokens);501assert.deepStrictEqual(newTokens, completeTableTokens);502});503504test('full header with incomplete separator', () => {505const incompleteTable = '| a | b |\n| ---';506const tokens = marked.marked.lexer(incompleteTable);507const completeTableTokens = marked.marked.lexer(completeTable);508509const newTokens = fillInIncompleteTokens(tokens);510assert.deepStrictEqual(newTokens, completeTableTokens);511});512513test('full header with incomplete separator 2', () => {514const incompleteTable = '| a | b |\n| --- |';515const tokens = marked.marked.lexer(incompleteTable);516const completeTableTokens = marked.marked.lexer(completeTable);517518const newTokens = fillInIncompleteTokens(tokens);519assert.deepStrictEqual(newTokens, completeTableTokens);520});521522test('full header with incomplete separator 3', () => {523const incompleteTable = '| a | b |\n|';524const tokens = marked.marked.lexer(incompleteTable);525const completeTableTokens = marked.marked.lexer(completeTable);526527const newTokens = fillInIncompleteTokens(tokens);528assert.deepStrictEqual(newTokens, completeTableTokens);529});530531test('not a table', () => {532const incompleteTable = '| a | b |\nsome text';533const tokens = marked.marked.lexer(incompleteTable);534535const newTokens = fillInIncompleteTokens(tokens);536assert.deepStrictEqual(newTokens, tokens);537});538539test('not a table 2', () => {540const incompleteTable = '| a | b |\n| --- |\nsome text';541const tokens = marked.marked.lexer(incompleteTable);542543const newTokens = fillInIncompleteTokens(tokens);544assert.deepStrictEqual(newTokens, tokens);545});546});547548function simpleMarkdownTestSuite(name: string, delimiter: string): void {549test(`incomplete ${name}`, () => {550const incomplete = `${delimiter}code`;551const tokens = marked.marked.lexer(incomplete);552const newTokens = fillInIncompleteTokens(tokens);553554const completeTokens = marked.marked.lexer(incomplete + delimiter);555assert.deepStrictEqual(newTokens, completeTokens);556});557558test(`complete ${name}`, () => {559const text = `leading text ${delimiter}code${delimiter} trailing text`;560const tokens = marked.marked.lexer(text);561const newTokens = fillInIncompleteTokens(tokens);562563assert.deepStrictEqual(newTokens, tokens);564});565566test(`${name} with leading text`, () => {567const incomplete = `some text and ${delimiter}some code`;568const tokens = marked.marked.lexer(incomplete);569const newTokens = fillInIncompleteTokens(tokens);570571const completeTokens = marked.marked.lexer(incomplete + delimiter);572assert.deepStrictEqual(newTokens, completeTokens);573});574575test(`${name} with trailing space`, () => {576const incomplete = `some text and ${delimiter}some code `;577const tokens = marked.marked.lexer(incomplete);578const newTokens = fillInIncompleteTokens(tokens);579580const completeTokens = marked.marked.lexer(incomplete.trimEnd() + delimiter);581assert.deepStrictEqual(newTokens, completeTokens);582});583584test(`single loose "${delimiter}"`, () => {585const text = `some text and ${delimiter}by itself\nmore text here`;586const tokens = marked.marked.lexer(text);587const newTokens = fillInIncompleteTokens(tokens);588589assert.deepStrictEqual(newTokens, tokens);590});591592test(`incomplete ${name} after newline`, () => {593const text = `some text\nmore text here and ${delimiter}text`;594const tokens = marked.marked.lexer(text);595const newTokens = fillInIncompleteTokens(tokens);596597const completeTokens = marked.marked.lexer(text + delimiter);598assert.deepStrictEqual(newTokens, completeTokens);599});600601test(`incomplete after complete ${name}`, () => {602const text = `leading text ${delimiter}code${delimiter} trailing text and ${delimiter}another`;603const tokens = marked.marked.lexer(text);604const newTokens = fillInIncompleteTokens(tokens);605606const completeTokens = marked.marked.lexer(text + delimiter);607assert.deepStrictEqual(newTokens, completeTokens);608});609610test(`incomplete ${name} in list`, () => {611const text = `- list item one\n- list item two and ${delimiter}text`;612const tokens = marked.marked.lexer(text);613const newTokens = fillInIncompleteTokens(tokens);614615const completeTokens = marked.marked.lexer(text + delimiter);616assert.deepStrictEqual(newTokens, completeTokens);617});618619test(`incomplete ${name} in asterisk list`, () => {620const text = `* list item one\n* list item two and ${delimiter}text`;621const tokens = marked.marked.lexer(text);622const newTokens = fillInIncompleteTokens(tokens);623624const completeTokens = marked.marked.lexer(text + delimiter);625assert.deepStrictEqual(newTokens, completeTokens);626});627628test(`incomplete ${name} in numbered list`, () => {629const text = `1. list item one\n2. list item two and ${delimiter}text`;630const tokens = marked.marked.lexer(text);631const newTokens = fillInIncompleteTokens(tokens);632633const completeTokens = marked.marked.lexer(text + delimiter);634assert.deepStrictEqual(newTokens, completeTokens);635});636}637638suite('list', () => {639test('list with complete codeblock', () => {640const list = `-641\`\`\`js642let x = 1;643\`\`\`644- list item two645`;646const tokens = marked.marked.lexer(list);647const newTokens = fillInIncompleteTokens(tokens);648649assert.deepStrictEqual(newTokens, tokens);650});651652test.skip('list with incomplete codeblock', () => {653const incomplete = `- list item one654655\`\`\`js656let x = 1;`;657const tokens = marked.marked.lexer(incomplete);658const newTokens = fillInIncompleteTokens(tokens);659660const completeTokens = marked.marked.lexer(incomplete + '\n ```');661assert.deepStrictEqual(newTokens, completeTokens);662});663664test('list with subitems', () => {665const list = `- hello666- sub item667- text668newline for some reason669`;670const tokens = marked.marked.lexer(list);671const newTokens = fillInIncompleteTokens(tokens);672673assert.deepStrictEqual(newTokens, tokens);674});675676test('ordered list with subitems', () => {677const list = `1. hello678- sub item6792. text680newline for some reason681`;682const tokens = marked.marked.lexer(list);683const newTokens = fillInIncompleteTokens(tokens);684685assert.deepStrictEqual(newTokens, tokens);686});687688test('list with stuff', () => {689const list = `- list item one \`codespan\` **bold** [link](http://microsoft.com) more text`;690const tokens = marked.marked.lexer(list);691const newTokens = fillInIncompleteTokens(tokens);692693assert.deepStrictEqual(newTokens, tokens);694});695696test('list with incomplete link text', () => {697const incomplete = `- list item one698- item two [link`;699const tokens = marked.marked.lexer(incomplete);700const newTokens = fillInIncompleteTokens(tokens);701702const completeTokens = marked.marked.lexer(incomplete + '](https://microsoft.com)');703assert.deepStrictEqual(newTokens, completeTokens);704});705706test('list with incomplete link target', () => {707const incomplete = `- list item one708- item two [link](`;709const tokens = marked.marked.lexer(incomplete);710const newTokens = fillInIncompleteTokens(tokens);711712const completeTokens = marked.marked.lexer(incomplete + ')');713assert.deepStrictEqual(newTokens, completeTokens);714});715716test('ordered list with incomplete link target', () => {717const incomplete = `1. list item one7182. item two [link](`;719const tokens = marked.marked.lexer(incomplete);720const newTokens = fillInIncompleteTokens(tokens);721722const completeTokens = marked.marked.lexer(incomplete + ')');723assert.deepStrictEqual(newTokens, completeTokens);724});725726test('ordered list with extra whitespace', () => {727const incomplete = `1. list item one7282. item two [link](`;729const tokens = marked.marked.lexer(incomplete);730const newTokens = fillInIncompleteTokens(tokens);731732const completeTokens = marked.marked.lexer(incomplete + ')');733assert.deepStrictEqual(newTokens, completeTokens);734});735736test('list with extra whitespace', () => {737const incomplete = `- list item one738- item two [link](`;739const tokens = marked.marked.lexer(incomplete);740const newTokens = fillInIncompleteTokens(tokens);741742const completeTokens = marked.marked.lexer(incomplete + ')');743assert.deepStrictEqual(newTokens, completeTokens);744});745746test('list with incomplete link with other stuff', () => {747const incomplete = `- list item one748- item two [\`link`;749const tokens = marked.marked.lexer(incomplete);750const newTokens = fillInIncompleteTokens(tokens);751752const completeTokens = marked.marked.lexer(incomplete + '\`](https://microsoft.com)');753assert.deepStrictEqual(newTokens, completeTokens);754});755756test('ordered list with incomplete link with other stuff', () => {757const incomplete = `1. list item one7581. item two [\`link`;759const tokens = marked.marked.lexer(incomplete);760const newTokens = fillInIncompleteTokens(tokens);761762const completeTokens = marked.marked.lexer(incomplete + '\`](https://microsoft.com)');763assert.deepStrictEqual(newTokens, completeTokens);764});765766test('list with incomplete subitem', () => {767const incomplete = `1. list item one768- `;769const tokens = marked.marked.lexer(incomplete);770const newTokens = fillInIncompleteTokens(tokens);771772const completeTokens = marked.marked.lexer(incomplete + ' ');773assert.deepStrictEqual(newTokens, completeTokens);774});775776test('list with incomplete nested subitem', () => {777const incomplete = `1. list item one778- item 2779- `;780const tokens = marked.marked.lexer(incomplete);781const newTokens = fillInIncompleteTokens(tokens);782783const completeTokens = marked.marked.lexer(incomplete + ' ');784assert.deepStrictEqual(newTokens, completeTokens);785});786787test('text with start of list is not a heading', () => {788const incomplete = `hello\n- `;789const tokens = marked.marked.lexer(incomplete);790const newTokens = fillInIncompleteTokens(tokens);791792const completeTokens = marked.marked.lexer(incomplete + ' ');793assert.deepStrictEqual(newTokens, completeTokens);794});795796test('even more text with start of list is not a heading', () => {797const incomplete = `# hello\n\ntext\n-`;798const tokens = marked.marked.lexer(incomplete);799const newTokens = fillInIncompleteTokens(tokens);800801const completeTokens = marked.marked.lexer(incomplete + ' ');802assert.deepStrictEqual(newTokens, completeTokens);803});804});805806suite('codespan', () => {807simpleMarkdownTestSuite('codespan', '`');808809test(`backtick between letters`, () => {810const text = 'a`b';811const tokens = marked.marked.lexer(text);812const newTokens = fillInIncompleteTokens(tokens);813814const completeCodespanTokens = marked.marked.lexer(text + '`');815assert.deepStrictEqual(newTokens, completeCodespanTokens);816});817818test(`nested pattern`, () => {819const text = 'sldkfjsd `abc __def__ ghi';820const tokens = marked.marked.lexer(text);821const newTokens = fillInIncompleteTokens(tokens);822823const completeTokens = marked.marked.lexer(text + '`');824assert.deepStrictEqual(newTokens, completeTokens);825});826});827828suite('star', () => {829simpleMarkdownTestSuite('star', '*');830831test(`star between letters`, () => {832const text = 'sldkfjsd a*b';833const tokens = marked.marked.lexer(text);834const newTokens = fillInIncompleteTokens(tokens);835836const completeTokens = marked.marked.lexer(text + '*');837assert.deepStrictEqual(newTokens, completeTokens);838});839840test(`nested pattern`, () => {841const text = 'sldkfjsd *abc __def__ ghi';842const tokens = marked.marked.lexer(text);843const newTokens = fillInIncompleteTokens(tokens);844845const completeTokens = marked.marked.lexer(text + '*');846assert.deepStrictEqual(newTokens, completeTokens);847});848});849850suite('double star', () => {851simpleMarkdownTestSuite('double star', '**');852853test(`double star between letters`, () => {854const text = 'a**b';855const tokens = marked.marked.lexer(text);856const newTokens = fillInIncompleteTokens(tokens);857858const completeTokens = marked.marked.lexer(text + '**');859assert.deepStrictEqual(newTokens, completeTokens);860});861862// TODO trim these patterns from end863test.skip(`ending in doublestar`, () => {864const incomplete = `some text and **`;865const tokens = marked.marked.lexer(incomplete);866const newTokens = fillInIncompleteTokens(tokens);867868const completeTokens = marked.marked.lexer(incomplete.trimEnd() + '**');869assert.deepStrictEqual(newTokens, completeTokens);870});871});872873suite('underscore', () => {874simpleMarkdownTestSuite('underscore', '_');875876test(`underscore between letters`, () => {877const text = `this_not_italics`;878const tokens = marked.marked.lexer(text);879const newTokens = fillInIncompleteTokens(tokens);880881assert.deepStrictEqual(newTokens, tokens);882});883});884885suite('double underscore', () => {886simpleMarkdownTestSuite('double underscore', '__');887888test(`double underscore between letters`, () => {889const text = `this__not__bold`;890const tokens = marked.marked.lexer(text);891const newTokens = fillInIncompleteTokens(tokens);892893assert.deepStrictEqual(newTokens, tokens);894});895});896897suite('link', () => {898test('incomplete link text', () => {899const incomplete = 'abc [text';900const tokens = marked.marked.lexer(incomplete);901const newTokens = fillInIncompleteTokens(tokens);902903const completeTokens = marked.marked.lexer(incomplete + '](https://microsoft.com)');904assert.deepStrictEqual(newTokens, completeTokens);905});906907test('incomplete link target', () => {908const incomplete = 'foo [text](http://microsoft';909const tokens = marked.marked.lexer(incomplete);910const newTokens = fillInIncompleteTokens(tokens);911912const completeTokens = marked.marked.lexer(incomplete + ')');913assert.deepStrictEqual(newTokens, completeTokens);914});915916test('incomplete link target 2', () => {917const incomplete = 'foo [text](http://microsoft.com';918const tokens = marked.marked.lexer(incomplete);919const newTokens = fillInIncompleteTokens(tokens);920921const completeTokens = marked.marked.lexer(incomplete + ')');922assert.deepStrictEqual(newTokens, completeTokens);923});924925test('incomplete link target with extra stuff', () => {926const incomplete = '[before `text` after](http://microsoft.com';927const tokens = marked.marked.lexer(incomplete);928const newTokens = fillInIncompleteTokens(tokens);929930const completeTokens = marked.marked.lexer(incomplete + ')');931assert.deepStrictEqual(newTokens, completeTokens);932});933934test('incomplete link target with extra stuff and incomplete arg', () => {935const incomplete = '[before `text` after](http://microsoft.com "more text ';936const tokens = marked.marked.lexer(incomplete);937const newTokens = fillInIncompleteTokens(tokens);938939const completeTokens = marked.marked.lexer(incomplete + '")');940assert.deepStrictEqual(newTokens, completeTokens);941});942943test('incomplete link target with incomplete arg', () => {944const incomplete = 'foo [text](http://microsoft.com "more text here ';945const tokens = marked.marked.lexer(incomplete);946const newTokens = fillInIncompleteTokens(tokens);947948const completeTokens = marked.marked.lexer(incomplete + '")');949assert.deepStrictEqual(newTokens, completeTokens);950});951952test('incomplete link target with incomplete arg 2', () => {953const incomplete = '[text](command:vscode.openRelativePath "arg';954const tokens = marked.marked.lexer(incomplete);955const newTokens = fillInIncompleteTokens(tokens);956957const completeTokens = marked.marked.lexer(incomplete + '")');958assert.deepStrictEqual(newTokens, completeTokens);959});960961test('incomplete link target with complete arg', () => {962const incomplete = 'foo [text](http://microsoft.com "more text here"';963const tokens = marked.marked.lexer(incomplete);964const newTokens = fillInIncompleteTokens(tokens);965966const completeTokens = marked.marked.lexer(incomplete + ')');967assert.deepStrictEqual(newTokens, completeTokens);968});969970test('link text with incomplete codespan', () => {971const incomplete = `text [\`codespan`;972const tokens = marked.marked.lexer(incomplete);973const newTokens = fillInIncompleteTokens(tokens);974975const completeTokens = marked.marked.lexer(incomplete + '`](https://microsoft.com)');976assert.deepStrictEqual(newTokens, completeTokens);977});978979test('link text with incomplete stuff', () => {980const incomplete = `text [more text \`codespan\` text **bold`;981const tokens = marked.marked.lexer(incomplete);982const newTokens = fillInIncompleteTokens(tokens);983984const completeTokens = marked.marked.lexer(incomplete + '**](https://microsoft.com)');985assert.deepStrictEqual(newTokens, completeTokens);986});987988test('Looks like incomplete link target but isn\'t', () => {989const complete = '**bold** `codespan` text](';990const tokens = marked.marked.lexer(complete);991const newTokens = fillInIncompleteTokens(tokens);992993const completeTokens = marked.marked.lexer(complete);994assert.deepStrictEqual(newTokens, completeTokens);995});996997test.skip('incomplete link in list', () => {998const incomplete = '- [text';999const tokens = marked.marked.lexer(incomplete);1000const newTokens = fillInIncompleteTokens(tokens);10011002const completeTokens = marked.marked.lexer(incomplete + '](https://microsoft.com)');1003assert.deepStrictEqual(newTokens, completeTokens);1004});10051006test('square brace between letters', () => {1007const incomplete = 'a[b';1008const tokens = marked.marked.lexer(incomplete);1009const newTokens = fillInIncompleteTokens(tokens);10101011assert.deepStrictEqual(newTokens, tokens);1012});10131014test('square brace on previous line', () => {1015const incomplete = 'text[\nmore text';1016const tokens = marked.marked.lexer(incomplete);1017const newTokens = fillInIncompleteTokens(tokens);10181019assert.deepStrictEqual(newTokens, tokens);1020});10211022test('square braces in text', () => {1023const incomplete = 'hello [what] is going on';1024const tokens = marked.marked.lexer(incomplete);1025const newTokens = fillInIncompleteTokens(tokens);10261027assert.deepStrictEqual(newTokens, tokens);1028});10291030test('complete link', () => {1031const incomplete = 'text [link](http://microsoft.com)';1032const tokens = marked.marked.lexer(incomplete);1033const newTokens = fillInIncompleteTokens(tokens);10341035assert.deepStrictEqual(newTokens, tokens);1036});1037});1038});1039});104010411042