diff --git a/README.md b/README.md index a2b19f9..2293896 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ This extension demonstrates how to perform checks against links in a Markdown document during editing. Specifically, this extension: -1. Extracts all links from the document (both inline and reference links, but not HTML links,) using a regular expression +1. Extracts all Markdown links from the document (both inline and reference links, but not raw HTML links,) using a regular expression. 2. Checks the HTTP/S URLs to see if they reference a language specific version of a URL by checking for a pattern of "LC-CC", where LC is a language code and CC is a country code. For example, "en-us". Ideally want to point to a generic URL that will route viewers to language specific pages based on the browser language setting. So these links are reported as errors. +3. Binds Alt-L to check for broken links. This tries to reach each link, and can take some time, so it opens an output panel to the left of the document and shows the status of the links as it checks them. -TODO + NOTE: Checking for broken links is more of an art than a science. Some sites don't actually return 404, but send you to a landing page. For example, Azure.com works this way. You can go to https://Azure.com/foo/bar and it will happily redirect you to https://Azure.com, with no 404 status returned. So take a status of "OK" with a grain of salt - you may not be arriving at the page you intend. -* Check for broken links: This took too long to check for every change to the document, so it's now tied to Alt+L. But the code isn't active yet, as I'm waiting on examples/guidance on using outputChannels or some other reporting mechanism. +TODO: + +* Refactor broken link checking to display the actual URL that you arrived at for "OK" results. diff --git a/extension.ts b/extension.ts index a5319da..aa0bbd1 100644 --- a/extension.ts +++ b/extension.ts @@ -1,16 +1,21 @@ // The some things from 'vscode', which contains the VS Code extensibility API import { + workspace, window, commands, languages, Diagnostic, DiagnosticSeverity, + DiagnosticCollection, Location, Range, + OutputChannel, Position, Uri, Disposable, - TextDocument} from 'vscode'; + TextDocument, + TextLine, + ViewColumn} from 'vscode'; // For HTTP/s address validation import validator = require('validator'); // For checking broken links @@ -20,146 +25,85 @@ import brokenLink = require('broken-link'); interface Link { text: string address: string - lineNumber: number - lineText: string + lineText: TextLine } // this method is called when your extension is activated // your extension is activated the very first time the command is executed export function activate(disposables: Disposable[]) { - // Create the checker and controller - let linkChecker = new LinkChecker(); - let controller = new LinkCheckController(linkChecker); + // Create the diagnostics collection + let diagnostics = languages.createDiagnosticCollection(); - // Dispose of stuff. - disposables.push(controller); - disposables.push(linkChecker); + console.log("Link checker active"); + + // Wire up onChange events + workspace.onDidChangeTextDocument(event => { + checkLinks(event.document, diagnostics) + }, undefined, disposables); + + workspace.onDidOpenTextDocument(event => { + checkLinks(event, diagnostics); + }, undefined, disposables); + + commands.registerCommand('extension.checkBrokenLinks', checkBrokenLinks); } -// Checks links & displays status (so-far) -class LinkChecker { - // For writing to the status bar - private _currentDiagnostics: Disposable; - private _uri: Uri; - - // For disposing - dispose() { - this.disposeCurrentDiagnostics; - } - - // Dispose of current diagnostics - private disposeCurrentDiagnostics() { - if(this._currentDiagnostics) { - this._currentDiagnostics.dispose(); - } - } - - // Show the link count in the status bar - public diagnoseLinks() { - // Get the current text editor - let editor = window.getActiveTextEditor(); - // If it's not an editor, return - if(!editor) { - return; - } - try { - // Get the document - let doc = editor.getTextDocument(); - // Get the document uri - this._uri = doc.getUri(); - - // Only update the status if a Markdown file - if(doc.getLanguageId() === "markdown") { - //Get a promise for an array of markdown links in the document, then... - getLinks(doc).then((links) => { - // Iterate over the array, generating an array of promises - let countryCodePromise = Promise.all(links.map((link): Diagnostic => { - // For each link, check the country code... - return isCountryCodeLink(link, this._uri); - // Then, when they are all done.. - })); - // Finally, let's complete the promise for country code... - countryCodePromise.then((countryCodeDiag) => { - // Then filter out null ones - let filteredDiag = countryCodeDiag.filter(diagnostic => diagnostic != null); - // Then dispose of current diags - this.disposeCurrentDiagnostics; - // Then add the new ones - this._currentDiagnostics = languages.addDiagnostics(filteredDiag); - }) - }).catch(); // do nothing; no links were found - } - } catch(err) { - let message: string=null; - if(typeof err.message==='string' || err.message instanceof String) { - message = err.message; - message = message.replace(/\r?\n/g, ' '); - throw new Error(message); - } - throw err; - } - } -} - -// Check for broken links /* -* This is where we check for broken links by actually requesting the link. -* This took too long to perform in real time as the user changes the document, -* so now it's triggered by Alt+L and the user will wait around for the results +* Checks links for errors. Currently this is just checking for a country code. +* For example, /en-us/ in the URL. +* +* NOTE: Checking for broken links is not integrated in this, as checking for +* those takes a long time, and this function needs to generate diagnostics every +* time the document changes, so needs to complete quickly */ -function checkBrokenLinks() { - let editor = window.getActiveTextEditor; - if(!editor) { - return; - } - try { - //TBD waiting on info for using outputChannel - - // Find the links that are only HTTP/s URIs - // let httpLinks = links.filter(value => isHttpLink(value.address)); - // Iterate over the array of HTTP/s linnks and get an array of promises - // let brokenLinkPromise = Promise.all(httpLinks.map((link): Promise => { - // let countryCodeDiag = isCountryCodeLink(link, this._uri); - // // For each link, generate a promise to return a diagnostic - // if(isHttpLink) - // return getBrokenLinkPromise(link, this._uri); - // // Then, when all the promises have completed - // })); - - } catch(err) { - let message: string=null; - if(typeof err.message==='string' || err.message instanceof String) { - message = err.message; - message = message.replace(/\r?\n/g, ' '); - throw new Error(message); - } - throw err; - } +function checkLinks(document: TextDocument, diagnostics: DiagnosticCollection) { + //Clear the diagnostics because we're sending new ones each time + diagnostics.clear(); + // Get all Markdown style lnks in the document + getLinks(document).then((links) => { + // Iterate over the array, generating an array of promises + let countryCodePromise = Promise.all(links.map((link): Diagnostic => { + // For each link, check the country code... + return isCountryCodeLink(link); + // Then, when they are all done.. + })); + // Finally, let's complete the promise for country code... + countryCodePromise.then((countryCodeDiag) => { + // Then filter out null ones + let filteredDiag = countryCodeDiag.filter(diagnostic => diagnostic != null); + // Then add the diagnostics + diagnostics.set(document.uri, filteredDiag); + }) + }).catch(); } -// Get promise for broken links -function getBrokenLinkPromise(link: Link, documentUri: Uri): Promise { - return new Promise((resolve, reject) => { - // Promise to check the link - brokenLink(link.address, {allow404Pages: true}).then((answer) => { - let brokenLinkDiag: Diagnostic = null; - // If it is broken, create and return a promise - if(answer) { - brokenLinkDiag = createDiagnostic( - DiagnosticSeverity.Error, - link.text, - link.lineText, - link.lineNumber, - documentUri, - `Link ${link.address} is unreachable` - ); - } - // Resolve the promise by returning the diagnostic - resolve(brokenLinkDiag); +function checkBrokenLinks() { + // Get the current document + let document = window.activeTextEditor.document; + // Create an output channel for displaying broken links + let outputChannel = window.createOutputChannel("Checked links"); + // Show the output channel in column three + outputChannel.show(ViewColumn.Three); + + // Get all Markdown style lnks in the document + getLinks(document).then((links) => { + // We only want links that are to HTTP/s addresses + let httpLinks=links.filter(link => isHttpLink(link.address)); + // Loop over those + httpLinks.forEach(link => { + // And check if they are broken or not. + brokenLink(link.address, {allowRedirects: true}).then((answer) => { + // Log to the outputChannel + if(answer) { + outputChannel.appendLine(`Broken: ${link.address} on line ${link.lineText.lineNumber} is unreachable.`); + } else { + outputChannel.appendLine(`OK: ${link.address} on line ${link.lineText.lineNumber}.`); + } + }); }); }); } - + // Parse the MD style links out of the document function getLinks(document: TextDocument): Promise { // Return a promise, since this might take a while for large documents @@ -167,14 +111,14 @@ function getLinks(document: TextDocument): Promise { // Create arrays to hold links as we parse them out let linksToReturn = new Array(); // Get lines in the document - let lineCount = document.getLineCount(); + let lineCount = document.lineCount; //Loop over the lines in a document - for(let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { + for(let lineNumber = 0; lineNumber < lineCount; lineNumber++) { // Get the text for the current line - let lineText = document.getTextOnLine(lineNumber); + let lineText = document.lineAt(lineNumber); // Are there links? - let links = lineText.match(/\[[^\[]+\]\([^\)]+\)|\[[a-zA-z0-9_-]+\]:\s*\S+/g); + let links = lineText.text.match(/\[[^\[]+\]\([^\)]+\)|\[[a-zA-z0-9_-]+\]:\s*\S+/g); if(links) { // Iterate over the links found on this line for(let i = 0; i< links.length; i++) { @@ -188,7 +132,6 @@ function getLinks(document: TextDocument): Promise { linksToReturn.push({ text: link[0], address: address, - lineNumber: lineNumber, lineText: lineText }); } @@ -201,11 +144,11 @@ function getLinks(document: TextDocument): Promise { //Reject, because we found no links reject; } - }); + }).catch(); } // Check for addresses that contain country codes -function isCountryCodeLink(link: Link, documentUri: Uri): Diagnostic { +function isCountryCodeLink(link: Link): Diagnostic { let countryCodeDiag=null; //Regex for country-code let hasCountryCode = link.address.match(/\/[a-z]{2}\-[a-z]{2}\//); @@ -216,8 +159,6 @@ function isCountryCodeLink(link: Link, documentUri: Uri): Diagnostic { DiagnosticSeverity.Warning, link.text, link.lineText, - link.lineNumber, - documentUri, `Link ${link.address} contains a language reference: ${hasCountryCode[0]} ` ); } @@ -231,47 +172,17 @@ function isHttpLink(linkToCheck: string): boolean { } // Generate a diagnostic object -function createDiagnostic(severity: DiagnosticSeverity, markdownLink, lineText, lineNumber, uri, message): Diagnostic { +function createDiagnostic(severity: DiagnosticSeverity, markdownLink, lineText: TextLine, message): Diagnostic { // Get the location of the text in the document // based on position within the line of text it occurs in - let startPos = lineText.indexOf(markdownLink); + let startPos = lineText.text.indexOf(markdownLink); let endPos = startPos + markdownLink.length -1; - let start = new Position(lineNumber,startPos); - let end = new Position(lineNumber, endPos); + let start = new Position(lineText.lineNumber,startPos); + let end = new Position(lineText.lineNumber, endPos); let range = new Range(start, end); - let loc = new Location(uri, range); // Create the diagnostic object - let diag = new Diagnostic(severity, loc, message); + let diag = new Diagnostic(range, message, severity); // Return the diagnostic return diag; } -class LinkCheckController { - private _linkChecker: LinkChecker; - private _disposable: Disposable; - - constructor(linkChecker: LinkChecker) { - this._linkChecker = linkChecker; - this._linkChecker.diagnoseLinks(); - - // Register a command (broken link check) - commands.registerCommand('extension.checkBrokenLinks', checkBrokenLinks); - - // Subscribe to selection changes - let subscriptions: Disposable[] = []; - window.onDidChangeTextEditorSelection(this._onEvent, this, subscriptions); - window.onDidChangeActiveTextEditor(this._onEvent, this, subscriptions); - - // create a combined disposable from both event subscriptions - this._disposable = Disposable.of(...subscriptions); - } - - dispose() { - this._disposable.dispose(); - } - - private _onEvent() { - this._linkChecker.diagnoseLinks(); - } -} - diff --git a/package.json b/package.json index 8d4b054..da8d435 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "publisher": "Larry Franks & Ralph Squillace", "engines": { - "vscode": "*" + "vscode": "0.10.x" }, "activationEvents": [ "onLanguage:markdown" diff --git a/typings/broken-link.d.ts b/typings/broken-link.d.ts index ad2fc5e..5b9435a 100644 --- a/typings/broken-link.d.ts +++ b/typings/broken-link.d.ts @@ -10,6 +10,6 @@ interface IBrokenLinkOptions { } declare module "broken-link" { - function brokenLink(url: string, options?: IBrokenLinkOptions): Promise; + function brokenLink(url: string, options?: IBrokenLinkOptions): Promise; export = brokenLink; } \ No newline at end of file