anyXML
# AnyXML Tools: Copilot Instructions
## Project overview
AnyXML Tools is a Visual Studio Code extension for formatting XML, converting XML to one line, and checking whether XML is well-formed. Keep changes consistent with the current implementation and user-facing behavior in `README.md`. XML Schema (XSD) validation and XML comparison are not implemented.
The extension targets VS Code `^1.90.0`. It is written in TypeScript and uses `fast-xml-parser` for in-memory XML syntax validation. PowerShell scripts handle formatting and validation of files over 50 MiB and perform formatting conversions for regular editor documents.
## Main files
- `src/extension.ts` implements activation, VS Code commands and menus, diagnostics, status bar state, editor formatting, large-file disk operations, and PowerShell process management.
- `formatXML.ps1` formats XML or converts it to one line. For large files it uses `XmlReader` and `XmlWriter` streaming.
- `validateXML.ps1` validates a file with `XmlReader`; it prohibits DTD processing and disables the XML resolver.
- `package.json` defines commands, menus, settings, runtime dependencies, and build scripts.
- `README.md` is the user guide. Keep implementation details out of it unless they help extension users.
- `.vscode/launch.json` and `.vscode/tasks.json` support local extension development.
## Current behavior and constraints
- The commands are `AnyXML: Format XML Document`, `AnyXML: Convert XML to One Line`, and `AnyXML: Validate XML`. They appear in the Command Palette and relevant editor and Explorer context menus.
- Regular editor documents are validated in memory. Report syntax failures as VS Code diagnostics and update the AnyXML status bar. Validation on change is debounced by 500 ms; `anyxml.validateOnChange` and `anyxml.validateOnSave` control automatic checks.
- **Format Document** for XML uses AnyXML's formatting provider. Formatting and one-line conversion must validate XML before changing editor contents. On failure, show an error and leave the document unchanged.
- VS Code cannot synchronize files larger than 50 MiB with extensions. Do not open such files to process them. Explorer context-menu commands operate on disk through PowerShell. Large-file validation reports its result in a message and the status bar, not editor diagnostics.
- Large-file format and one-line commands must stream from input to a separate sibling output file and leave the original untouched. Output names use `_formatted.xml` or `_oneline.xml`; if a name exists, append a numeric suffix.
- Formatting writes to a temporary output and moves it into place only after conversion succeeds. Preserve this failure behavior so malformed input cannot truncate or replace the original or final output.
- Use `anyxml.powershellExecutable` when configured. Otherwise use `pwsh` on macOS/Linux and `powershell.exe` on Windows. Spawn PowerShell with an argument array; do not build a shell command string.
- PowerShell validation uses `ANYXML_VALID` on success and `ANYXML_ERROR<TAB>line<TAB>column<TAB>message` for XML syntax errors. Keep this protocol synchronized with `src/extension.ts`.
- Keep `@vscode/vsce` in development dependencies. Only packages needed at extension runtime belong in `dependencies`.
- Keep XML samples, generated `.vsix` files, build output, and development-only files out of the Marketplace package. `.vscodeignore` controls package contents.
## Build and packaging
- `npm run compile` compiles TypeScript into `out/`.
- `npx --yes @vscode/vsce package` runs the prepublish compile and creates the local VSIX.
- There is currently no automated test script in `package.json`. Do not claim tests passed unless tests have been added and run.
- Keep platform-specific behavior compatible with Windows, macOS, and Linux. PowerShell may be installed under a custom path, so preserve the setting for its executable.
## Full source and configuration snapshot
The sections below contain verbatim snapshots of the current project files. Use these code blocks as the implementation context when the repository files themselves are unavailable. They were generated from the workspace; update this snapshot whenever one of these source or configuration files changes.
### `package.json`
````json
{
"name": "anyxml-tools",
"displayName": "AnyXML Tools",
"description": "Format and validate XML files in Visual Studio Code.",
"version": "0.0.9",
"publisher": "SanyamKumar",
"repository": {
"type": "git",
"url": "https://github.com/snymkmr/anyXML.git"
},
"icon": "icon.png",
"engines": {
"vscode": "^1.90.0"
},
"categories": [
"Formatters",
"Linters"
],
"activationEvents": [
"onLanguage:xml",
"onCommand:anyxml.formatXml",
"onCommand:anyxml.oneLineXml",
"onCommand:anyxml.validateXml"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "anyxml.formatXml",
"title": "AnyXML: Format XML Document"
},
{
"command": "anyxml.oneLineXml",
"title": "AnyXML: Convert XML to One Line"
},
{
"command": "anyxml.validateXml",
"title": "AnyXML: Validate XML"
}
],
"menus": {
"editor/context": [
{
"command": "anyxml.formatXml",
"when": "editorLangId == xml || resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "1_modification@1"
},
{
"command": "anyxml.validateXml",
"when": "editorLangId == xml || resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "1_modification@3"
},
{
"command": "anyxml.oneLineXml",
"when": "editorLangId == xml || resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "1_modification@2"
}
],
"explorer/context": [
{
"command": "anyxml.formatXml",
"when": "resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "2_workspace@1"
},
{
"command": "anyxml.validateXml",
"when": "resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "2_workspace@3"
},
{
"command": "anyxml.oneLineXml",
"when": "resourceExtname == .xml || resourceExtname == .xsd || resourceExtname == .xsl || resourceExtname == .xslt",
"group": "2_workspace@2"
}
]
},
"configuration": {
"title": "AnyXML Tools",
"properties": {
"anyxml.powershellExecutable": {
"type": "string",
"default": "",
"description": "PowerShell executable used for formatting and validation of files over 50 MiB. Leave empty to use pwsh (or powershell.exe on Windows)."
},
"anyxml.validateOnChange": {
"type": "boolean",
"default": true,
"description": "Validate XML after edits. Validation is debounced; disable this for very large documents if it slows editing."
},
"anyxml.validateOnSave": {
"type": "boolean",
"default": true,
"description": "Validate XML when a document is saved."
}
}
}
},
"scripts": {
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"vscode:prepublish": "npm run compile"
},
"dependencies": {
"fast-xml-parser": "^5.2.5"
},
"devDependencies": {
"@types/node": "^20.11.24",
"@types/vscode": "^1.90.0",
"@vscode/vsce": "^4.1.0",
"typescript": "^5.4.5"
}
}
````
### `tsconfig.json`
````json
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"lib": ["ES2022"],
"outDir": "out",
"rootDir": "src",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
````
### `src/extension.ts`
````typescript
import * as vscode from "vscode";
import { XMLValidator } from "fast-xml-parser";
import { spawn } from "node:child_process";
import { promises as fs } from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const diagnosticCollectionName = "anyxml";
const maximumSynchronizedFileSize = 50 * 1024 * 1024;
interface ValidationState {
version: number;
diagnostic?: vscode.Diagnostic;
}
interface LargeFileValidationState {
uri: vscode.Uri;
isValid: boolean;
line?: number;
column?: number;
message?: string;
}
export function activate(context: vscode.ExtensionContext): void {
const diagnostics = vscode.languages.createDiagnosticCollection(diagnosticCollectionName);
const validationTimers = new Map<string, NodeJS.Timeout>();
const validationStates = new Map<string, ValidationState>();
const pendingValidations = new Set<string>();
let largeFileValidationState: LargeFileValidationState | undefined;
let pendingLargeFileValidation: vscode.Uri | undefined;
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusBar.name = "AnyXML XML Validation";
const updateStatusBar = (): void => {
const document = vscode.window.activeTextEditor?.document;
if (!isXmlDocument(document)) {
const largeFile = pendingLargeFileValidation ? undefined : largeFileValidationState;
if (pendingLargeFileValidation) {
statusBar.show();
statusBar.command = {
command: "anyxml.validateXml",
title: "Validate XML",
arguments: [pendingLargeFileValidation]
};
statusBar.text = `$(sync~spin) XML: Checking ${path.basename(pendingLargeFileValidation.fsPath)}`;
statusBar.color = undefined;
statusBar.tooltip = `Checking ${pendingLargeFileValidation.fsPath}…`;
return;
}
if (!largeFile) {
statusBar.hide();
return;
}
statusBar.show();
statusBar.command = {
command: "anyxml.validateXml",
title: "Validate XML",
arguments: [largeFile.uri]
};
const fileName = path.basename(largeFile.uri.fsPath);
if (largeFile.isValid) {
statusBar.text = `$(check) XML: Valid (${fileName})`;
statusBar.color = new vscode.ThemeColor("testing.iconPassed");
statusBar.tooltip = `${fileName} is well-formed. Click to validate again.`;
} else {
statusBar.text = `$(error) XML: Error (${fileName})`;
statusBar.color = new vscode.ThemeColor("testing.iconFailed");
statusBar.tooltip = `${fileName}, line ${largeFile.line}, column ${largeFile.column}: ${largeFile.message}\nClick to validate again.`;
}
return;
}
statusBar.show();
statusBar.command = {
command: "anyxml.validateXml",
title: "Validate XML",
arguments: [document.uri]
};
const key = document.uri.toString();
if (pendingValidations.has(key)) {
statusBar.text = "$(sync~spin) XML: Checking";
statusBar.color = undefined;
statusBar.tooltip = "Checking XML syntax…";
return;
}
const state = validationStates.get(key);
if (!state) {
statusBar.text = "$(question) XML: Not checked";
statusBar.color = undefined;
statusBar.tooltip = "Click to validate this XML document.";
return;
}
if (state.version !== document.version) {
statusBar.text = "$(warning) XML: Needs validation";
statusBar.color = undefined;
statusBar.tooltip = "This document has changed since its last validation. Click to validate it.";
return;
}
if (!state.diagnostic) {
statusBar.text = "$(check) XML: Valid";
statusBar.color = new vscode.ThemeColor("testing.iconPassed");
statusBar.tooltip = "XML is well-formed. Click to validate again.";
return;
}
const line = state.diagnostic.range.start.line + 1;
const column = state.diagnostic.range.start.character + 1;
statusBar.text = `$(error) XML: syntax error at ${line}:${column}`;
statusBar.color = new vscode.ThemeColor("testing.iconFailed");
statusBar.tooltip = `Line ${line}, column ${column}: ${state.diagnostic.message}\nClick to validate again.`;
};
const validateNow = (document: vscode.TextDocument): void => {
const key = document.uri.toString();
const timer = validationTimers.get(key);
if (timer) {
clearTimeout(timer);
validationTimers.delete(key);
}
pendingValidations.delete(key);
validateDocument(document, diagnostics, validationStates);
updateStatusBar();
};
const scheduleValidation = (document: vscode.TextDocument): void => {
if (!isXmlDocument(document)) {
return;
}
const key = document.uri.toString();
const existingTimer = validationTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
pendingValidations.add(key);
updateStatusBar();
validationTimers.set(
key,
setTimeout(() => {
validationTimers.delete(key);
pendingValidations.delete(key);
validateDocument(document, diagnostics, validationStates);
updateStatusBar();
}, 500)
);
};
context.subscriptions.push(diagnostics);
context.subscriptions.push(statusBar);
context.subscriptions.push(
vscode.languages.registerDocumentFormattingEditProvider("xml", {
provideDocumentFormattingEdits: async (document) => {
try {
const version = document.version;
const formatted = await formatXml(document.getText(), context);
if (document.version !== version) {
throw new Error("The XML document changed while formatting was running. Run Format Document again.");
}
return [replaceWholeDocument(document, formatted)];
} catch (error) {
void vscode.window.showErrorMessage(errorMessage(error));
return [];
}
}
})
);
const transformXmlCommand = async (resource: vscode.Uri | undefined, oneLine: boolean): Promise<void> => {
if (resource?.scheme === "file") {
try {
const stat = await fs.stat(resource.fsPath);
if (stat.size > maximumSynchronizedFileSize) {
pendingLargeFileValidation = resource;
largeFileValidationState = undefined;
updateStatusBar();
try {
const validation = await validateLargeXmlFile(
context.asAbsolutePath("validateXML.ps1"),
resource
);
largeFileValidationState = validation;
if (!validation.isValid) {
void vscode.window.showErrorMessage(
`Cannot convert ${path.basename(resource.fsPath)}: XML syntax error at line ${validation.line}, column ${validation.column}: ${validation.message}`
);
return;
}
const outputPath = await getTransformOutputPath(resource.fsPath, oneLine);
await runPowerShell(context.asAbsolutePath("formatXML.ps1"), resource.fsPath, outputPath, oneLine);
const action = oneLine ? "One-line copy" : "Formatted copy";
void vscode.window.showInformationMessage(`${action} saved to ${outputPath}`);
} finally {
pendingLargeFileValidation = undefined;
updateStatusBar();
}
return;
}
} catch (error) {
void vscode.window.showErrorMessage(errorMessage(error));
return;
}
}
let document: vscode.TextDocument | undefined;
try {
document = await resolveXmlDocument(resource);
} catch (error) {
void vscode.window.showErrorMessage(errorMessage(error));
return;
}
if (!document) {
void vscode.window.showWarningMessage("Select or open an XML file before converting it.");
return;
}
try {
const editor = vscode.window.visibleTextEditors.find(
(candidate) => candidate.document.uri.toString() === document!.uri.toString()
) ?? await vscode.window.showTextDocument(document);
const version = document.version;
const formatted = await formatXml(document.getText(), context, oneLine);
if (document.version !== version) {
throw new Error("The XML document changed while conversion was running. Run the command again.");
}
const applied = await editor.edit((edit) => edit.replace(fullDocumentRange(document!), formatted));
if (!applied) {
throw new Error("VS Code could not apply the formatted XML edits.");
}
} catch (error) {
void vscode.window.showErrorMessage(errorMessage(error));
}
};
context.subscriptions.push(
vscode.commands.registerCommand("anyxml.formatXml", (resource?: vscode.Uri) =>
transformXmlCommand(resource, false)
),
vscode.commands.registerCommand("anyxml.oneLineXml", (resource?: vscode.Uri) =>
transformXmlCommand(resource, true)
)
);
context.subscriptions.push(
vscode.commands.registerCommand("anyxml.validateXml", async (resource?: vscode.Uri) => {
if (resource?.scheme === "file") {
try {
const stat = await fs.stat(resource.fsPath);
if (stat.size > maximumSynchronizedFileSize) {
pendingLargeFileValidation = resource;
largeFileValidationState = undefined;
updateStatusBar();
try {
largeFileValidationState = await validateLargeXmlFile(
context.asAbsolutePath("validateXML.ps1"),
resource
);
} finally {
pendingLargeFileValidation = undefined;
updateStatusBar();
}
if (largeFileValidationState.isValid) {
void vscode.window.showInformationMessage(`${path.basename(resource.fsPath)} is well-formed.`);
} else {
void vscode.window.showWarningMessage(
`${path.basename(resource.fsPath)} has an XML syntax error at line ${largeFileValidationState.line}, column ${largeFileValidationState.column}: ${largeFileValidationState.message}`
);
}
return;
}
} catch (error) {
pendingLargeFileValidation = undefined;
updateStatusBar();
void vscode.window.showErrorMessage(errorMessage(error));
return;
}
}
let document: vscode.TextDocument | undefined;
try {
document = await resolveXmlDocument(resource);
} catch (error) {
void vscode.window.showErrorMessage(errorMessage(error));
return;
}
if (!document) {
void vscode.window.showWarningMessage("Select or open an XML file before validating.");
return;
}
validateNow(document);
const state = validationStates.get(document.uri.toString());
if (!state?.diagnostic) {
void vscode.window.showInformationMessage("XML is well-formed.");
} else {
void vscode.window.showWarningMessage("XML validation found a syntax error. See the Problems panel and status bar for its location.");
}
})
);
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((event) => {
if (vscode.workspace.getConfiguration("anyxml").get<boolean>("validateOnChange", false)) {
scheduleValidation(event.document);
} else {
updateStatusBar();
}
}),
vscode.workspace.onDidSaveTextDocument((document) => {
if (vscode.workspace.getConfiguration("anyxml").get<boolean>("validateOnSave", true)) {
scheduleValidation(document);
} else {
updateStatusBar();
}
}),
vscode.workspace.onDidCloseTextDocument((document) => {
const key = document.uri.toString();
const timer = validationTimers.get(key);
if (timer) {
clearTimeout(timer);
validationTimers.delete(key);
}
diagnostics.delete(document.uri);
validationStates.delete(key);
pendingValidations.delete(key);
updateStatusBar();
}),
vscode.window.onDidChangeActiveTextEditor((editor) => {
const document = editor?.document;
if (isXmlDocument(document) && !validationStates.has(document.uri.toString())) {
scheduleValidation(document);
} else {
updateStatusBar();
}
})
);
context.subscriptions.push({
dispose: () => {
for (const timer of validationTimers.values()) {
clearTimeout(timer);
}
validationTimers.clear();
}
});
const activeDocument = vscode.window.activeTextEditor?.document;
if (isXmlDocument(activeDocument) && !validationStates.has(activeDocument.uri.toString())) {
scheduleValidation(activeDocument);
} else {
updateStatusBar();
}
}
export function deactivate(): void {
// VS Code disposes subscriptions registered during activation.
}
async function formatXml(
xml: string,
context: vscode.ExtensionContext,
oneLine = false
): Promise<string> {
const validation = XMLValidator.validate(xml);
if (validation !== true) {
throw new Error(
`Cannot format malformed XML. Syntax error at line ${validation.err.line}, column ${validation.err.col}: ${validation.err.msg}`
);
}
const temporaryDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "anyxml-"));
const inputPath = path.join(temporaryDirectory, "input.xml");
const outputPath = path.join(temporaryDirectory, oneLine ? "oneline.xml" : "formatted.xml");
const scriptPath = context.asAbsolutePath("formatXML.ps1");
try {
await fs.writeFile(inputPath, xml, "utf8");
await runPowerShell(scriptPath, inputPath, outputPath, oneLine);
return (await fs.readFile(outputPath, "utf8")).replace(/^\uFEFF/, "");
} finally {
await fs.rm(temporaryDirectory, { recursive: true, force: true });
}
}
function runPowerShell(
scriptPath: string,
inputPath: string,
outputPath: string,
oneLine = false
): Promise<void> {
const executable = getPowerShellExecutable();
const args = [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
scriptPath,
"-InputFile",
inputPath,
"-OutputFile",
outputPath
];
if (oneLine) {
args.push("-OneLine");
}
return new Promise((resolve, reject) => {
let stderr = "";
const child = spawn(executable, args, { windowsHide: true });
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk: string) => {
stderr += chunk;
});
child.once("error", (error) => reject(powerShellError(error)));
child.once("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(stderr.trim() || `PowerShell formatter exited with code ${code ?? "unknown"}.`));
}
});
});
}
function validateLargeXmlFile(scriptPath: string, uri: vscode.Uri): Promise<LargeFileValidationState> {
const executable = getPowerShellExecutable();
const args = [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
scriptPath,
"-InputFile",
uri.fsPath
];
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
const child = spawn(executable, args, { windowsHide: true });
child.stdout?.setEncoding("utf8");
child.stdout?.on("data", (chunk: string) => {
stdout += chunk;
});
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk: string) => {
stderr += chunk;
});
child.once("error", (error) => reject(powerShellError(error)));
child.once("close", (code) => {
if (code === 0 && stdout.includes("ANYXML_VALID")) {
resolve({ uri, isValid: true });
return;
}
const match = stdout.match(/ANYXML_ERROR\t(\d+)\t(\d+)\t([^\r\n]*)/);
if (code === 2 && match) {
resolve({
uri,
isValid: false,
line: Number(match[1]),
column: Number(match[2]),
message: match[3]
});
return;
}
reject(new Error(stderr.trim() || stdout.trim() || `PowerShell validator exited with code ${code ?? "unknown"}.`));
});
});
}
function getPowerShellExecutable(): string {
const configuredExecutable = vscode.workspace
.getConfiguration("anyxml")
.get<string>("powershellExecutable", "")
.trim();
return configuredExecutable || (process.platform === "win32" ? "powershell.exe" : "pwsh");
}
function powerShellError(error: unknown): Error {
const executable = getPowerShellExecutable();
const detail = error instanceof Error ? error.message : String(error);
return new Error(`Could not start PowerShell (${executable}). Install PowerShell or set anyxml.powershellExecutable. ${detail}`);
}
function validateDocument(
document: vscode.TextDocument,
diagnostics: vscode.DiagnosticCollection,
validationStates: Map<string, ValidationState>
): void {
if (!isXmlDocument(document)) {
return;
}
const result = XMLValidator.validate(document.getText());
const key = document.uri.toString();
if (result === true) {
diagnostics.delete(document.uri);
validationStates.set(key, { version: document.version });
return;
}
const line = Math.max(0, result.err.line - 1);
const character = Math.max(0, result.err.col - 1);
const position = new vscode.Position(line, character);
const diagnostic = new vscode.Diagnostic(
new vscode.Range(position, position),
result.err.msg,
vscode.DiagnosticSeverity.Error
);
diagnostic.source = "AnyXML";
diagnostic.code = result.err.code;
diagnostics.set(document.uri, [diagnostic]);
validationStates.set(key, { version: document.version, diagnostic });
}
async function resolveXmlDocument(resource?: vscode.Uri): Promise<vscode.TextDocument | undefined> {
const document = resource
? await vscode.workspace.openTextDocument(resource)
: vscode.window.activeTextEditor?.document;
return isXmlDocument(document) ? document : undefined;
}
async function getTransformOutputPath(inputPath: string, oneLine: boolean): Promise<string> {
const extension = path.extname(inputPath) || ".xml";
const stem = path.basename(inputPath, path.extname(inputPath));
const directory = path.dirname(inputPath);
for (let suffix = 0; suffix < 1000; suffix++) {
const markerBase = oneLine ? "_oneline" : "_formatted";
const marker = suffix === 0 ? markerBase : `${markerBase}_${suffix}`;
const candidate = path.join(directory, `${stem}${marker}${extension}`);
try {
await fs.access(candidate);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
return candidate;
}
throw error;
}
}
throw new Error("Could not choose a name for the formatted output file.");
}
function isXmlDocument(document: vscode.TextDocument | undefined): document is vscode.TextDocument {
if (!document) {
return false;
}
const extension = path.extname(document.uri.fsPath).toLowerCase();
return document.languageId === "xml" || [".xml", ".xsd", ".xsl", ".xslt"].includes(extension);
}
function replaceWholeDocument(document: vscode.TextDocument, text: string): vscode.TextEdit {
return vscode.TextEdit.replace(fullDocumentRange(document), text);
}
function fullDocumentRange(document: vscode.TextDocument): vscode.Range {
return new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length));
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
````
### `formatXML.ps1`
````powershell
#Requires -Version 3.0
param(
[string]$InputFile = "",
[string]$OutputFile = "",
[switch]$Streaming = $false,
[switch]$OneLine = $false
)
function Format-XMLContent {
param(
[string]$xmlString,
[switch]$OneLine
)
$xml = [xml]$xmlString
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Indent = -not $OneLine
$settings.IndentChars = " "
$settings.NewLineChars = "`n"
$tempFile = [System.IO.Path]::GetTempFileName()
$writer = $null
try {
$writer = [System.Xml.XmlWriter]::Create($tempFile, $settings)
$xml.WriteContentTo($writer)
$writer.Flush()
$writer.Close()
$writer = $null
return [System.IO.File]::ReadAllText($tempFile, [System.Text.Encoding]::UTF8)
}
finally {
if ($null -ne $writer) {
$writer.Dispose()
}
if (Test-Path -LiteralPath $tempFile) {
Remove-Item -LiteralPath $tempFile -Force
}
}
}
function Format-XMLContentStreaming {
param(
[string]$inputFilePath,
[string]$outputFilePath,
[switch]$OneLine
)
$readerSettings = New-Object System.Xml.XmlReaderSettings
$readerSettings.ConformanceLevel = "Document"
$readerSettings.IgnoreWhitespace = [bool]$OneLine
$writerSettings = New-Object System.Xml.XmlWriterSettings
$writerSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Document
$writerSettings.Indent = -not $OneLine
$writerSettings.IndentChars = " "
$writerSettings.NewLineChars = "`n"
$writerSettings.Encoding = [System.Text.UTF8Encoding]::new($false)
$reader = $null
$writer = $null
try {
$reader = [System.Xml.XmlReader]::Create($inputFilePath, $readerSettings)
$writer = [System.Xml.XmlWriter]::Create($outputFilePath, $writerSettings)
if ($reader.Read()) {
do {
# WriteNode advances the reader to the next sibling, so do not call Read() again here.
$writer.WriteNode($reader, $false)
} while (-not $reader.EOF)
}
}
finally {
if ($null -ne $reader) {
$reader.Dispose()
}
if ($null -ne $writer) {
$writer.Dispose()
}
}
}
# Main logic
if ([string]::IsNullOrWhiteSpace($InputFile)) {
Write-Host "XML Formatter" -ForegroundColor Cyan
Write-Host "=============" -ForegroundColor Cyan
Write-Host ""
$InputFile = Read-Host "Enter path to XML file"
}
try {
if (-not (Test-Path -LiteralPath $InputFile -PathType Leaf)) {
throw "File not found: $InputFile"
}
$inputPath = [System.IO.Path]::GetFullPath($InputFile)
if ([string]::IsNullOrWhiteSpace($OutputFile)) {
$suffix = if ($OneLine) { "_oneline.xml" } else { "_formatted.xml" }
$OutputFile = [System.IO.Path]::GetFileNameWithoutExtension($inputPath) + $suffix
}
$outputPath = [System.IO.Path]::GetFullPath($OutputFile)
if ([string]::Equals($inputPath, $outputPath, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "The output file must be different from the input file. The original was not changed."
}
$outputDirectory = [System.IO.Path]::GetDirectoryName($outputPath)
if (-not [System.IO.Directory]::Exists($outputDirectory)) {
throw "Output directory does not exist: $outputDirectory"
}
$temporaryOutputPath = [System.IO.Path]::Combine(
$outputDirectory,
"." + [System.IO.Path]::GetFileName($outputPath) + "." + [System.Guid]::NewGuid().ToString("N") + ".tmp"
)
$fileSize = (Get-Item -LiteralPath $inputPath).Length / 1MB
Write-Host "File size: $([Math]::Round($fileSize, 2)) MB" -ForegroundColor Cyan
# Auto-select streaming for large files
if ($fileSize -gt 50) {
Write-Host "Large file detected. Using streaming mode for better performance..." -ForegroundColor Yellow
$Streaming = $true
}
if ($Streaming) {
$action = if ($OneLine) { "compacting" } else { "formatting" }
Write-Host "Reading and $action XML (streaming mode)..." -ForegroundColor Yellow
Format-XMLContentStreaming -inputFilePath $inputPath -outputFilePath $temporaryOutputPath -OneLine:$OneLine
} else {
Write-Host "Reading XML file..." -ForegroundColor Yellow
$xmlContent = Get-Content -LiteralPath $inputPath -Raw
$action = if ($OneLine) { "Compacting" } else { "Formatting" }
Write-Host "$action XML..." -ForegroundColor Yellow
$formattedXml = Format-XMLContent -xmlString $xmlContent -OneLine:$OneLine
[System.IO.File]::WriteAllText($temporaryOutputPath, $formattedXml, [System.Text.Encoding]::UTF8)
}
Move-Item -LiteralPath $temporaryOutputPath -Destination $outputPath -Force
Write-Host "Done! Formatted XML saved to: $outputPath" -ForegroundColor Green
exit 0
}
catch {
if ($temporaryOutputPath -and (Test-Path -LiteralPath $temporaryOutputPath)) {
Remove-Item -LiteralPath $temporaryOutputPath -Force -ErrorAction SilentlyContinue
}
[Console]::Error.WriteLine($_.Exception.Message)
exit 1
}
````
### `validateXML.ps1`
````powershell
#Requires -Version 3.0
param(
[Parameter(Mandatory = $true)]
[string]$InputFile
)
$reader = $null
$exitCode = 0
try {
$settings = New-Object System.Xml.XmlReaderSettings
$settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
$settings.XmlResolver = $null
$reader = [System.Xml.XmlReader]::Create($InputFile, $settings)
while ($reader.Read()) { }
[Console]::Out.WriteLine("ANYXML_VALID")
}
catch [System.Xml.XmlException] {
$exception = $_.Exception
$message = $exception.Message -replace "[\r\n\t]+", " "
[Console]::Out.WriteLine("ANYXML_ERROR`t$($exception.LineNumber)`t$($exception.LinePosition)`t$message")
$exitCode = 2
}
catch {
[Console]::Error.WriteLine($_.Exception.Message)
$exitCode = 1
}
finally {
if ($null -ne $reader) {
$reader.Dispose()
}
}
exit $exitCode
````
### `.vscode/launch.json`
````json
{
"version": "0.2.0",
"configurations": [
{
"name": "Run AnyXML Tools",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
}
]
}
````
### `.vscode/tasks.json`
````json
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"problemMatcher": "$tsc",
"label": "npm: compile",
"detail": "Compile the VS Code extension TypeScript source"
}
]
}
````
### `.gitignore`
````gitignore
node_modules/
out/
*.vsix
.DS_Store
*.xml
````
### `.vscodeignore`
````gitignore
.vscode/**
.github/**
.gitignore
.git/**
src/**
tsconfig.json
**/*.map
anyXML.png
*.xml
````
### `README.md`
````markdown
# AnyXML Tools
Format and check XML documents in Visual Studio Code.
## Requirements
- **PowerShell** is required for formatting and validation of files over 50 MiB. The extension looks for `pwsh` on macOS and Linux, or `powershell.exe` on Windows. If it is installed somewhere else, set `anyxml.powershellExecutable` to its executable path in VS Code Settings.
## Use AnyXML
For regular XML files, either right-click in the editor or right-click the file in Explorer. Choose:
- **AnyXML: Format XML Document** to format the document. Large file conversions use streaming mode.
- **AnyXML: Convert XML to One Line** to remove formatting whitespace and compact the document. For a regular open document, this replaces its contents; use Undo to reverse it.
- **AnyXML: Validate XML** to check that the document is well-formed. The first syntax error appears in the Problems panel.
### Large files
- VS Code cannot synchronize files over 50 MiB with extensions, so keep these files closed in the editor.
- Right-click the file in Explorer to run a command directly from disk:
- **AnyXML: Format XML Document** saves a separate `*_formatted.xml` copy.
- **AnyXML: Convert XML to One Line** saves a separate `*_oneline.xml` copy.
- **AnyXML: Validate XML** checks the file and reports the first syntax error.
- If an output name already exists, AnyXML adds a number. Formatting and one-line conversion leave the original unchanged.
Validation shows a green or red result in the status bar and a message with the error location. VS Code cannot show editor diagnostics or live validation for these files because they cannot be opened in the editor.
You can also run these commands from the Command Palette (`Cmd+Shift+P` on macOS, `Ctrl+Shift+P` on Windows/Linux). **Format Document** also uses AnyXML for XML files.
## Validation status
The status bar shows green when the active XML file is valid and red when it has a syntax error. Hover over the status to see the error location and message; click it to validate again. Validation runs after edits (debounced by 500 ms) and when you save.
To check another file, click the status bar or run **AnyXML: Validate XML** from the Command Palette or a context menu.
Validation checks XML syntax and well-formedness. It does not validate an XML Schema (XSD).
````
### `LICENSE`
````text
MIT License
Copyright (c) 2026 Sañyam Kumar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
````
About the author
I am an Engineer in love with software and writing code. I am super passionate about the reach of technology, and when it comes to conquering new frontiers, I want to be there in the forefront.
Comments
Join the discussionNo comments yet. Be the first to comment.



