Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",

Check warning on line 3 in .vscode/extensions.json

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (dbaeumer)
"editorconfig.editorconfig",
"esbenp.prettier-vscode",

Check warning on line 5 in .vscode/extensions.json

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (esbenp)
"streetsidesoftware.code-spell-checker"
]
}
7 changes: 5 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.defaultFormatter": "EditorConfig.EditorConfig",
"[javascript][typescript][json][jsonc][markdown][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",

Check warning on line 7 in .vscode/settings.json

View workflow job for this annotation

GitHub Actions / spellcheck

Unknown word (esbenp)
"editor.formatOnSave": true
},
"editor.renderWhitespace": "all",
"editor.trimAutoWhitespace": true,
"files.trimTrailingWhitespace": true
Expand Down
76 changes: 47 additions & 29 deletions htmlhint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,64 +19,74 @@
*/

import * as path from "path";
import * as fs from "fs";
import {
createConnection,
TextDocuments,
Diagnostic,
DiagnosticSeverity,
ProposedFeatures,
InitializeParams,
TextDocumentSyncKind,
InitializeResult,
Connection,
ErrorMessageTracker,
CancellationToken,
CodeActionParams,
Command,
CodeAction,
CodeActionKind,
TextEdit,
WorkspaceEdit,
CodeDescription,
} from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
import * as htmlhint from "htmlhint";
import fs = require("fs");
import { URI } from "vscode-uri";
import ignore from "ignore";
let stripJsonComments: any = require("strip-json-comments");
import stripJsonComments from "strip-json-comments";

// Cache for gitignore patterns to avoid repeatedly parsing .gitignore files
let gitignoreCache: Map<string, any> = new Map();
let gitignoreCache: Map<string, ReturnType<typeof ignore>> = new Map();

// Cache for workspace root detection to avoid repeated filesystem calls
let workspaceRootCache: Map<string, string | null> = new Map();

interface HtmlHintSettings {
configFile: string;
enable: boolean;
options: Record<string, unknown>;
optionsFile: string;
ignoreGitignore: boolean;
}

interface Settings {
htmlhint: {
configFile: string;
enable: boolean;
options: any;
optionsFile: string;
ignoreGitignore: boolean;
};
[key: string]: any;
htmlhint: HtmlHintSettings;
[key: string]: unknown;
}

interface HtmlHintConfig {
[key: string]: unknown;
}

let settings: Settings | null = null;
let linter: any = null;
let linter: {
verify: (text: string, config?: HtmlHintConfig) => htmlhint.Error[];
} | null = null;

/**
* This variable is used to cache loaded htmlhintrc objects. It is a dictionary from path -> config object.
* A value of null means a .htmlhintrc object didn't exist at the given path.
* A value of undefined means the file at this path hasn't been loaded yet, or should be reloaded because it changed
*/
let htmlhintrcOptions: any = {};
let htmlhintrcOptions: Record<string, HtmlHintConfig | null | undefined> = {};

