chore(vscode): update to 1.53.2
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
This commit is contained in:
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
!function(e){var t={};function n(s){if(t[s])return t[s].exports;var o=t[s]={i:s,l:!1,exports:{}};return e[s].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,s){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(s,o,function(t){return e[t]}.bind(null,o));return s},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});let s=void 0;function o(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error(`Could not load data for ${e}`)}t.getData=o,t.getSettings=function(){if(s)return s;if(s=o("data-settings"))return s;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const s=n(12),o=n(14);window.cspAlerter=new s.CspAlerter,window.styleLoadingMonitor=new o.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const s=n(0),o=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=o.getStrings(),t=s.getSettings();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]);
|
||||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var s=t[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(o,s,function(t){return e[t]}.bind(null,s));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSettings=t.getData=void 0;let o=void 0;function s(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error("Could not load data for "+e)}t.getData=s,t.getSettings=function(){if(o)return o;if(o=s("data-settings"),o)return o;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(12),s=n(14);window.cspAlerter=new o.CspAlerter,window.styleLoadingMonitor=new s.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CspAlerter=void 0;const o=n(0),s=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=s.getStrings(),t=o.getSettings();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=void 0,t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StyleLoadingMonitor=void 0;t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]);
|
@ -324,24 +324,17 @@
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "10.1.2",
|
||||
"markdown-it": "^10.0.0",
|
||||
"highlight.js": "^10.4.1",
|
||||
"markdown-it": "^12.0.3",
|
||||
"markdown-it-front-matter": "^0.2.1",
|
||||
"vscode-extension-telemetry": "0.1.1",
|
||||
"vscode-nls": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/highlight.js": "9.12.3",
|
||||
"@types/highlight.js": "10.1.0",
|
||||
"@types/lodash.throttle": "^4.1.3",
|
||||
"@types/markdown-it": "0.0.2",
|
||||
"@types/node": "^12.11.7",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.7.3",
|
||||
"vscode": "^1.1.10",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.0"
|
||||
"@types/node": "^12.19.9",
|
||||
"lodash.throttle": "^4.1.1"
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import throttle = require('lodash.throttle');
|
||||
|
||||
declare let acquireVsCodeApi: any;
|
||||
|
||||
let scrollDisabled = true;
|
||||
let scrollDisabledCount = 0;
|
||||
const marker = new ActiveLineMarker();
|
||||
const settings = getSettings();
|
||||
|
||||
@ -37,19 +37,39 @@ window.onload = () => {
|
||||
updateImageSizes();
|
||||
};
|
||||
|
||||
|
||||
function doAfterImagesLoaded(cb: () => void) {
|
||||
const imgElements = document.getElementsByTagName('img');
|
||||
if (imgElements.length > 0) {
|
||||
const ps = Array.from(imgElements).map(e => {
|
||||
if (e.complete) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return new Promise<void>((resolve) => {
|
||||
e.addEventListener('load', () => resolve());
|
||||
e.addEventListener('error', () => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
Promise.all(ps).then(() => setImmediate(cb));
|
||||
} else {
|
||||
setImmediate(cb);
|
||||
}
|
||||
}
|
||||
|
||||
onceDocumentLoaded(() => {
|
||||
const scrollProgress = state.scrollProgress;
|
||||
|
||||
if (typeof scrollProgress === 'number' && !settings.fragment) {
|
||||
setImmediate(() => {
|
||||
scrollDisabled = true;
|
||||
doAfterImagesLoaded(() => {
|
||||
scrollDisabledCount += 1;
|
||||
window.scrollTo(0, scrollProgress * document.body.clientHeight);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.scrollPreviewWithEditor) {
|
||||
setImmediate(() => {
|
||||
doAfterImagesLoaded(() => {
|
||||
// Try to scroll to fragment if available
|
||||
if (settings.fragment) {
|
||||
state.fragment = undefined;
|
||||
@ -57,12 +77,12 @@ onceDocumentLoaded(() => {
|
||||
|
||||
const element = getLineElementForFragment(settings.fragment);
|
||||
if (element) {
|
||||
scrollDisabled = true;
|
||||
scrollDisabledCount += 1;
|
||||
scrollToRevealSourceLine(element.line);
|
||||
}
|
||||
} else {
|
||||
if (!isNaN(settings.line!)) {
|
||||
scrollDisabled = true;
|
||||
scrollDisabledCount += 1;
|
||||
scrollToRevealSourceLine(settings.line!);
|
||||
}
|
||||
}
|
||||
@ -72,8 +92,8 @@ onceDocumentLoaded(() => {
|
||||
|
||||
const onUpdateView = (() => {
|
||||
const doScroll = throttle((line: number) => {
|
||||
scrollDisabled = true;
|
||||
scrollToRevealSourceLine(line);
|
||||
scrollDisabledCount += 1;
|
||||
doAfterImagesLoaded(() => scrollToRevealSourceLine(line));
|
||||
}, 50);
|
||||
|
||||
return (line: number) => {
|
||||
@ -109,7 +129,7 @@ let updateImageSizes = throttle(() => {
|
||||
}, 50);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
scrollDisabled = true;
|
||||
scrollDisabledCount += 1;
|
||||
updateScrollProgress();
|
||||
updateImageSizes();
|
||||
}, true);
|
||||
@ -189,8 +209,8 @@ document.addEventListener('click', event => {
|
||||
window.addEventListener('scroll', throttle(() => {
|
||||
updateScrollProgress();
|
||||
|
||||
if (scrollDisabled) {
|
||||
scrollDisabled = false;
|
||||
if (scrollDisabledCount > 0) {
|
||||
scrollDisabledCount -= 1;
|
||||
} else {
|
||||
const line = getEditorLineNumberForPageOffset(window.scrollY);
|
||||
if (typeof line === 'number' && !isNaN(line)) {
|
||||
|
@ -143,6 +143,7 @@ export function scrollToRevealSourceLine(line: number) {
|
||||
const progressInElement = line - Math.floor(line);
|
||||
scrollTo = previousTop + (rect.height * progressInElement);
|
||||
}
|
||||
scrollTo = Math.abs(scrollTo) < 1 ? Math.sign(scrollTo) : scrollTo;
|
||||
window.scroll(window.scrollX, Math.max(1, window.scrollY + scrollTo));
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,6 @@ export class RenderDocument implements Command {
|
||||
) { }
|
||||
|
||||
public async execute(document: SkinnyTextDocument | string): Promise<string> {
|
||||
return this.engine.render(document);
|
||||
return (await (this.engine.render(document))).html;
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,9 @@ import { isMarkdownFile } from '../util/file';
|
||||
import { normalizeResource, WebviewResourceProvider } from '../util/resources';
|
||||
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { urlToUri } from '../util/url';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@ -90,7 +91,7 @@ class StartingScrollLine {
|
||||
) { }
|
||||
}
|
||||
|
||||
class StartingScrollFragment {
|
||||
export class StartingScrollFragment {
|
||||
public readonly type = 'fragment';
|
||||
|
||||
constructor(
|
||||
@ -118,6 +119,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
private _disposed: boolean = false;
|
||||
private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
|
||||
|
||||
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
|
||||
|
||||
constructor(
|
||||
webview: vscode.WebviewPanel,
|
||||
resource: vscode.Uri,
|
||||
@ -156,6 +159,16 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
}));
|
||||
|
||||
const watcher = this._register(vscode.workspace.createFileSystemWatcher(resource.fsPath));
|
||||
this._register(watcher.onDidChange(uri => {
|
||||
if (this.isPreviewOf(uri)) {
|
||||
// Only use the file system event when VS Code does not already know about the file
|
||||
if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() !== uri.toString())) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => {
|
||||
if (e.source !== this._resource.toString()) {
|
||||
return;
|
||||
@ -198,6 +211,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
super.dispose();
|
||||
this._disposed = true;
|
||||
clearTimeout(this.throttleTimer);
|
||||
for (const entry of this._fileWatchersBySrc.values()) {
|
||||
entry.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public get resource(): vscode.Uri {
|
||||
@ -214,6 +230,10 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The first call immediately refreshes the preview,
|
||||
* calls happening shortly thereafter are debounced.
|
||||
*/
|
||||
public refresh() {
|
||||
// Schedule update if none is pending
|
||||
if (!this.throttleTimer) {
|
||||
@ -350,7 +370,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
this._webviewPanel.webview.html = this._contentProvider.provideFileNotFoundContent(this._resource);
|
||||
}
|
||||
|
||||
private setContent(html: string): void {
|
||||
private setContent(content: MarkdownContentProviderOutput): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
@ -361,7 +381,30 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
this._webviewPanel.iconPath = this.iconPath;
|
||||
this._webviewPanel.webview.options = this.getWebviewOptions();
|
||||
|
||||
this._webviewPanel.webview.html = html;
|
||||
this._webviewPanel.webview.html = content.html;
|
||||
|
||||
const srcs = new Set(content.containingImages.map(img => img.src));
|
||||
|
||||
// Delete stale file watchers.
|
||||
for (const [src, watcher] of [...this._fileWatchersBySrc]) {
|
||||
if (!srcs.has(src)) {
|
||||
watcher.dispose();
|
||||
this._fileWatchersBySrc.delete(src);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new file watchers.
|
||||
const root = vscode.Uri.joinPath(this._resource, '../');
|
||||
for (const src of srcs) {
|
||||
const uri = urlToUri(src, root);
|
||||
if (uri && uri.scheme === 'file' && !this._fileWatchersBySrc.has(src)) {
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(uri.fsPath);
|
||||
watcher.onDidChange(() => {
|
||||
this.refresh();
|
||||
});
|
||||
this._fileWatchersBySrc.set(src, watcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getWebviewOptions(): vscode.WebviewOptions {
|
||||
|
@ -39,6 +39,12 @@ function escapeAttribute(value: string | vscode.Uri): string {
|
||||
return value.toString().replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export interface MarkdownContentProviderOutput {
|
||||
html: string;
|
||||
containingImages: { src: string }[];
|
||||
}
|
||||
|
||||
|
||||
export class MarkdownContentProvider {
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine,
|
||||
@ -54,11 +60,12 @@ export class MarkdownContentProvider {
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
initialLine: number | undefined = undefined,
|
||||
state?: any
|
||||
): Promise<string> {
|
||||
): Promise<MarkdownContentProviderOutput> {
|
||||
const sourceUri = markdownDocument.uri;
|
||||
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
|
||||
const initialData = {
|
||||
source: sourceUri.toString(),
|
||||
fragment: state?.fragment || markdownDocument.uri.fragment || undefined,
|
||||
line: initialLine,
|
||||
lineCount: markdownDocument.lineCount,
|
||||
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
|
||||
@ -75,7 +82,7 @@ export class MarkdownContentProvider {
|
||||
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
|
||||
|
||||
const body = await this.engine.render(markdownDocument);
|
||||
return `<!DOCTYPE html>
|
||||
const html = `<!DOCTYPE html>
|
||||
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
@ -89,11 +96,15 @@ export class MarkdownContentProvider {
|
||||
<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">
|
||||
</head>
|
||||
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body}
|
||||
${body.html}
|
||||
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
|
||||
${this.getScripts(resourceProvider, nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
return {
|
||||
html,
|
||||
containingImages: body.containingImages,
|
||||
};
|
||||
}
|
||||
|
||||
public provideFileNotFoundContent(
|
||||
|
@ -9,7 +9,7 @@ import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { Disposable, disposeAll } from '../util/dispose';
|
||||
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
|
||||
@ -106,7 +106,10 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
preview = this.createNewDynamicPreview(resource, settings);
|
||||
}
|
||||
|
||||
preview.update(resource);
|
||||
preview.update(
|
||||
resource,
|
||||
resource.fragment ? new StartingScrollFragment(resource.fragment) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
public get activePreviewResource() {
|
||||
|
@ -162,9 +162,9 @@ function createFencedRange(token: Token, cursorLine: number, document: vscode.Te
|
||||
}
|
||||
|
||||
function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const regex = /(?:^|(?<=\s))(?:\*\*\s*([^*]+)(?:\*\s*([^*]+)\s*?\*)*([^*]+)\s*?\*\*)/g;
|
||||
const regex = /(?:\*\*([^*]+)(?:\*([^*]+)([^*]+)\*)*([^*]+)\*\*)/g;
|
||||
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
if (matches.length > 0) {
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and index 0 contains the entire match
|
||||
const bold = matches[0][0];
|
||||
const startIndex = lineText.indexOf(bold);
|
||||
@ -177,11 +177,20 @@ function createBoldRange(lineText: string, cursorChar: number, cursorLine: numbe
|
||||
}
|
||||
|
||||
function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const regex = isItalic ? /(?:^|(?<=\s))(?:\*\s*([^*]+)(?:\*\*\s*([^*]+)\s*?\*\*)*([^*]+)\s*?\*)/g : /\`[^\`]*\`/g;
|
||||
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
if (matches.length > 0) {
|
||||
// should only be one match, so select first and index 0 contains the entire match
|
||||
const match = matches[0][0];
|
||||
const italicRegexes = [/(?:[^*]+)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]+)/g, /^(?:[^*]*)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]*)$/g];
|
||||
let matches = [];
|
||||
if (isItalic) {
|
||||
matches = [...lineText.matchAll(italicRegexes[0])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
if (!matches.length) {
|
||||
matches = [...lineText.matchAll(italicRegexes[1])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
}
|
||||
} else {
|
||||
matches = [...lineText.matchAll(/\`[^\`]*\`/g)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
|
||||
}
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and select group 1 for italics because that contains just the italic section
|
||||
// doesn't include the leading and trailing characters which are guaranteed to not be * so as not to be confused with bold
|
||||
const match = isItalic ? matches[0][1] : matches[0][0];
|
||||
const startIndex = lineText.indexOf(match);
|
||||
const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length;
|
||||
const contentAndType = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + match.length), parent);
|
||||
@ -195,7 +204,7 @@ function createLinkRange(lineText: string, cursorChar: number, cursorLine: numbe
|
||||
const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g;
|
||||
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar);
|
||||
|
||||
if (matches.length > 0) {
|
||||
if (matches.length) {
|
||||
// should only be one match, so select first and index 0 contains the entire match, so match = [text](url)
|
||||
const link = matches[0][0];
|
||||
const linkRange = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent);
|
||||
|
@ -54,6 +54,15 @@ class TokenCache {
|
||||
}
|
||||
}
|
||||
|
||||
export interface RenderOutput {
|
||||
html: string;
|
||||
containingImages: { src: string }[];
|
||||
}
|
||||
|
||||
interface RenderEnv {
|
||||
containingImages: { src: string }[];
|
||||
}
|
||||
|
||||
export class MarkdownEngine {
|
||||
private md?: Promise<MarkdownIt>;
|
||||
|
||||
@ -141,7 +150,7 @@ export class MarkdownEngine {
|
||||
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {});
|
||||
}
|
||||
|
||||
public async render(input: SkinnyTextDocument | string): Promise<string> {
|
||||
public async render(input: SkinnyTextDocument | string): Promise<RenderOutput> {
|
||||
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
|
||||
const engine = await this.getEngine(config);
|
||||
|
||||
@ -149,10 +158,19 @@ export class MarkdownEngine {
|
||||
? this.tokenizeString(input, engine)
|
||||
: this.tokenizeDocument(input, config, engine);
|
||||
|
||||
return engine.renderer.render(tokens, {
|
||||
const env: RenderEnv = {
|
||||
containingImages: []
|
||||
};
|
||||
|
||||
const html = engine.renderer.render(tokens, {
|
||||
...(engine as any).options,
|
||||
...config
|
||||
}, {});
|
||||
}, env);
|
||||
|
||||
return {
|
||||
html,
|
||||
containingImages: env.containingImages
|
||||
};
|
||||
}
|
||||
|
||||
public async parse(document: SkinnyTextDocument): Promise<Token[]> {
|
||||
@ -192,12 +210,13 @@ export class MarkdownEngine {
|
||||
|
||||
private addImageStabilizer(md: any): void {
|
||||
const original = md.renderer.rules.image;
|
||||
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: any, self: any) => {
|
||||
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => {
|
||||
const token = tokens[idx];
|
||||
token.attrJoin('class', 'loading');
|
||||
|
||||
const src = token.attrGet('src');
|
||||
if (src) {
|
||||
env.containingImages.push({ src });
|
||||
const imgHash = hash(src);
|
||||
token.attrSet('id', `image-hash-${imgHash}`);
|
||||
}
|
||||
@ -231,6 +250,13 @@ export class MarkdownEngine {
|
||||
return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString());
|
||||
}
|
||||
|
||||
// Support file:// links
|
||||
if (isOfScheme(Schemes.file, link)) {
|
||||
// Ensure link is relative by prepending `/` so that it uses the <base> element URI
|
||||
// when resolving the absolute URL
|
||||
return normalizeLink('/' + link.replace(/^file:/, 'file'));
|
||||
}
|
||||
|
||||
// If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace
|
||||
if (!/^[a-z\-]+:/i.test(link)) {
|
||||
// Use a fake scheme for parsing
|
||||
@ -241,12 +267,14 @@ export class MarkdownEngine {
|
||||
if (uri.path[0] === '/') {
|
||||
const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!);
|
||||
if (root) {
|
||||
const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath);
|
||||
uri = fileUri.with({
|
||||
scheme: uri.scheme,
|
||||
const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({
|
||||
fragment: uri.fragment,
|
||||
query: uri.query,
|
||||
});
|
||||
|
||||
// Ensure fileUri is relative by prepending `/` so that it uses the <base> element URI
|
||||
// when resolving the absolute URL
|
||||
uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme));
|
||||
}
|
||||
}
|
||||
|
||||
@ -269,9 +297,7 @@ export class MarkdownEngine {
|
||||
private addLinkValidator(md: any): void {
|
||||
const validateLink = md.validateLink;
|
||||
md.validateLink = (link: string) => {
|
||||
// support file:// links
|
||||
return validateLink(link)
|
||||
|| isOfScheme(Schemes.file, link)
|
||||
|| isOfScheme(Schemes.vscode, link)
|
||||
|| isOfScheme(Schemes['vscode-insiders'], link)
|
||||
|| /^data:image\/.*?;/.test(link);
|
||||
|
@ -20,6 +20,11 @@ async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]>
|
||||
|
||||
suite('Markdown Document links', () => {
|
||||
|
||||
setup(async () => {
|
||||
// the tests make the assumption that link providers are already registered
|
||||
await vscode.extensions.getExtension('vscode.markdown-language-features')!.activate();
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
@ -21,12 +21,31 @@ suite('markdown.engine', () => {
|
||||
test('Renders a document', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, input);
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.strictEqual(await engine.render(doc), output);
|
||||
assert.strictEqual((await engine.render(doc)).html, output);
|
||||
});
|
||||
|
||||
test('Renders a string', async () => {
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.strictEqual(await engine.render(input), output);
|
||||
assert.strictEqual((await engine.render(input)).html, output);
|
||||
});
|
||||
});
|
||||
|
||||
suite('image-caching', () => {
|
||||
const input = ' [](no-img.png)   ';
|
||||
|
||||
test('Extracts all images', async () => {
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.deepStrictEqual((await engine.render(input)), {
|
||||
html: '<p data-line="0" class="code-line">'
|
||||
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
|
||||
+ '<a href="no-img.png" data-href="no-img.png"></a> '
|
||||
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170"> '
|
||||
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
|
||||
+ '<img src="./img2.png" alt="" class="loading" id="image-hash-265238964">'
|
||||
+ '</p>\n'
|
||||
,
|
||||
containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const path = require('path');
|
||||
const testRunner = require('vscode/lib/testrunner');
|
||||
const testRunner = require('../../../../test/integration/electron/testrunner');
|
||||
|
||||
const options: any = {
|
||||
ui: 'tdd',
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual } from 'assert';
|
||||
import 'mocha';
|
||||
import { Uri } from 'vscode';
|
||||
import { urlToUri } from '../util/url';
|
||||
|
||||
suite('urlToUri', () => {
|
||||
test('Absolute File', () => {
|
||||
deepStrictEqual(
|
||||
urlToUri('file:///root/test.txt', Uri.parse('file:///usr/home/')),
|
||||
Uri.parse('file:///root/test.txt')
|
||||
);
|
||||
});
|
||||
|
||||
test('Relative File', () => {
|
||||
deepStrictEqual(
|
||||
urlToUri('./file.ext', Uri.parse('file:///usr/home/')),
|
||||
Uri.parse('file:///usr/home/file.ext')
|
||||
);
|
||||
});
|
||||
|
||||
test('Http Basic', () => {
|
||||
deepStrictEqual(
|
||||
urlToUri('http://example.org?q=10&f', Uri.parse('file:///usr/home/')),
|
||||
Uri.parse('http://example.org?q=10&f')
|
||||
);
|
||||
});
|
||||
|
||||
test('Http Encoded Chars', () => {
|
||||
deepStrictEqual(
|
||||
urlToUri('http://example.org/%C3%A4', Uri.parse('file:///usr/home/')),
|
||||
Uri.parse('http://example.org/%C3%A4')
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
declare const URL: typeof import('url').URL;
|
||||
|
||||
/**
|
||||
* Tries to convert an url into a vscode uri and returns undefined if this is not possible.
|
||||
* `url` can be absolute or relative.
|
||||
*/
|
||||
export function urlToUri(url: string, base: vscode.Uri): vscode.Uri | undefined {
|
||||
try {
|
||||
// `vscode.Uri.joinPath` cannot be used, since it understands
|
||||
// `src` as path, not as relative url. This is problematic for query args.
|
||||
const parsedUrl = new URL(url, base.toString());
|
||||
const uri = vscode.Uri.parse(parsedUrl.toString());
|
||||
return uri;
|
||||
} catch (e) {
|
||||
// Don't crash if `URL` cannot parse `src`.
|
||||
return undefined;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user