Skip to content

Commit e2b075d

Browse files
committed
feat: html validator custom config support-htmlvalidate.json
1 parent 010dc47 commit e2b075d

File tree

7 files changed

+229
-17
lines changed

7 files changed

+229
-17
lines changed

src/extensions/default/HTMLCodeHints/html-lint.js

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
*/
2121

22-
// Parts of this file is adapted from https://github.com/cfjedimaster/brackets-jshint
22+
/* global path*/
2323

2424
/**
2525
* Provides JSLint results via the core linting extension point
@@ -28,16 +28,26 @@ define(function (require, exports, module) {
2828

2929
// Load dependent modules
3030
const CodeInspection = brackets.getModule("language/CodeInspection"),
31+
AppInit = brackets.getModule("utils/AppInit"),
3132
Strings = brackets.getModule("strings"),
33+
StringUtils = brackets.getModule("utils/StringUtils"),
34+
FileSystemError = brackets.getModule("filesystem/FileSystemError"),
35+
DocumentManager = brackets.getModule("document/DocumentManager"),
3236
EditorManager = brackets.getModule("editor/EditorManager"),
3337
ProjectManager = brackets.getModule("project/ProjectManager"),
3438
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
39+
Metrics = brackets.getModule("utils/Metrics"),
40+
FileSystem = brackets.getModule("filesystem/FileSystem"),
3541
IndexingWorker = brackets.getModule("worker/IndexingWorker");
3642

3743
IndexingWorker.loadScriptInWorker(`${module.uri}/../worker/html-worker.js`);
3844

3945
const prefs = PreferencesManager.getExtensionPrefs("HTMLLint");
4046
const PREFS_HTML_LINT_DISABLED = "disabled";
47+
const CONFIG_FILE_NAME = ".htmlvalidate.json";
48+
const UNSUPPORTED_CONFIG_FILES = [".htmlvalidate.js", ".htmlvalidate.cjs"];
49+
50+
let projectSpecificOptions, configErrorMessage, configID = 0;
4151

4252
prefs.definePreference(PREFS_HTML_LINT_DISABLED, "boolean", false, {
4353
description: Strings.DESCRIPTION_HTML_LINT_DISABLE
@@ -54,15 +64,30 @@ define(function (require, exports, module) {
5464
}
5565
}
5666

67+
function _getLinterConfigFileErrorMsg() {
68+
return [{
69+
// JSLint returns 1-based line/col numbers
70+
pos: { line: -1, ch: 0 },
71+
message: configErrorMessage,
72+
type: CodeInspection.Type.ERROR
73+
}];
74+
}
75+
5776
/**
5877
* Run JSLint on the current document. Reports results to the main UI. Displays
5978
* a gold star when no errors are found.
6079
*/
6180
async function lintOneFile(text, fullPath) {
6281
return new Promise((resolve, reject)=>{
82+
if(configErrorMessage){
83+
resolve({ errors: _getLinterConfigFileErrorMsg() });
84+
return;
85+
}
6386
IndexingWorker.execPeer("htmlLint", {
6487
text,
65-
filePath: fullPath
88+
filePath: fullPath,
89+
configID,
90+
config: projectSpecificOptions
6691
}).then(lintResult =>{
6792
const editor = EditorManager.getCurrentFullEditor();
6893
if(!editor || editor.document.file.fullPath !== fullPath) {
@@ -88,6 +113,110 @@ define(function (require, exports, module) {
88113
});
89114
}
90115

116+
function _readConfig(dir) {
117+
return new Promise((resolve, reject)=>{
118+
const configFilePath = path.join(dir, CONFIG_FILE_NAME);
119+
let displayPath = ProjectManager.getProjectRelativeOrDisplayPath(configFilePath);
120+
DocumentManager.getDocumentForPath(configFilePath).done(function (configDoc) {
121+
let config;
122+
const content = configDoc.getText();
123+
try {
124+
config = JSON.parse(content);
125+
console.log("html-lint: loaded config file for project " + configFilePath);
126+
} catch (e) {
127+
console.log("html-lint: error parsing " + configFilePath, content, e);
128+
// just log and return as this is an expected failure for us while the user edits code
129+
reject(StringUtils.format(Strings.HTML_LINT_CONFIG_JSON_ERROR, displayPath));
130+
return;
131+
}
132+
resolve(config);
133+
}).fail((err)=>{
134+
if(err === FileSystemError.NOT_FOUND){
135+
resolve(null); // no config file is a valid case. we just resolve with null
136+
return;
137+
}
138+
console.error("Error reading JSHint Config File", configFilePath, err);
139+
reject("Error reading JSHint Config File", displayPath);
140+
});
141+
});
142+
}
143+
144+
async function _validateUnsupportedConfig(scanningProjectPath) {
145+
let errorMessage;
146+
for(let unsupportedFileName of UNSUPPORTED_CONFIG_FILES) {
147+
let exists = await FileSystem.existsAsync(path.join(scanningProjectPath, unsupportedFileName));
148+
if(exists) {
149+
errorMessage = StringUtils.format(Strings.HTML_LINT_CONFIG_UNSUPPORTED, unsupportedFileName);
150+
break;
151+
}
152+
}
153+
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath) {
154+
// this is a rare race condition where the user switches project between the config reload
155+
// Eg. in integ tests. do nothing as another scan for the new project will be in progress.
156+
return;
157+
}
158+
configErrorMessage = errorMessage;
159+
CodeInspection.requestRun(Strings.HTML_LINT_NAME);
160+
}
161+
162+
function _reloadOptions() {
163+
projectSpecificOptions = null;
164+
configErrorMessage = null;
165+
const scanningProjectPath = ProjectManager.getProjectRoot().fullPath;
166+
configID++;
167+
_readConfig(scanningProjectPath, CONFIG_FILE_NAME).then((config)=>{
168+
configID++;
169+
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath){
170+
// this is a rare race condition where the user switches project between the get document call.
171+
// Eg. in integ tests. do nothing as another scan for the new project will be in progress.
172+
return;
173+
}
174+
if(config) {
175+
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "html", "configPresent");
176+
projectSpecificOptions = config;
177+
configErrorMessage = null;
178+
CodeInspection.requestRun(Strings.HTML_LINT_NAME);
179+
} else {
180+
_validateUnsupportedConfig(scanningProjectPath)
181+
.catch(console.error);
182+
}
183+
}).catch((err)=>{
184+
configID++;
185+
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath){
186+
return;
187+
}
188+
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "HTMLConfig", "error");
189+
configErrorMessage = err;
190+
CodeInspection.requestRun(Strings.HTML_LINT_NAME);
191+
});
192+
}
193+
194+
function _isFileInArray(pathToMatch, filePathArray){
195+
if(!filePathArray){
196+
return false;
197+
}
198+
for(let filePath of filePathArray){
199+
if(filePath === pathToMatch){
200+
return true;
201+
}
202+
}
203+
return false;
204+
}
205+
206+
function _projectFileChanged(_evt, changedPath, added, removed) {
207+
let configFilePath = path.join(ProjectManager.getProjectRoot().fullPath, CONFIG_FILE_NAME);
208+
if(changedPath=== configFilePath
209+
|| _isFileInArray(configFilePath, added) || _isFileInArray(configFilePath, removed)){
210+
_reloadOptions();
211+
}
212+
}
213+
214+
AppInit.appReady(function () {
215+
ProjectManager.on(ProjectManager.EVENT_PROJECT_PATH_CHANGED_OR_RENAMED, _projectFileChanged);
216+
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN, _reloadOptions);
217+
_reloadOptions();
218+
});
219+
91220
CodeInspection.register("html", {
92221
name: Strings.HTML_LINT_NAME,
93222
scanFileAsync: lintOneFile,

src/extensions/default/HTMLCodeHints/worker/html-worker.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,42 @@
2222

2323
(function () {
2424
let htmlValidator = HTMLLanguageService.createHTMLValidator({
25-
extends: ["html-validate:standard"]
26-
});
25+
extends: ["html-validate:standard"]
26+
});
27+
28+
let isUsingCustomConfig = false, currentConfigID;
29+
30+
function setupValidator(config, configID) {
31+
try{
32+
if(!config && isUsingCustomConfig) {
33+
// reset the config
34+
htmlValidator = HTMLLanguageService.createHTMLValidator({
35+
extends: ["html-validate:standard"]
36+
});
37+
isUsingCustomConfig = false;
38+
currentConfigID = null;
39+
} else if(config && currentConfigID !== configID) {
40+
htmlValidator = HTMLLanguageService.createHTMLValidator(config);
41+
isUsingCustomConfig = true;
42+
currentConfigID = configID;
43+
}
44+
return null;
45+
} catch (e) {
46+
return e.message;
47+
}
48+
}
49+
2750
async function htmlLint(params) {
51+
let errorMessage = setupValidator(params.config, params.configID);
52+
if(errorMessage) {
53+
return [{
54+
start: 0,
55+
end: 0,
56+
severity: 2, // 1 warning and 2 is error
57+
message: "Invalid config file `.htmlvalidate.json`"+ errorMessage,
58+
ruleId: "INVALID_CONFIG"
59+
}];
60+
}
2861
const validatorResult = await htmlValidator.validateString(params.text, params.filePath);
2962
if(!validatorResult || !validatorResult.results || !validatorResult.results.length){
3063
return [];
@@ -48,5 +81,14 @@
4881
return errors;
4982
}
5083

84+
async function updateHTMLLintConfig(params) {
85+
if(params.config){
86+
console.error("HTML Lint worker updateHTMLLintConfig received null config", params);
87+
return;
88+
}
89+
htmlValidator = HTMLLanguageService.createHTMLValidator(params.config);
90+
}
91+
5192
WorkerComm.setExecHandler("htmlLint", htmlLint);
93+
WorkerComm.setExecHandler("updateHTMLLintConfig", updateHTMLLintConfig);
5294
}());

src/extensions/default/JSLint/ESLint.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ define(function (require, exports, module) {
236236
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath){
237237
return;
238238
}
239-
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "eslintErr", "project");
239+
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "eslintConfig", "error");
240240
useESLintFromProject = false;
241241
CodeInspection.requestRun(Strings.ESLINT_NAME);
242242
});

