This repository has been archived by the owner on May 9, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathextension.ts
247 lines (226 loc) · 10.1 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// The some things from 'vscode', which contains the VS Code extensibility API
import {
workspace,
window,
commands,
languages,
Diagnostic,
DiagnosticSeverity,
DiagnosticCollection,
Range,
OutputChannel,
Position,
Uri,
Disposable,
TextDocument,
TextLine,
ViewColumn,
WorkspaceConfiguration} from 'vscode';
// For HTTP/s address validation
import validator = require('validator');
// For checking broken links
import brokenLink = require('broken-link');
// For checking relative URIs against the local file system
import path = require('path');
import fs = require('fs');
//Interface for links
interface Link {
text: string
address: string
lineText: TextLine
}
const configSection = "linkcheckmd";
let configuration: WorkspaceConfiguration = workspace.getConfiguration(configSection);
// 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 diagnostics collection
let diagnostics = languages.createDiagnosticCollection();
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);
workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration(configSection)) {
configuration = workspace.getConfiguration(configSection);
}
});
commands.registerCommand('extension.generateLinkReport', generateLinkReport);
}
/*
* 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 checkLinks(document: TextDocument, diagnostics: DiagnosticCollection) {
//Clear the diagnostics because we're sending new ones each time
diagnostics.clear();
// Get all Markdown style links in the document
getLinks(document).then((links) => {
if (configuration.get("enableCountryCodeCheck", true)) {
// Iterate over the array, generating an array of promises
let countryCodePromise = Promise.all<Diagnostic>(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();
}
// Generate a report of broken, country code, etc. links and the line they occur on
function generateLinkReport() {
// 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) => {
// Loop over the links
links.forEach(link => {
// Get the line number, because internally it's 0 based, but not in display
let lineNumber = link.lineText.lineNumber + 1;
// Is it an HTTPS link or a relative link?
if(isHttpLink(link.address)) {
// And check if they are broken or not.
brokenLink(link.address, {allowRedirects: true}).then((answer) => {
// Also check for country code
if(configuration.get("enableCountryCodeCheck", true) && hasCountryCode(link.address)) {
outputChannel.appendLine(`Warning: ${link.address} on line ${lineNumber} contains a language reference code.`);
}
// Log to the outputChannel
if(answer) {
outputChannel.appendLine(`Error: ${link.address} on line ${lineNumber} is unreachable.`);
} else {
outputChannel.appendLine(`Info: ${link.address} on line ${lineNumber}.`);
}
});
} else {
if(isFtpLink(link.address)) {
// We don't do anything with FTP
outputChannel.appendLine(`Info: ${link.address} on line ${lineNumber} is an FTP link.`);
} else {
// Must be a relative path, but might not be, so try it...
try {
// Find the directory from the path to the current document
let currentWorkingDirectory = path.dirname(document.fileName);
// Use that to resolve the full path from the relative link address
// The `.split('#')[0]` at the end is so that relative links that also reference an anchor in the document will work with file checking.
let fullPath = path.resolve(currentWorkingDirectory, link.address).split('#')[0];
// Check if the file exists and log appropriately
if(fs.existsSync(fullPath)) {
outputChannel.appendLine(`Info: ${link.address} on line ${lineNumber}.`);
} else {
outputChannel.appendLine(`Error: ${link.address} on line ${lineNumber} does not exist.`);
}
} catch (error) {
// If there's an error, log the link
outputChannel.appendLine(`Error: ${link.address} on line ${lineNumber} is not an HTTP/s or relative link.`);
}
}
}
});
});
}
// Parse the MD style links out of the document
function getLinks(document: TextDocument): Promise<Link[]> {
// Return a promise, since this might take a while for large documents
return new Promise<Link[]>((resolve, reject) => {
// Create arrays to hold links as we parse them out
let linksToReturn = new Array<Link>();
// Get lines in the document
let lineCount = document.lineCount;
//Loop over the lines in a document
for(let lineNumber = 0; lineNumber < lineCount; lineNumber++) {
// Get the text for the current line
let lineText = document.lineAt(lineNumber);
// Are there links?
let links = lineText.text.match(/\[[^\[]+\]\(([^\)]+(\)[a-zA-Z0-9-]*.\w*\)|\)))|\[[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++) {
// Get the URL from each individual link
// ([^\)]+) captures inline style link address
// (\S+) captures reference style link address
var link = links[i].match(/\[[^\[]+\]\(([^\)]+(\)[a-zA-Z0-9-]*.\w+\)|\)))|\[[a-zA-z0-9_-]+\]:\s*(\S+)/);
// Figure out which capture contains the address; inline style or reference
let address = (link[3] == null) ? link[1].slice(0, -1) : link[3];
//Push it to the array
linksToReturn.push({
text: link[0],
address: address,
lineText: lineText
});
}
}
}
if(linksToReturn.length > 0) {
//Return the populated array, which completes the promise.
resolve(linksToReturn);
} else {
//Reject, because we found no links
reject;
}
}).catch();
}
// Check for addresses that contain country codes
function isCountryCodeLink(link: Link): Diagnostic {
let countryCodeDiag=null;
//If one was found...
if(hasCountryCode(link.address)) {
//Create the diagnostics object
countryCodeDiag = createDiagnostic(
DiagnosticSeverity.Warning,
link.text,
link.lineText,
`Link ${link.address} contains a language reference code.`,
'LNK0001'
);
}
return countryCodeDiag;
}
function hasCountryCode(linkToCheck: string): boolean {
//Regex for country-code
let hasCountryCode = linkToCheck.match(/(.com|aka\.ms)\/[a-z]{2}\-[a-z]{2}\//);
return hasCountryCode ? true : false;
}
// Is this a valid HTTP/S link?
function isHttpLink(linkToCheck: string): boolean {
// Use the validator to avoid writing URL checking logic
return validator.isURL(linkToCheck, {require_protocol: true, protocols: ['http','https']}) ? true : false;
}
// Is this an FTP link?
function isFtpLink(linkToCheck: string): boolean {
return linkToCheck.toLowerCase().startsWith('ftp');
}
// Generate a diagnostic object
function createDiagnostic(severity: DiagnosticSeverity, markdownLink, lineText: TextLine, message, code): Diagnostic {
// Get the location of the text in the document
// based on position within the line of text it occurs in
let startPos = lineText.text.indexOf(markdownLink);
let endPos = startPos + markdownLink.length -1;
let start = new Position(lineText.lineNumber,startPos);
let end = new Position(lineText.lineNumber, endPos);
let range = new Range(start, end);
// Create the diagnostic object
let diag = new Diagnostic(range, message, severity);
diag.code = code;
diag.source = "linkcheckmd";
// Return the diagnostic
return diag;
}