/**
* Given an htmlhint Error object, approximate the text range highlight
*/
function getRange(error: htmlhint.Error, lines: string[]): any {
function getRange(
error: htmlhint.Error,
lines: string[],
): {
start: { line: number; character: number };
end: { line: number; character: number };
} {
let line = lines[error.line - 1];
if (!line) {
// Fallback if line doesn't exist
Expand Down Expand Up @@ -157,8 +167,8 @@
* Get the HTMLHint configuration settings for the given HTML file. This method will take care of whether to use
* VS Code settings, or to use a .htmlhintrc file.
*/
function getConfiguration(filePath: string): any {
let options: any;
function getConfiguration(filePath: string): HtmlHintConfig {
let options: HtmlHintConfig | undefined;

trace(`[HTMLHint Debug] Getting configuration for file: ${filePath}`);
trace(`[HTMLHint Debug] Current settings: ${JSON.stringify(settings)}`);
Expand Down Expand Up @@ -228,8 +238,8 @@
* Given the path of an HTML file, this function will look in current directory & parent directories
* to find a .htmlhintrc file to use as the linter configuration. The settings are
*/
function findConfigForHtmlFile(base: string) {
let options: any;
function findConfigForHtmlFile(base: string): HtmlHintConfig | undefined {
let options: HtmlHintConfig | undefined;
trace(`[HTMLHint Debug] Looking for config starting from: ${base}`);

if (fs.existsSync(base)) {
Expand Down Expand Up @@ -285,8 +295,8 @@
/**
* Given a path to a .htmlhintrc file, load it into a javascript object and return it.
*/
function loadConfigurationFile(configFile: string): any {
let ruleset: any = null;
function loadConfigurationFile(configFile: string): HtmlHintConfig | null {
let ruleset: HtmlHintConfig | null = null;
trace(`[HTMLHint Debug] Attempting to load config file: ${configFile}`);
if (fs.existsSync(configFile)) {
trace(`[HTMLHint Debug] Config file exists, reading: ${configFile}`);
Expand Down Expand Up @@ -339,16 +349,22 @@
return;
}

let tracker = new ErrorMessageTracker();
// Collect all errors and send them together instead of using ErrorMessageTracker
const errors: string[] = [];
documents.forEach((document) => {
try {
trace(`[DEBUG] Revalidating document: ${document.uri}`);
validateTextDocument(connection, document);
} catch (err) {
tracker.add(getErrorMessage(err, document));
errors.push(getErrorMessage(err, document));
}
});
tracker.sendErrors(connection);

// Send all errors at once if there are any
if (errors.length > 0) {
connection.window.showErrorMessage(errors.join("\n"));
}

trace(`[DEBUG] validateAllTextDocuments completed`);
}

Expand All @@ -363,7 +379,7 @@
}
}

let connection: Connection = createConnection(ProposedFeatures.all);
let connection: Connection = createConnection();
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
documents.listen(connection);

Expand Down Expand Up @@ -1914,7 +1930,9 @@
: undefined;

// Since Files API is no longer available, we'll use embedded htmlhint directly
linter = htmlhint.default || htmlhint.HTMLHint || htmlhint;
linter = (htmlhint.default ||
htmlhint.HTMLHint ||
htmlhint) as typeof linter;

let result: InitializeResult = {
capabilities: {
Expand Down
25 changes: 25 additions & 0 deletions test/autofix/tag-no-obsolete-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Tag No Obsolete Test" />
<title>Tag No Obsolete Test</title>
</head>
<body>
<!-- Test case 1: Obsolete acronym tag (should be converted to abbr) -->
<acronym title="HyperText Markup Language">HTML</acronym>

<!-- Test case 2: Multiple acronym tags -->
<p>This is an <acronym title="World Wide Web">WWW</acronym> example with <acronym title="Uniform Resource Locator">URL</acronym>.</p>

<!-- Test case 3: Acronym with attributes -->
<acronym title="Cascading Style Sheets" class="highlight">CSS</acronym>

<!-- Test case 4: Already correct abbr tag - should not trigger -->
<abbr title="JavaScript">JS</abbr>

<!-- Test case 5: Mixed case acronym -->
<ACRONYM title="Extensible Markup Language">XML</ACRONYM>
</body>
</html>
15 changes: 15 additions & 0 deletions test/autofix/test-autofixes.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@

<!-- More void elements -->
<img src="footer.jpg">

<!-- Test case 1: Obsolete acronym tag (should be converted to abbr) -->
<acronym title="HyperText Markup Language">HTML</acronym>

<!-- Test case 2: Multiple acronym tags -->
<p>This is an <acronym title="World Wide Web">WWW</acronym> example with <acronym title="Uniform Resource Locator">URL</acronym>.</p>

<!-- Test case 3: Acronym with attributes -->
<acronym title="Cascading Style Sheets" class="highlight">CSS</acronym>

<!-- Test case 4: Already correct abbr tag - should not trigger -->
<abbr title="JavaScript">JS</abbr>

<!-- Test case 5: Mixed case acronym -->
<ACRONYM title="Extensible Markup Language">XML</ACRONYM>
<br />
</body>
</html>