mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
63
extensions/markdown/src/documentLinkProvider.ts
Normal file
63
extensions/markdown/src/documentLinkProvider.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
export default class MarkdownDocumentLinkProvider implements vscode.DocumentLinkProvider {
|
||||
|
||||
private _linkPattern = /(\[[^\]]*\]\(\s*?)(((((?=.*\)\)+)|(?=.*\)\]+))[^\s\)]+?)|([^\s]+)))\)/g;
|
||||
|
||||
constructor() { }
|
||||
|
||||
public provideDocumentLinks(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
const base = path.dirname(document.uri.fsPath);
|
||||
const text = document.getText();
|
||||
|
||||
this._linkPattern.lastIndex = 0;
|
||||
let match: RegExpMatchArray | null;
|
||||
while ((match = this._linkPattern.exec(text))) {
|
||||
const pre = match[1];
|
||||
const link = match[2];
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
try {
|
||||
results.push(new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
this.normalizeLink(document, link, base)));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private normalizeLink(document: vscode.TextDocument, link: string, base: string): vscode.Uri {
|
||||
const uri = vscode.Uri.parse(link);
|
||||
if (uri.scheme) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
// assume it must be a file
|
||||
let resourcePath = uri.path;
|
||||
if (!uri.path) {
|
||||
resourcePath = document.uri.path;
|
||||
} else if (uri.path[0] === '/') {
|
||||
const root = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||
if (root) {
|
||||
resourcePath = path.join(root.uri.fsPath, uri.path);
|
||||
}
|
||||
} else {
|
||||
resourcePath = path.join(base, uri.path);
|
||||
}
|
||||
|
||||
return vscode.Uri.parse(`command:_markdown.openDocumentLink?${encodeURIComponent(JSON.stringify({ fragment: uri.fragment, path: resourcePath }))}`);
|
||||
}
|
||||
}
|
||||
25
extensions/markdown/src/documentSymbolProvider.ts
Normal file
25
extensions/markdown/src/documentSymbolProvider.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
|
||||
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.SymbolInformation[]> {
|
||||
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
|
||||
return toc.map(entry => {
|
||||
return new vscode.SymbolInformation('#'.repeat(entry.level) + ' ' + entry.text, vscode.SymbolKind.Namespace, '', entry.location);
|
||||
});
|
||||
}
|
||||
}
|
||||
308
extensions/markdown/src/extension.ts
Normal file
308
extensions/markdown/src/extension.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import DocumentLinkProvider from './documentLinkProvider';
|
||||
import MDDocumentSymbolProvider from './documentSymbolProvider';
|
||||
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
|
||||
import { MDDocumentContentProvider, getMarkdownUri, isMarkdownFile } from './previewContentProvider';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
import { Logger } from './logger';
|
||||
|
||||
interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
}
|
||||
|
||||
interface OpenDocumentLinkArgs {
|
||||
path: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
const resolveExtensionResources = (extension: vscode.Extension<any>, stylePath: string): vscode.Uri => {
|
||||
const resource = vscode.Uri.parse(stylePath);
|
||||
if (resource.scheme) {
|
||||
return resource;
|
||||
}
|
||||
return vscode.Uri.file(path.join(extension.extensionPath, stylePath));
|
||||
};
|
||||
|
||||
var telemetryReporter: TelemetryReporter | null;
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const packageInfo = getPackageInfo();
|
||||
telemetryReporter = packageInfo && new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
|
||||
if (telemetryReporter) {
|
||||
context.subscriptions.push(telemetryReporter);
|
||||
}
|
||||
|
||||
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState);
|
||||
const engine = new MarkdownEngine();
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const contentProvider = new MDDocumentContentProvider(engine, context, cspArbiter, logger);
|
||||
const contentProviderRegistration = vscode.workspace.registerTextDocumentContentProvider('markdown', contentProvider);
|
||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, contentProvider);
|
||||
|
||||
for (const extension of vscode.extensions.all) {
|
||||
const contributes = extension.packageJSON && extension.packageJSON.contributes;
|
||||
if (!contributes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const styles = contributes['markdown.previewStyles'];
|
||||
if (styles && Array.isArray(styles)) {
|
||||
for (const style of styles) {
|
||||
try {
|
||||
contentProvider.addStyle(resolveExtensionResources(extension, style));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scripts = contributes['markdown.previewScripts'];
|
||||
if (scripts && Array.isArray(scripts)) {
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
contentProvider.addScript(resolveExtensionResources(extension, script));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contributes['markdown.markdownItPlugins']) {
|
||||
extension.activate().then(() => {
|
||||
if (extension.exports && extension.exports.extendMarkdownIt) {
|
||||
engine.addPlugin((md: any) => extension.exports.extendMarkdownIt(md));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const symbolsProvider = new MDDocumentSymbolProvider(engine);
|
||||
const symbolsProviderRegistration = vscode.languages.registerDocumentSymbolProvider({ language: 'markdown' }, symbolsProvider);
|
||||
context.subscriptions.push(contentProviderRegistration, symbolsProviderRegistration);
|
||||
|
||||
context.subscriptions.push(vscode.languages.registerDocumentLinkProvider('markdown', new DocumentLinkProvider()));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreview', (uri) => showPreview(cspArbiter, uri, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewToSide', uri => showPreview(cspArbiter, uri, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('markdown.showSource', showSource));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('_markdown.revealLine', (uri, line) => {
|
||||
const sourceUri = vscode.Uri.parse(decodeURIComponent(uri));
|
||||
logger.log('revealLine', { uri, sourceUri: sourceUri.toString(), line });
|
||||
|
||||
vscode.window.visibleTextEditors
|
||||
.filter(editor => isMarkdownFile(editor.document) && editor.document.uri.fsPath === sourceUri.fsPath)
|
||||
.forEach(editor => {
|
||||
const sourceLine = Math.floor(line);
|
||||
const fraction = line - sourceLine;
|
||||
const text = editor.document.lineAt(sourceLine).text;
|
||||
const start = Math.floor(fraction * text.length);
|
||||
editor.revealRange(
|
||||
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
|
||||
vscode.TextEditorRevealType.AtTop);
|
||||
});
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('_markdown.didClick', (uri: string, line) => {
|
||||
const sourceUri = vscode.Uri.parse(decodeURIComponent(uri));
|
||||
return vscode.workspace.openTextDocument(sourceUri)
|
||||
.then(document => vscode.window.showTextDocument(document))
|
||||
.then(editor =>
|
||||
vscode.commands.executeCommand('revealLine', { lineNumber: Math.floor(line), at: 'center' })
|
||||
.then(() => editor))
|
||||
.then(editor => {
|
||||
if (editor) {
|
||||
editor.selection = new vscode.Selection(
|
||||
new vscode.Position(Math.floor(line), 0),
|
||||
new vscode.Position(Math.floor(line), 0));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('_markdown.openDocumentLink', (args: OpenDocumentLinkArgs) => {
|
||||
const tryRevealLine = async (editor: vscode.TextEditor) => {
|
||||
if (editor && args.fragment) {
|
||||
const toc = new TableOfContentsProvider(engine, editor.document);
|
||||
const line = await toc.lookup(args.fragment);
|
||||
if (!isNaN(line)) {
|
||||
return editor.revealRange(
|
||||
new vscode.Range(line, 0, line, 0),
|
||||
vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document) && vscode.window.activeTextEditor.document.uri.fsPath === args.path) {
|
||||
return tryRevealLine(vscode.window.activeTextEditor);
|
||||
} else {
|
||||
const resource = vscode.Uri.file(args.path);
|
||||
return vscode.workspace.openTextDocument(resource)
|
||||
.then(vscode.window.showTextDocument)
|
||||
.then(tryRevealLine, _ => vscode.commands.executeCommand('vscode.open', resource));
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('markdown.showPreviewSecuritySelector', (resource: string | undefined) => {
|
||||
if (resource) {
|
||||
const source = vscode.Uri.parse(resource).query;
|
||||
previewSecuritySelector.showSecutitySelectorForResource(vscode.Uri.parse(source));
|
||||
} else {
|
||||
if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.languageId === 'markdown') {
|
||||
previewSecuritySelector.showSecutitySelectorForResource(vscode.window.activeTextEditor.document.uri);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('markdown.refreshPreview', (resource: string | undefined) => {
|
||||
if (resource) {
|
||||
const source = vscode.Uri.parse(resource);
|
||||
contentProvider.update(source);
|
||||
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
contentProvider.update(getMarkdownUri(vscode.window.activeTextEditor.document.uri));
|
||||
} else {
|
||||
// update all generated md documents
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
if (document.uri.scheme === 'markdown') {
|
||||
contentProvider.update(document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('_markdown.onPreviewStyleLoadError', (resources: string[]) => {
|
||||
vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", resources.join(', ')));
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidSaveTextDocument(document => {
|
||||
if (isMarkdownFile(document)) {
|
||||
const uri = getMarkdownUri(document.uri);
|
||||
contentProvider.update(uri);
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (isMarkdownFile(event.document)) {
|
||||
const uri = getMarkdownUri(event.document.uri);
|
||||
contentProvider.update(uri);
|
||||
}
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
|
||||
logger.updateConfiguration();
|
||||
contentProvider.updateConfiguration();
|
||||
}));
|
||||
|
||||
context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => {
|
||||
if (isMarkdownFile(event.textEditor.document)) {
|
||||
const markdownFile = getMarkdownUri(event.textEditor.document.uri);
|
||||
logger.log('updatePreviewForSelection', { markdownFile: markdownFile.toString() });
|
||||
|
||||
vscode.commands.executeCommand('_workbench.htmlPreview.postMessage',
|
||||
markdownFile,
|
||||
{
|
||||
line: event.selections[0].active.line
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function showPreview(cspArbiter: ExtensionContentSecurityPolicyArbiter, uri?: vscode.Uri, sideBySide: boolean = false) {
|
||||
let resource = uri;
|
||||
if (!(resource instanceof vscode.Uri)) {
|
||||
if (vscode.window.activeTextEditor) {
|
||||
// we are relaxed and don't check for markdown files
|
||||
resource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(resource instanceof vscode.Uri)) {
|
||||
if (!vscode.window.activeTextEditor) {
|
||||
// this is most likely toggling the preview
|
||||
return vscode.commands.executeCommand('markdown.showSource');
|
||||
}
|
||||
// nothing found that could be shown or toggled
|
||||
return;
|
||||
}
|
||||
|
||||
const thenable = vscode.commands.executeCommand('vscode.previewHtml',
|
||||
getMarkdownUri(resource),
|
||||
getViewColumn(sideBySide),
|
||||
`Preview '${path.basename(resource.fsPath)}'`,
|
||||
{
|
||||
allowScripts: true,
|
||||
allowSvgs: cspArbiter.shouldAllowSvgsForResource(resource)
|
||||
});
|
||||
|
||||
if (telemetryReporter) {
|
||||
telemetryReporter.sendTelemetryEvent('openPreview', {
|
||||
where: sideBySide ? 'sideBySide' : 'inPlace',
|
||||
how: (uri instanceof vscode.Uri) ? 'action' : 'pallete'
|
||||
});
|
||||
}
|
||||
|
||||
return thenable;
|
||||
}
|
||||
|
||||
function getViewColumn(sideBySide: boolean): vscode.ViewColumn | undefined {
|
||||
const active = vscode.window.activeTextEditor;
|
||||
if (!active) {
|
||||
return vscode.ViewColumn.One;
|
||||
}
|
||||
|
||||
if (!sideBySide) {
|
||||
return active.viewColumn;
|
||||
}
|
||||
|
||||
switch (active.viewColumn) {
|
||||
case vscode.ViewColumn.One:
|
||||
return vscode.ViewColumn.Two;
|
||||
case vscode.ViewColumn.Two:
|
||||
return vscode.ViewColumn.Three;
|
||||
}
|
||||
|
||||
return active.viewColumn;
|
||||
}
|
||||
|
||||
function showSource(mdUri: vscode.Uri) {
|
||||
if (!mdUri) {
|
||||
return vscode.commands.executeCommand('workbench.action.navigateBack');
|
||||
}
|
||||
|
||||
const docUri = vscode.Uri.parse(mdUri.query);
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
if (editor.document.uri.scheme === docUri.scheme && editor.document.uri.fsPath === docUri.fsPath) {
|
||||
return vscode.window.showTextDocument(editor.document, editor.viewColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return vscode.workspace.openTextDocument(docUri)
|
||||
.then(vscode.window.showTextDocument);
|
||||
}
|
||||
|
||||
function getPackageInfo(): IPackageInfo | null {
|
||||
const extention = vscode.extensions.getExtension('Microsoft.vscode-markdown');
|
||||
if (extention && extention.packageJSON) {
|
||||
return {
|
||||
name: extention.packageJSON.name,
|
||||
version: extention.packageJSON.version,
|
||||
aiKey: extention.packageJSON.aiKey
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
78
extensions/markdown/src/logger.ts
Normal file
78
extensions/markdown/src/logger.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { OutputChannel, window, workspace } from 'vscode';
|
||||
|
||||
enum Trace {
|
||||
Off,
|
||||
Verbose
|
||||
}
|
||||
|
||||
namespace Trace {
|
||||
export function fromString(value: string): Trace {
|
||||
value = value.toLowerCase();
|
||||
switch (value) {
|
||||
case 'off':
|
||||
return Trace.Off;
|
||||
case 'verbose':
|
||||
return Trace.Verbose;
|
||||
default:
|
||||
return Trace.Off;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return Object.prototype.toString.call(value) === '[object String]';
|
||||
}
|
||||
|
||||
export class Logger {
|
||||
private trace: Trace;
|
||||
private _output: OutputChannel;
|
||||
|
||||
constructor() {
|
||||
this.updateConfiguration();
|
||||
}
|
||||
|
||||
public log(message: string, data?: any): void {
|
||||
if (this.trace === Trace.Verbose) {
|
||||
this.output.appendLine(`[Log - ${(new Date().toLocaleTimeString())}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
this.trace = this.readTrace();
|
||||
}
|
||||
|
||||
private get output(): OutputChannel {
|
||||
if (!this._output) {
|
||||
this._output = window.createOutputChannel('Markdown');
|
||||
}
|
||||
return this._output;
|
||||
}
|
||||
|
||||
private readTrace(): Trace {
|
||||
return Trace.fromString(workspace.getConfiguration().get<string>('markdown.trace', 'off'));
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
if (isString(data.stack)) {
|
||||
return data.stack;
|
||||
}
|
||||
return (data as Error).message;
|
||||
}
|
||||
if (isString(data)) {
|
||||
return data;
|
||||
}
|
||||
return JSON.stringify(data, undefined, 2);
|
||||
}
|
||||
}
|
||||
162
extensions/markdown/src/markdownEngine.ts
Normal file
162
extensions/markdown/src/markdownEngine.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { TableOfContentsProvider } from './tableOfContentsProvider';
|
||||
import { MarkdownIt, Token } from 'markdown-it';
|
||||
|
||||
const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/;
|
||||
|
||||
export class MarkdownEngine {
|
||||
private md: MarkdownIt;
|
||||
|
||||
private firstLine: number;
|
||||
|
||||
private currentDocument: vscode.Uri;
|
||||
|
||||
private plugins: Array<(md: any) => any> = [];
|
||||
|
||||
public addPlugin(factory: (md: any) => any): void {
|
||||
if (this.md) {
|
||||
this.usePlugin(factory);
|
||||
} else {
|
||||
this.plugins.push(factory);
|
||||
}
|
||||
}
|
||||
|
||||
private usePlugin(factory: (md: any) => any): void {
|
||||
try {
|
||||
this.md = factory(this.md);
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
private async getEngine(): Promise<MarkdownIt> {
|
||||
if (!this.md) {
|
||||
const hljs = await import('highlight.js');
|
||||
const mdnh = await import('markdown-it-named-headers');
|
||||
this.md = (await import('markdown-it'))({
|
||||
html: true,
|
||||
highlight: (str: string, lang: string) => {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return `<pre class="hljs"><code><div>${hljs.highlight(lang, str, true).value}</div></code></pre>`;
|
||||
} catch (error) { }
|
||||
}
|
||||
return `<pre class="hljs"><code><div>${this.md.utils.escapeHtml(str)}</div></code></pre>`;
|
||||
}
|
||||
}).use(mdnh, {
|
||||
slugify: (header: string) => TableOfContentsProvider.slugify(header)
|
||||
});
|
||||
|
||||
for (const plugin of this.plugins) {
|
||||
this.usePlugin(plugin);
|
||||
}
|
||||
this.plugins = [];
|
||||
|
||||
for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'blockquote_open', 'list_item_open']) {
|
||||
this.addLineNumberRenderer(this.md, renderName);
|
||||
}
|
||||
|
||||
this.addLinkNormalizer(this.md);
|
||||
this.addLinkValidator(this.md);
|
||||
}
|
||||
|
||||
const config = vscode.workspace.getConfiguration('markdown');
|
||||
this.md.set({
|
||||
breaks: config.get('preview.breaks', false),
|
||||
linkify: config.get('preview.linkify', true)
|
||||
});
|
||||
return this.md;
|
||||
}
|
||||
|
||||
private stripFrontmatter(text: string): { text: string, offset: number } {
|
||||
let offset = 0;
|
||||
const frontMatterMatch = FrontMatterRegex.exec(text);
|
||||
if (frontMatterMatch) {
|
||||
const frontMatter = frontMatterMatch[0];
|
||||
offset = frontMatter.split(/\r\n|\n|\r/g).length - 1;
|
||||
text = text.substr(frontMatter.length);
|
||||
}
|
||||
return { text, offset };
|
||||
}
|
||||
|
||||
public async render(document: vscode.Uri, stripFrontmatter: boolean, text: string): Promise<string> {
|
||||
let offset = 0;
|
||||
if (stripFrontmatter) {
|
||||
const markdownContent = this.stripFrontmatter(text);
|
||||
offset = markdownContent.offset;
|
||||
text = markdownContent.text;
|
||||
}
|
||||
this.currentDocument = document;
|
||||
this.firstLine = offset;
|
||||
const engine = await this.getEngine();
|
||||
return engine.render(text);
|
||||
}
|
||||
|
||||
public async parse(document: vscode.Uri, source: string): Promise<Token[]> {
|
||||
const { text, offset } = this.stripFrontmatter(source);
|
||||
this.currentDocument = document;
|
||||
const engine = await this.getEngine();
|
||||
|
||||
return engine.parse(text, {}).map(token => {
|
||||
if (token.map) {
|
||||
token.map[0] += offset;
|
||||
}
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
private addLineNumberRenderer(md: any, ruleName: string): void {
|
||||
const original = md.renderer.rules[ruleName];
|
||||
md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
if (token.map && token.map.length) {
|
||||
token.attrSet('data-line', this.firstLine + token.map[0]);
|
||||
token.attrJoin('class', 'code-line');
|
||||
}
|
||||
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private addLinkNormalizer(md: any): void {
|
||||
const normalizeLink = md.normalizeLink;
|
||||
md.normalizeLink = (link: string) => {
|
||||
try {
|
||||
let uri = vscode.Uri.parse(link);
|
||||
if (!uri.scheme && uri.path && !uri.fragment) {
|
||||
// Assume it must be a file
|
||||
if (uri.path[0] === '/') {
|
||||
const root = vscode.workspace.getWorkspaceFolder(this.currentDocument);
|
||||
if (root) {
|
||||
uri = vscode.Uri.file(path.join(root.uri.fsPath, uri.path));
|
||||
}
|
||||
} else {
|
||||
uri = vscode.Uri.file(path.join(path.dirname(this.currentDocument.path), uri.path));
|
||||
}
|
||||
return normalizeLink(uri.toString(true));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
return normalizeLink(link);
|
||||
};
|
||||
}
|
||||
|
||||
private addLinkValidator(md: any): void {
|
||||
const validateLink = md.validateLink;
|
||||
md.validateLink = (link: string) => {
|
||||
// support file:// links
|
||||
return validateLink(link) || link.indexOf('file:') === 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
290
extensions/markdown/src/previewContentProvider.ts
Normal file
290
extensions/markdown/src/previewContentProvider.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Logger } from './logger';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const previewStrings = {
|
||||
cspAlertMessageText: localize('preview.securityMessage.text', 'Some content has been disabled in this document'),
|
||||
cspAlertMessageTitle: localize('preview.securityMessage.title', 'Potentially unsafe or insecure content has been disabled in the markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts'),
|
||||
cspAlertMessageLabel: localize('preview.securityMessage.label', 'Content Disabled Security Warning')
|
||||
};
|
||||
|
||||
export function isMarkdownFile(document: vscode.TextDocument) {
|
||||
return document.languageId === 'markdown'
|
||||
&& document.uri.scheme !== 'markdown'; // prevent processing of own documents
|
||||
}
|
||||
|
||||
export function getMarkdownUri(uri: vscode.Uri) {
|
||||
if (uri.scheme === 'markdown') {
|
||||
return uri;
|
||||
}
|
||||
|
||||
return uri.with({
|
||||
scheme: 'markdown',
|
||||
path: uri.path + '.rendered',
|
||||
query: uri.toString()
|
||||
});
|
||||
}
|
||||
|
||||
class MarkdownPreviewConfig {
|
||||
public static getCurrentConfig() {
|
||||
return new MarkdownPreviewConfig();
|
||||
}
|
||||
|
||||
public readonly scrollBeyondLastLine: boolean;
|
||||
public readonly wordWrap: boolean;
|
||||
public readonly previewFrontMatter: string;
|
||||
public readonly lineBreaks: boolean;
|
||||
public readonly doubleClickToSwitchToEditor: boolean;
|
||||
public readonly scrollEditorWithPreview: boolean;
|
||||
public readonly scrollPreviewWithEditorSelection: boolean;
|
||||
public readonly markEditorSelection: boolean;
|
||||
|
||||
public readonly lineHeight: number;
|
||||
public readonly fontSize: number;
|
||||
public readonly fontFamily: string | undefined;
|
||||
public readonly styles: string[];
|
||||
|
||||
private constructor() {
|
||||
const editorConfig = vscode.workspace.getConfiguration('editor');
|
||||
const markdownConfig = vscode.workspace.getConfiguration('markdown');
|
||||
const markdownEditorConfig = vscode.workspace.getConfiguration('[markdown]');
|
||||
|
||||
this.scrollBeyondLastLine = editorConfig.get<boolean>('scrollBeyondLastLine', false);
|
||||
|
||||
this.wordWrap = editorConfig.get<string>('wordWrap', 'off') !== 'off';
|
||||
if (markdownEditorConfig && markdownEditorConfig['editor.wordWrap']) {
|
||||
this.wordWrap = markdownEditorConfig['editor.wordWrap'] !== 'off';
|
||||
}
|
||||
|
||||
this.previewFrontMatter = markdownConfig.get<string>('previewFrontMatter', 'hide');
|
||||
this.scrollPreviewWithEditorSelection = !!markdownConfig.get<boolean>('preview.scrollPreviewWithEditorSelection', true);
|
||||
this.scrollEditorWithPreview = !!markdownConfig.get<boolean>('preview.scrollEditorWithPreview', true);
|
||||
this.lineBreaks = !!markdownConfig.get<boolean>('preview.breaks', false);
|
||||
this.doubleClickToSwitchToEditor = !!markdownConfig.get<boolean>('preview.doubleClickToSwitchToEditor', true);
|
||||
this.markEditorSelection = !!markdownConfig.get<boolean>('preview.markEditorSelection', true);
|
||||
|
||||
this.fontFamily = markdownConfig.get<string | undefined>('preview.fontFamily', undefined);
|
||||
this.fontSize = Math.max(8, +markdownConfig.get<number>('preview.fontSize', NaN));
|
||||
this.lineHeight = Math.max(0.6, +markdownConfig.get<number>('preview.lineHeight', NaN));
|
||||
|
||||
this.styles = markdownConfig.get<string[]>('styles', []);
|
||||
}
|
||||
|
||||
public isEqualTo(otherConfig: MarkdownPreviewConfig) {
|
||||
for (let key in this) {
|
||||
if (this.hasOwnProperty(key) && key !== 'styles') {
|
||||
if (this[key] !== otherConfig[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check styles
|
||||
if (this.styles.length !== otherConfig.styles.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < this.styles.length; ++i) {
|
||||
if (this.styles[i] !== otherConfig.styles[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class MDDocumentContentProvider implements vscode.TextDocumentContentProvider {
|
||||
private _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
private _waiting: boolean = false;
|
||||
|
||||
private config: MarkdownPreviewConfig;
|
||||
|
||||
private extraStyles: Array<vscode.Uri> = [];
|
||||
private extraScripts: Array<vscode.Uri> = [];
|
||||
|
||||
constructor(
|
||||
private engine: MarkdownEngine,
|
||||
private context: vscode.ExtensionContext,
|
||||
private cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private logger: Logger
|
||||
) {
|
||||
this.config = MarkdownPreviewConfig.getCurrentConfig();
|
||||
}
|
||||
|
||||
public addScript(resource: vscode.Uri): void {
|
||||
this.extraScripts.push(resource);
|
||||
}
|
||||
|
||||
public addStyle(resource: vscode.Uri): void {
|
||||
this.extraStyles.push(resource);
|
||||
}
|
||||
|
||||
private getMediaPath(mediaFile: string): string {
|
||||
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))).toString();
|
||||
}
|
||||
|
||||
private fixHref(resource: vscode.Uri, href: string): string {
|
||||
if (!href) {
|
||||
return href;
|
||||
}
|
||||
|
||||
// Use href if it is already an URL
|
||||
const hrefUri = vscode.Uri.parse(href);
|
||||
if (['file', 'http', 'https'].indexOf(hrefUri.scheme) >= 0) {
|
||||
return hrefUri.toString();
|
||||
}
|
||||
|
||||
// Use href as file URI if it is absolute
|
||||
if (path.isAbsolute(href)) {
|
||||
return vscode.Uri.file(href).toString();
|
||||
}
|
||||
|
||||
// use a workspace relative path if there is a workspace
|
||||
let root = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (root) {
|
||||
return vscode.Uri.file(path.join(root.uri.fsPath, href)).toString();
|
||||
}
|
||||
|
||||
// otherwise look relative to the markdown file
|
||||
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString();
|
||||
}
|
||||
|
||||
private computeCustomStyleSheetIncludes(uri: vscode.Uri): string {
|
||||
if (this.config.styles && Array.isArray(this.config.styles)) {
|
||||
return this.config.styles.map((style) => {
|
||||
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '"')}" href="${this.fixHref(uri, style)}" type="text/css" media="screen">`;
|
||||
}).join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getSettingsOverrideStyles(nonce: string): string {
|
||||
return `<style nonce="${nonce}">
|
||||
body {
|
||||
${this.config.fontFamily ? `font-family: ${this.config.fontFamily};` : ''}
|
||||
${isNaN(this.config.fontSize) ? '' : `font-size: ${this.config.fontSize}px;`}
|
||||
${isNaN(this.config.lineHeight) ? '' : `line-height: ${this.config.lineHeight};`}
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
private getStyles(resource: vscode.Uri, nonce: string): string {
|
||||
const baseStyles = [
|
||||
this.getMediaPath('markdown.css'),
|
||||
this.getMediaPath('tomorrow.css')
|
||||
].concat(this.extraStyles.map(resource => resource.toString()));
|
||||
|
||||
return `${baseStyles.map(href => `<link rel="stylesheet" type="text/css" href="${href}">`).join('\n')}
|
||||
${this.getSettingsOverrideStyles(nonce)}
|
||||
${this.computeCustomStyleSheetIncludes(resource)}`;
|
||||
}
|
||||
|
||||
private getScripts(nonce: string): string {
|
||||
const scripts = [this.getMediaPath('main.js')].concat(this.extraScripts.map(resource => resource.toString()));
|
||||
return scripts
|
||||
.map(source => `<script async src="${source}" nonce="${nonce}" charset="UTF-8"></script>`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
public async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
|
||||
const sourceUri = vscode.Uri.parse(uri.query);
|
||||
|
||||
let initialLine: number | undefined = undefined;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.fsPath === sourceUri.fsPath) {
|
||||
initialLine = editor.selection.active.line;
|
||||
}
|
||||
|
||||
const document = await vscode.workspace.openTextDocument(sourceUri);
|
||||
this.config = MarkdownPreviewConfig.getCurrentConfig();
|
||||
|
||||
const initialData = {
|
||||
previewUri: uri.toString(),
|
||||
source: sourceUri.toString(),
|
||||
line: initialLine,
|
||||
scrollPreviewWithEditorSelection: this.config.scrollPreviewWithEditorSelection,
|
||||
scrollEditorWithPreview: this.config.scrollEditorWithPreview,
|
||||
doubleClickToSwitchToEditor: this.config.doubleClickToSwitchToEditor
|
||||
};
|
||||
|
||||
this.logger.log('provideTextDocumentContent', initialData);
|
||||
|
||||
// Content Security Policy
|
||||
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
|
||||
const csp = this.getCspForResource(sourceUri, nonce);
|
||||
|
||||
const body = await this.engine.render(sourceUri, this.config.previewFrontMatter === 'hide', document.getText());
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
${csp}
|
||||
<meta id="vscode-markdown-preview-data" data-settings="${JSON.stringify(initialData).replace(/"/g, '"')}" data-strings="${JSON.stringify(previewStrings).replace(/"/g, '"')}">
|
||||
<script src="${this.getMediaPath('csp.js')}" nonce="${nonce}"></script>
|
||||
<script src="${this.getMediaPath('loading.js')}" nonce="${nonce}"></script>
|
||||
${this.getStyles(sourceUri, nonce)}
|
||||
<base href="${document.uri.toString(true)}">
|
||||
</head>
|
||||
<body class="vscode-body ${this.config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${this.config.wordWrap ? 'wordWrap' : ''} ${this.config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body}
|
||||
<div class="code-line" data-line="${document.lineCount}"></div>
|
||||
${this.getScripts(nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
const newConfig = MarkdownPreviewConfig.getCurrentConfig();
|
||||
if (!this.config.isEqualTo(newConfig)) {
|
||||
this.config = newConfig;
|
||||
// update all generated md documents
|
||||
for (const document of vscode.workspace.textDocuments) {
|
||||
if (document.uri.scheme === 'markdown') {
|
||||
this.update(document.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get onDidChange(): vscode.Event<vscode.Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
public update(uri: vscode.Uri) {
|
||||
if (!this._waiting) {
|
||||
this._waiting = true;
|
||||
setTimeout(() => {
|
||||
this._waiting = false;
|
||||
this._onDidChange.fire(uri);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private getCspForResource(resource: vscode.Uri, nonce: string): string {
|
||||
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
|
||||
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' http: https: data:; media-src 'self' http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' http: https: data:; font-src 'self' http: https: data:;">`;
|
||||
|
||||
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
|
||||
return '';
|
||||
|
||||
case MarkdownPreviewSecurityLevel.Strict:
|
||||
default:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data:; media-src 'self' https: data:; script-src 'nonce-${nonce}'; style-src 'self' 'unsafe-inline' https: data:; font-src 'self' https: data:;">`;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
extensions/markdown/src/security.ts
Normal file
140
extensions/markdown/src/security.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { getMarkdownUri, MDDocumentContentProvider } from './previewContentProvider';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum MarkdownPreviewSecurityLevel {
|
||||
Strict = 0,
|
||||
AllowInsecureContent = 1,
|
||||
AllowScriptsAndAllContent = 2
|
||||
}
|
||||
|
||||
export interface ContentSecurityPolicyArbiter {
|
||||
getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel;
|
||||
|
||||
setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable<void>;
|
||||
|
||||
shouldAllowSvgsForResource(resource: vscode.Uri): void;
|
||||
}
|
||||
|
||||
export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPolicyArbiter {
|
||||
private readonly old_trusted_workspace_key = 'trusted_preview_workspace:';
|
||||
private readonly security_level_key = 'preview_security_level:';
|
||||
|
||||
constructor(
|
||||
private globalState: vscode.Memento
|
||||
) { }
|
||||
|
||||
public getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel {
|
||||
// Use new security level setting first
|
||||
const level = this.globalState.get<MarkdownPreviewSecurityLevel | undefined>(this.security_level_key + this.getRoot(resource), undefined);
|
||||
if (typeof level !== 'undefined') {
|
||||
return level;
|
||||
}
|
||||
|
||||
// Fallback to old trusted workspace setting
|
||||
if (this.globalState.get<boolean>(this.old_trusted_workspace_key + this.getRoot(resource), false)) {
|
||||
return MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent;
|
||||
}
|
||||
return MarkdownPreviewSecurityLevel.Strict;
|
||||
}
|
||||
|
||||
public setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable<void> {
|
||||
return this.globalState.update(this.security_level_key + this.getRoot(resource), level);
|
||||
}
|
||||
|
||||
public shouldAllowSvgsForResource(resource: vscode.Uri) {
|
||||
const securityLevel = this.getSecurityLevelForResource(resource);
|
||||
return securityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent || securityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent;
|
||||
}
|
||||
|
||||
private getRoot(resource: vscode.Uri): vscode.Uri {
|
||||
if (vscode.workspace.workspaceFolders) {
|
||||
const folderForResource = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (folderForResource) {
|
||||
return folderForResource.uri;
|
||||
}
|
||||
|
||||
if (vscode.workspace.workspaceFolders.length) {
|
||||
return vscode.workspace.workspaceFolders[0].uri;
|
||||
}
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviewSecuritySelector {
|
||||
|
||||
public constructor(
|
||||
private cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private contentProvider: MDDocumentContentProvider
|
||||
) { }
|
||||
|
||||
public async showSecutitySelectorForResource(resource: vscode.Uri): Promise<void> {
|
||||
interface PreviewSecurityPickItem extends vscode.QuickPickItem {
|
||||
type: 'moreinfo' | MarkdownPreviewSecurityLevel;
|
||||
}
|
||||
|
||||
function markActiveWhen(when: boolean): string {
|
||||
return when ? '• ' : '';
|
||||
}
|
||||
|
||||
const currentSecurityLevel = this.cspArbiter.getSecurityLevelForResource(resource);
|
||||
const selection = await vscode.window.showQuickPick<PreviewSecurityPickItem>(
|
||||
[
|
||||
{
|
||||
type: MarkdownPreviewSecurityLevel.Strict,
|
||||
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.Strict) + localize('strict.title', 'Strict'),
|
||||
description: localize('strict.description', 'Only load secure content'),
|
||||
}, {
|
||||
type: MarkdownPreviewSecurityLevel.AllowInsecureContent,
|
||||
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent) + localize('insecureContent.title', 'Allow insecure content'),
|
||||
description: localize('insecureContent.description', 'Enable loading content over http'),
|
||||
}, {
|
||||
type: MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent,
|
||||
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent) + localize('disable.title', 'Disable'),
|
||||
description: localize('disable.description', 'Allow all content and script execution. Not recommended'),
|
||||
}, {
|
||||
type: 'moreinfo',
|
||||
label: localize('moreInfo.title', 'More Information'),
|
||||
description: ''
|
||||
}
|
||||
], {
|
||||
placeHolder: localize(
|
||||
'preview.showPreviewSecuritySelector.title',
|
||||
'Select security settings for Markdown previews in this workspace'),
|
||||
});
|
||||
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.type === 'moreinfo') {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse('https://go.microsoft.com/fwlink/?linkid=854414'));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
|
||||
|
||||
const sourceUri = getMarkdownUri(resource);
|
||||
|
||||
await vscode.commands.executeCommand('_workbench.htmlPreview.updateOptions',
|
||||
sourceUri,
|
||||
{
|
||||
allowScripts: true,
|
||||
allowSvgs: this.cspArbiter.shouldAllowSvgsForResource(resource)
|
||||
});
|
||||
|
||||
this.contentProvider.update(sourceUri);
|
||||
}
|
||||
}
|
||||
94
extensions/markdown/src/tableOfContentsProvider.ts
Normal file
94
extensions/markdown/src/tableOfContentsProvider.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
|
||||
export interface TocEntry {
|
||||
slug: string;
|
||||
text: string;
|
||||
level: number;
|
||||
line: number;
|
||||
location: vscode.Location;
|
||||
}
|
||||
|
||||
export class TableOfContentsProvider {
|
||||
private toc: TocEntry[];
|
||||
|
||||
public constructor(
|
||||
private engine: MarkdownEngine,
|
||||
private document: vscode.TextDocument
|
||||
) { }
|
||||
|
||||
public async getToc(): Promise<TocEntry[]> {
|
||||
if (!this.toc) {
|
||||
try {
|
||||
this.toc = await this.buildToc(this.document);
|
||||
} catch (e) {
|
||||
this.toc = [];
|
||||
}
|
||||
}
|
||||
return this.toc;
|
||||
}
|
||||
|
||||
public async lookup(fragment: string): Promise<number> {
|
||||
const slug = TableOfContentsProvider.slugify(fragment);
|
||||
for (const entry of await this.getToc()) {
|
||||
if (entry.slug === slug) {
|
||||
return entry.line;
|
||||
}
|
||||
}
|
||||
return NaN;
|
||||
}
|
||||
|
||||
private async buildToc(document: vscode.TextDocument): Promise<TocEntry[]> {
|
||||
const toc: TocEntry[] = [];
|
||||
const tokens = await this.engine.parse(document.uri, document.getText());
|
||||
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
const href = TableOfContentsProvider.slugify(line.text);
|
||||
const level = TableOfContentsProvider.getHeaderLevel(heading.markup);
|
||||
if (href) {
|
||||
toc.push({
|
||||
slug: href,
|
||||
text: TableOfContentsProvider.getHeaderText(line.text),
|
||||
level: level,
|
||||
line: lineNumber,
|
||||
location: new vscode.Location(document.uri, line.range)
|
||||
});
|
||||
}
|
||||
}
|
||||
return toc;
|
||||
}
|
||||
|
||||
private static getHeaderLevel(markup: string): number {
|
||||
if (markup === '=') {
|
||||
return 1;
|
||||
} else if (markup === '-') {
|
||||
return 2;
|
||||
} else { // '#', '##', ...
|
||||
return markup.length;
|
||||
}
|
||||
}
|
||||
|
||||
private static getHeaderText(header: string): string {
|
||||
return header.replace(/^\s*#+\s*(.*?)\s*\1*$/, (_, word) => `${word.trim()}`);
|
||||
}
|
||||
|
||||
public static slugify(header: string): string {
|
||||
return encodeURI(header.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^\-+/, '')
|
||||
.replace(/\-+$/, ''));
|
||||
}
|
||||
}
|
||||
|
||||
5
extensions/markdown/src/typings/markdown-it-named-headers.d.ts
vendored
Normal file
5
extensions/markdown/src/typings/markdown-it-named-headers.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
declare module 'markdown-it-named-headers' { }
|
||||
7
extensions/markdown/src/typings/ref.d.ts
vendored
Normal file
7
extensions/markdown/src/typings/ref.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
Reference in New Issue
Block a user