Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Refactor HtmlBeautifier class and improve error handling #73

Merged
merged 2 commits into from
Aug 31, 2024
Merged
Changes from all commits
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
231 changes: 131 additions & 100 deletions src/formatter/htmlbeautifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,39 @@ import * as cp from "child_process";
const isWsl = require("is-wsl");

export default class HtmlBeautifier {
private logChannel: vscode.LogOutputChannel;

constructor() {
this.logChannel = vscode.window.createOutputChannel("ERB Beautifier", {
log: true,
});
}

/**
* Formats the given input using HTML Beautifier
* @param {string} input - The input to be formatted
* @returns {Promise<string>} The formatted input
* Formats the input string using HTML Beautifier.
* @param input The input string to be formatted.
* @returns A promise that resolves to the formatted string.
*/
public async format(input: string): Promise<string> {
try {
const cmd = `${this.exe} ${this.cliOptions.join(
" "
)} with custom env ${JSON.stringify(this.customEnvVars)}`;
console.log(`Formatting ERB with command: ${cmd}`);
console.time(cmd);

const startTime = Date.now();
const result = await this.executeCommand(input);

console.timeEnd(cmd);
const duration = Date.now() - startTime;
this.logChannel.info(
`Formatting completed successfully in ${duration}ms.`
);
return result;
} catch (error) {
console.error(error);
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
vscode.window.showErrorMessage(
`Error occurred while formatting: ${errorMessage}`
);
this.handleError(error, "Error occurred while formatting");
throw error;
}
}

/**
* Executes the formatting command with the provided input.
* @param input The input to format.
* @returns A promise that resolves to the formatted output.
*/
private executeCommand(input: string): Promise<string> {
return new Promise((resolve, reject) => {
// Handle spawn EINVAL error on Windows. See https://github.com/nodejs/node/issues/52554
Expand All @@ -44,53 +49,37 @@ export default class HtmlBeautifier {
...shellOptions,
});

if (htmlbeautifier.stdin === null || htmlbeautifier.stdout === null) {
const msg = "Couldn't initialize STDIN or STDOUT";
console.warn(msg);
vscode.window.showErrorMessage(msg);
reject(new Error(msg));
return;
}

let formattedResult = "";
let errorMessage = "";
let stdoutChunks: Buffer[] = [];
let stderrChunks: Buffer[] = [];
const fullCommand = `${this.exe} ${this.cliOptions.join(" ")} (cwd: ${
vscode.workspace.rootPath || __dirname
}) with custom env: ${JSON.stringify(this.customEnvVars)}`;
this.logChannel.info(`Formatting ERB with command: ${fullCommand}`);

htmlbeautifier.on("error", (err) => {
console.warn(err);
vscode.window.showErrorMessage(
`Couldn't run ${this.exe} '${err.message}'`
if (!htmlbeautifier.stdin || !htmlbeautifier.stdout) {
return this.handleSpawnError(
reject,
"Couldn't initialize STDIN or STDOUT"
);
reject(err);
});

htmlbeautifier.stdout.on("data", (chunk) => {
stdoutChunks.push(chunk);
});
}

htmlbeautifier.stdout.on("end", () => {
let result = Buffer.concat(stdoutChunks).toString();
formattedResult = this.handleFinalNewline(input, result);
});
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];

htmlbeautifier.stderr.on("data", (chunk) => {
stderrChunks.push(chunk);
});
htmlbeautifier.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
htmlbeautifier.stderr.on("data", (chunk) => stderrChunks.push(chunk));

htmlbeautifier.stderr.on("end", () => {
errorMessage = Buffer.concat(stderrChunks).toString();
});
htmlbeautifier.on("error", (err) =>
this.handleSpawnError(
reject,
`Couldn't run ${this.exe}: ${err.message}`,
err
)
);

htmlbeautifier.on("exit", (code) => {
if (code) {
vscode.window.showErrorMessage(
`Failed with exit code: ${code}. '${errorMessage}'`
);
reject(new Error(`Command failed with exit code ${code}`));
} else {
resolve(formattedResult);
}
const formattedResult = Buffer.concat(stdoutChunks).toString();
const finalResult = this.handleFinalNewline(input, formattedResult);
const errorMessage = Buffer.concat(stderrChunks).toString();
this.handleExit(code, finalResult, errorMessage, resolve, reject);
});

htmlbeautifier.stdin.write(input);
Expand All @@ -99,8 +88,64 @@ export default class HtmlBeautifier {
}

/**
* Returns the executable path for HTML Beautifier
* @returns {string} The executable path
* Handles errors during process spawning.
* @param reject The promise reject function.
* @param message The error message to log and show to the user.
* @param err Optional error object.
*/
private handleSpawnError(
reject: (reason?: any) => void,
message: string,
err?: Error
): void {
this.logChannel.warn(message);
vscode.window.showErrorMessage(message);
if (err) {
this.logChannel.warn(err.message);
}
reject(err || new Error(message));
}

/**
* Handles the process exit event and resolves or rejects the promise.
* @param code The process exit code.
* @param result The formatted result.
* @param errorMessage The error message, if any.
* @param resolve The promise resolve function.
* @param reject The promise reject function.
*/
private handleExit(
code: number | null,
result: string,
errorMessage: string,
resolve: (value: string | PromiseLike<string>) => void,
reject: (reason?: any) => void
): void {
if (code && code !== 0) {
const error = `Failed with exit code: ${code}. ${errorMessage}`;
this.logChannel.error(error);
vscode.window.showErrorMessage(error);
reject(new Error(error));
} else {
resolve(result);
}
}

/**
* Handles errors by logging and displaying a message to the user.
* @param error The error object or message.
* @param userMessage The message to display to the user.
*/
private handleError(error: any, userMessage: string): void {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
this.logChannel.error(errorMessage);
vscode.window.showErrorMessage(`${userMessage}: ${errorMessage}`);
}

/**
* Gets the executable path for HTML Beautifier based on the configuration.
* @returns The path to the executable.
*/
private get exe(): string {
const config = vscode.workspace.getConfiguration("vscode-erb-beautify");
Expand All @@ -112,26 +157,26 @@ export default class HtmlBeautifier {
}

/**
* Determines if the current platform is Windows (excluding WSL)
* @returns {boolean} True if the platform is Windows, false otherwise
* Checks if the current platform is Windows (excluding WSL).
* @returns True if the platform is Windows; false otherwise.
*/
private isWindows(): boolean {
return process.platform === "win32" && !isWsl;
}

/**
* Returns the command-line options for HTML Beautifier
* @returns {string[]} The command-line options
* Retrieves the command-line options for HTML Beautifier from the configuration.
* @returns An array of command-line options.
*/
private get cliOptions(): string[] {
const config = vscode.workspace.getConfiguration("vscode-erb-beautify");
const acc: string[] = [];
const options: string[] = [];

if (config.get("useBundler")) {
acc.push("exec", "htmlbeautifier");
options.push("exec", "htmlbeautifier");
}

return Object.keys(config).reduce(function (acc, key) {
return Object.keys(config).reduce((acc, key) => {
switch (key) {
case "indentBy":
acc.push("--indent-by", config[key]);
Expand All @@ -154,35 +199,31 @@ export default class HtmlBeautifier {
break;
}
return acc;
}, acc);
}, options);
}

/**
* Retrieves the custom environment variables from the configuration
* @returns {Record<string, string>} The custom environment variables
* Retrieves custom environment variables from the configuration.
* @returns A record of custom environment variables.
*/
private get customEnvVars(): Record<string, string> {
const config = vscode.workspace.getConfiguration("vscode-erb-beautify");
const customEnvVar = config.get("customEnvVar", {}) as Record<
string,
string
>;
return customEnvVar;
return config.get("customEnvVar", {}) as Record<string, string>;
}

/**
* Adjusts the final newline of the result string based on the VS Code configuration and the input string.
* @param {string} input - The original input string.
* @param {string} result - The result string to be processed.
* @returns {string} The processed result string.
* Adjusts the final newline of the result string based on VS Code configuration.
* @param input The original input string.
* @param result The formatted result string.
* @returns The adjusted result string.
*/
private handleFinalNewline(input: string, result: string): string {
// Get the 'insertFinalNewline' setting from VS Code configuration
const insertFinalNewline = vscode.workspace
.getConfiguration()
.get("files.insertFinalNewline");

// Check if the result string ends with a newline
// Determine if the result ends with a newline
const resultEndsWithNewline =
result.endsWith("\n") || result.endsWith("\r\n");

Expand All @@ -191,11 +232,8 @@ export default class HtmlBeautifier {
// Get the 'files.eol' setting from VS Code configuration
const eol = vscode.workspace.getConfiguration().get("files.eol");

// Set the newline character(s) based on the 'files.eol' setting and the platform
let newline = eol;
if (eol === "auto") {
newline = this.isWindows() ? "\r\n" : "\n";
}
// Determine newline character(s) based on the 'files.eol' setting and the platform
const newline = eol === "auto" ? (this.isWindows() ? "\r\n" : "\n") : eol;

// Append the newline to the result
result += newline;
Expand All @@ -208,11 +246,8 @@ export default class HtmlBeautifier {

// If the input and result use different newline character(s)
if (inputNewline !== resultNewline) {
// Remove the newline from the end of the result
result = result.slice(0, -resultNewline.length);

// Append the newline from the input to the result
result += inputNewline;
// Remove the newline from the end of the result and append the input's newline
result = result.slice(0, -resultNewline.length) + inputNewline;
}
}

Expand All @@ -221,20 +256,16 @@ export default class HtmlBeautifier {
}

/**
* Determines the type of newline used in the input string.
* @param {string} input - The input string.
* @returns {string} The newline character(s) used in the input string, or an empty string if the input does not end with a newline.
* Determines the newline character(s) used in the input string.
* @param input The input string.
* @returns The newline character(s) used, or an empty string if none.
*/
private getNewline(input: string): string {
// If the input ends with a Windows-style newline, return '\r\n'
if (input.endsWith("\r\n")) {
return "\r\n";
}
// If the input ends with a Unix-style newline, return '\n'
else if (input.endsWith("\n")) {
return "\n";
return "\r\n"; // Return Windows-style newline
} else if (input.endsWith("\n")) {
return "\n"; // Return Unix-style newline
}
// If the input does not end with a newline, return an empty string
return "";
return ""; // Return empty if no newline found
}
}