Skip to content

Commit

Permalink
chore: Refactor HtmlBeautifier class and improve error handling (#73)
Browse files Browse the repository at this point in the history
* chore: Refactor HtmlBeautifier class and improve error handling

* refactor: Update HtmlBeautifier class to handle error cases and improve code readability
  • Loading branch information
aliariff authored Aug 31, 2024
1 parent 216a63c commit 9ca0ecc
Showing 1 changed file with 131 additions and 100 deletions.
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
}
}

0 comments on commit 9ca0ecc

Please sign in to comment.