src/extensions/default/JSLint/JSHint.js

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@
2929
define(function (require, exports, module) {
3030

3131
// Load dependent modules
32-
const _ = brackets.getModule("thirdparty/lodash"),
33-
CodeInspection = brackets.getModule("language/CodeInspection"),
32+
const CodeInspection = brackets.getModule("language/CodeInspection"),
3433
FileSystemError = brackets.getModule("filesystem/FileSystemError"),
3534
AppInit = brackets.getModule("utils/AppInit"),
3635
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
3736
DocumentManager = brackets.getModule("document/DocumentManager"),
3837
Strings = brackets.getModule("strings"),
38+
StringUtils = brackets.getModule("utils/StringUtils"),
3939
ProjectManager = brackets.getModule("project/ProjectManager"),
4040
FileSystem = brackets.getModule("filesystem/FileSystem"),
4141
IndexingWorker = brackets.getModule("worker/IndexingWorker"),
@@ -163,8 +163,7 @@ define(function (require, exports, module) {
163163
return new Promise((resolve, reject)=>{
164164
configFileName = configFileName || CONFIG_FILE_NAME;
165165
const configFilePath = path.join(dir, configFileName);
166-
let displayPath = ProjectManager.makeProjectRelativeIfPossible(configFilePath);
167-
displayPath = ProjectManager.getProjectRelativeOrDisplayPath(displayPath);
166+
let displayPath = ProjectManager.getProjectRelativeOrDisplayPath(configFilePath);
168167
DocumentManager.getDocumentForPath(configFilePath).done(function (configDoc) {
169168
if (!ProjectManager.isWithinProject(configFilePath)) {
170169
// this is a rare race condition where the user switches project between the get document call.
@@ -180,7 +179,7 @@ define(function (require, exports, module) {
180179
} catch (e) {
181180
console.log("JSHint: error parsing " + configFilePath, content, e);
182181
// just log and return as this is an expected failure for us while the user edits code
183-
reject("Error parsing JSHint config file: " + displayPath);
182+
reject(StringUtils.format(Strings.JSHINT_CONFIG_JSON_ERROR, displayPath));
184183
return;
185184
}
186185
// Load any base config defined by "extends".
@@ -199,10 +198,9 @@ define(function (require, exports, module) {
199198
}).catch(()=>{
200199
let extendDisplayPath = ProjectManager.makeProjectRelativeIfPossible(extendFile.fullPath);
201200
extendDisplayPath = ProjectManager.getProjectRelativeOrDisplayPath(extendDisplayPath);
202-
reject("Error parsing JSHint config file: " + extendDisplayPath);
201+
reject(StringUtils.format(Strings.JSHINT_CONFIG_JSON_ERROR, extendDisplayPath));
203202
});
204-
}
205-
else {
203+
} else {
206204
resolve(config);
207205
}
208206
}).fail((err)=>{
@@ -211,13 +209,14 @@ define(function (require, exports, module) {
211209
return;
212210
}
213211
console.error("Error reading JSHint Config File", configFilePath, err);
214-
reject("Error reading JSHint Config File", displayPath);
212+
reject(StringUtils.format(Strings.JSHINT_CONFIG_ERROR, displayPath));
215213
});
216214
});
217215
}
218216

219217
function _reloadOptions() {
220218
projectSpecificOptions = null;
219+
jsHintConfigFileErrorMessage = null;
221220
const scanningProjectPath = ProjectManager.getProjectRoot().fullPath;
222221
_readConfig(scanningProjectPath, CONFIG_FILE_NAME).then((config)=>{
223222
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath){
@@ -235,7 +234,7 @@ define(function (require, exports, module) {
235234
if(scanningProjectPath !== ProjectManager.getProjectRoot().fullPath){
236235
return;
237236
}
238-
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "jsHintErr", "project");
237+
Metrics.countEvent(Metrics.EVENT_TYPE.LINT, "jsHintConfig", "error");
239238
jsHintConfigFileErrorMessage = err;
240239
CodeInspection.requestRun(Strings.JSHINT_NAME);
241240
});

src/nls/root/strings.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,13 +898,17 @@ define({
898898

899899
// extensions/default/JSLint
900900
"JSHINT_NAME": "JSHint",
901+
"JSHINT_CONFIG_ERROR": "Error reading JSHint config file: {0}",
902+
"JSHINT_CONFIG_JSON_ERROR": "Error: JSHint config file `{0}` is not valid JSON",
901903
"ESLINT_NAME": "ESLint",
902904

903905
// extension css code hints
904906
"CSS_LINT_NAME": "{0}",
905907

906908
// html lint
907909
"HTML_LINT_NAME": "HTML",
910+
"HTML_LINT_CONFIG_JSON_ERROR": "Error: HTML Validator config file `{0}` is not valid JSON",
911+
"HTML_LINT_CONFIG_UNSUPPORTED": "Error: Unsupported config format `{0}`. Use JSON config `.htmlvalidate.json`",
908912

909913
// Features/QuickView and quick view extensions
910914
"CMD_ENABLE_QUICK_VIEW": "Quick View on Hover",

src/project/ProjectManager.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ define(function (require, exports, module) {
9292
EVENT_AFTER_STARTUP_FILES_LOADED = "startupFilesLoaded",
9393
EVENT_PROJECT_REFRESH = "projectRefresh",
9494
EVENT_CONTENT_CHANGED = "contentChanged",
95+
// This will capture all file/folder changes in projects except renames. If you want to track renames,
96+
// use EVENT_PROJECT_PATH_CHANGED_OR_RENAMED to track all changes or EVENT_PROJECT_FILE_RENAMED too.
9597
EVENT_PROJECT_FILE_CHANGED = "projectFileChanged",
96-
EVENT_PROJECT_FILE_RENAMED = "projectFileRenamed";
98+
EVENT_PROJECT_FILE_RENAMED = "projectFileRenamed",
99+
// the path changed event differs in the sense that all events returned by this will be a path.
100+
EVENT_PROJECT_PATH_CHANGED_OR_RENAMED = "projectPathChanged";
97101

98102
EventDispatcher.setLeakThresholdForEvent(EVENT_PROJECT_OPEN, 25);
99103

@@ -2021,6 +2025,32 @@ define(function (require, exports, module) {
20212025
return !unsafeExit;
20222026
}
20232027

2028+
function _entryToPathArray(entryArray) {
2029+
if(!entryArray || !entryArray.length) {
2030+
return [];
2031+
}
2032+
return entryArray.map(entry => path.normalize(entry.fullPath));
2033+
}
2034+
2035+
exports.on(EVENT_PROJECT_FILE_CHANGED, (_evt, entry, addedInProject, removedInProject)=>{
2036+
exports.trigger(EVENT_PROJECT_PATH_CHANGED_OR_RENAMED, entry && path.normalize(entry.fullPath),
2037+
_entryToPathArray(addedInProject), _entryToPathArray(removedInProject));
2038+
});
2039+
exports.on(EVENT_PROJECT_FILE_RENAMED, (_evt, oldPath, newPath)=>{
2040+
oldPath = path.normalize(oldPath);
2041+
newPath = path.normalize(newPath);
2042+
if(oldPath === newPath){
2043+
return; // no change
2044+
}
2045+
const oldParent = path.dirname(oldPath), newParent = path.dirname(newPath);
2046+
if(oldParent === newParent) {
2047+
exports.trigger(EVENT_PROJECT_PATH_CHANGED_OR_RENAMED, newParent, [newPath], [oldPath]);
2048+
} else {
2049+
exports.trigger(EVENT_PROJECT_PATH_CHANGED_OR_RENAMED, oldParent, [], [oldPath]);
2050+
exports.trigger(EVENT_PROJECT_PATH_CHANGED_OR_RENAMED, newParent, [newPath], []);
2051+
}
2052+
});
2053+
20242054
exports.on(EVENT_PROJECT_OPEN, (_evt, projectRoot)=>{
20252055
_reloadProjectPreferencesScope();
20262056
_saveProjectPath();
@@ -2285,5 +2315,6 @@ define(function (require, exports, module) {
22852315
exports.EVENT_CONTENT_CHANGED = EVENT_CONTENT_CHANGED;
22862316
exports.EVENT_PROJECT_FILE_CHANGED = EVENT_PROJECT_FILE_CHANGED;
22872317
exports.EVENT_PROJECT_FILE_RENAMED = EVENT_PROJECT_FILE_RENAMED;
2318+
exports.EVENT_PROJECT_PATH_CHANGED_OR_RENAMED = EVENT_PROJECT_PATH_CHANGED_OR_RENAMED;
22882319
exports.EVENT_PROJECT_OPEN_FAILED = EVENT_PROJECT_OPEN_FAILED;
22892320
});

0 commit comments

Comments
 (0)