🚧 Modify toml,powershell prettier plugin(beta)
This commit is contained in:
65
frontend/package-lock.json
generated
65
frontend/package-lock.json
generated
@@ -42,6 +42,8 @@
|
|||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@prettier/plugin-xml": "^3.4.2",
|
"@prettier/plugin-xml": "^3.4.2",
|
||||||
"@reteps/dockerfmt": "^0.3.6",
|
"@reteps/dockerfmt": "^0.3.6",
|
||||||
|
"@toml-tools/lexer": "^1.0.0",
|
||||||
|
"@toml-tools/parser": "^1.0.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"colors-named": "^1.0.2",
|
"colors-named": "^1.0.2",
|
||||||
@@ -52,14 +54,12 @@
|
|||||||
"java-parser": "^3.0.1",
|
"java-parser": "^3.0.1",
|
||||||
"jinx-rust": "^0.1.6",
|
"jinx-rust": "^0.1.6",
|
||||||
"jsox": "^1.2.123",
|
"jsox": "^1.2.123",
|
||||||
"lezer": "^0.13.5",
|
|
||||||
"linguist-languages": "^9.0.0",
|
"linguist-languages": "^9.0.0",
|
||||||
"node-sql-parser": "^5.3.12",
|
"node-sql-parser": "^5.3.12",
|
||||||
"php-parser": "^3.2.5",
|
"php-parser": "^3.2.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-toml": "^2.0.6",
|
|
||||||
"remarkable": "^2.0.1",
|
"remarkable": "^2.0.1",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.92.1",
|
||||||
"sh-syntax": "^0.5.8",
|
"sh-syntax": "^0.5.8",
|
||||||
@@ -2236,19 +2236,23 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@taplo/core": {
|
"node_modules/@toml-tools/lexer": {
|
||||||
"version": "0.2.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@taplo/core/-/core-0.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@toml-tools/lexer/-/lexer-1.0.0.tgz",
|
||||||
"integrity": "sha512-r8bl54Zj1In3QLkiW/ex694bVzpPJ9EhwqT9xkcUVODnVUGirdB1JTsmiIv0o1uwqZiwhi8xNnTOQBRQCpizrQ==",
|
"integrity": "sha512-rVoOC9FibF2CICwCBWQnYcjAEOmLCJExer178K2AsY0Nk9FjJNVoVJuR5UAtuq42BZOajvH+ainf6Gj2GpCnXQ==",
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@taplo/lib": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@taplo/lib/-/lib-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-+xIqpQXJco3T+VGaTTwmhxLa51qpkQxCjRwezjFZgr+l21ExlywJFcDfTrNmL6lG6tqb0h8GyJKO3UPGPtSCWg==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@taplo/core": "^0.2.0"
|
"chevrotain": "^11.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@toml-tools/parser": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@toml-tools/parser/-/parser-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-j8cd3A3ccLHppGoWI69urbiVJslrpwI6sZ61ySDUPxM/FTkQWRx/JkkF8aipnl0Ds0feWXyjyvmWzn70mIohYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@toml-tools/lexer": "^1.0.0",
|
||||||
|
"chevrotain": "^11.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
@@ -5127,16 +5131,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lezer": {
|
|
||||||
"version": "0.13.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/lezer/-/lezer-0.13.5.tgz",
|
|
||||||
"integrity": "sha512-cAiMQZGUo2BD8mpcz7Nv1TlKzWP7YIdIRrX41CiP5bk5t4GHxskOxWUx2iAOuHlz8dO+ivbuXr0J1bfHsWD+lQ==",
|
|
||||||
"deprecated": "This package has been replaced by @lezer/lr",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"lezer-tree": "^0.13.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lezer-elixir": {
|
"node_modules/lezer-elixir": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/lezer-elixir/-/lezer-elixir-1.1.2.tgz",
|
||||||
@@ -5146,13 +5140,6 @@
|
|||||||
"@lezer/lr": "^1.3.0"
|
"@lezer/lr": "^1.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lezer-tree": {
|
|
||||||
"version": "0.13.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/lezer-tree/-/lezer-tree-0.13.2.tgz",
|
|
||||||
"integrity": "sha512-15ZxW8TxVNAOkHIo43Iouv4zbSkQQ5chQHBpwXcD2bBFz46RB4jYLEEww5l1V0xyIx9U2clSyyrLes+hAUFrGQ==",
|
|
||||||
"deprecated": "This package has been replaced by @lezer/common",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/linguist-languages": {
|
"node_modules/linguist-languages": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/linguist-languages/-/linguist-languages-9.0.0.tgz",
|
||||||
@@ -6004,24 +5991,6 @@
|
|||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier-plugin-toml": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/prettier-plugin-toml/-/prettier-plugin-toml-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-12N/wBuHa9jd/KVy9pRP20NMKxQfQLMseQCt66lIbLaPLItvGUcSIryE1eZZMJ7loSws6Ig3M2Elc2EreNh76w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@taplo/lib": "^0.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/unts"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"prettier": "^3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process": {
|
"node_modules/process": {
|
||||||
"version": "0.11.10",
|
"version": "0.11.10",
|
||||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@prettier/plugin-xml": "^3.4.2",
|
"@prettier/plugin-xml": "^3.4.2",
|
||||||
"@reteps/dockerfmt": "^0.3.6",
|
"@reteps/dockerfmt": "^0.3.6",
|
||||||
|
"@toml-tools/lexer": "^1.0.0",
|
||||||
|
"@toml-tools/parser": "^1.0.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"codemirror-lang-elixir": "^4.0.0",
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"colors-named": "^1.0.2",
|
"colors-named": "^1.0.2",
|
||||||
@@ -56,14 +58,12 @@
|
|||||||
"java-parser": "^3.0.1",
|
"java-parser": "^3.0.1",
|
||||||
"jinx-rust": "^0.1.6",
|
"jinx-rust": "^0.1.6",
|
||||||
"jsox": "^1.2.123",
|
"jsox": "^1.2.123",
|
||||||
"lezer": "^0.13.5",
|
|
||||||
"linguist-languages": "^9.0.0",
|
"linguist-languages": "^9.0.0",
|
||||||
"node-sql-parser": "^5.3.12",
|
"node-sql-parser": "^5.3.12",
|
||||||
"php-parser": "^3.2.5",
|
"php-parser": "^3.2.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.5.0",
|
"pinia-plugin-persistedstate": "^4.5.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-toml": "^2.0.6",
|
|
||||||
"remarkable": "^2.0.1",
|
"remarkable": "^2.0.1",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.92.1",
|
||||||
"sh-syntax": "^0.5.8",
|
"sh-syntax": "^0.5.8",
|
||||||
|
|||||||
Binary file not shown.
@@ -79,22 +79,32 @@ const initGoRuntimeNode = async () => {
|
|||||||
|
|
||||||
// Browser Go runtime initialization
|
// Browser Go runtime initialization
|
||||||
const initGoRuntimeBrowser = async () => {
|
const initGoRuntimeBrowser = async () => {
|
||||||
if (globalThis.Go) return;
|
// 总是重新初始化,因为可能存在版本不兼容问题
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load wasm_exec.js dynamically in browser
|
// 移除旧的 Go 运行时
|
||||||
|
delete globalThis.Go;
|
||||||
|
|
||||||
|
// 动态导入本地的 wasm_exec.js 内容
|
||||||
|
const wasmExecResponse = await fetch('/wasm_exec.js');
|
||||||
|
if (!wasmExecResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch wasm_exec.js: ${wasmExecResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasmExecCode = await wasmExecResponse.text();
|
||||||
|
|
||||||
|
// 在全局作用域中执行 wasm_exec.js 代码
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = '/wasm_exec.js';
|
script.textContent = wasmExecCode;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
// 等待一小段时间确保脚本执行完成
|
||||||
script.onload = resolve;
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
script.onerror = () => reject(new Error('Failed to load wasm_exec.js'));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!globalThis.Go) {
|
if (!globalThis.Go) {
|
||||||
throw new Error('Go WASM runtime not available after loading wasm_exec.js');
|
throw new Error('Go WASM runtime not available after executing wasm_exec.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Go runtime initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Browser Go runtime initialization failed:', error);
|
console.error('Browser Go runtime initialization failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -108,37 +118,70 @@ const initialize = async () => {
|
|||||||
initializePromise = (async () => {
|
initializePromise = (async () => {
|
||||||
let wasmBuffer;
|
let wasmBuffer;
|
||||||
|
|
||||||
|
console.log('Starting Go WASM initialization...');
|
||||||
|
|
||||||
// Environment-specific initialization
|
// Environment-specific initialization
|
||||||
if (isNode()) {
|
if (isNode()) {
|
||||||
|
console.log('Initializing for Node.js environment');
|
||||||
await initGoRuntimeNode();
|
await initGoRuntimeNode();
|
||||||
wasmBuffer = await loadWasmNode();
|
wasmBuffer = await loadWasmNode();
|
||||||
} else if (isBrowser()) {
|
} else if (isBrowser()) {
|
||||||
|
console.log('Initializing for Browser environment');
|
||||||
await initGoRuntimeBrowser();
|
await initGoRuntimeBrowser();
|
||||||
wasmBuffer = await loadWasmBrowser();
|
wasmBuffer = await loadWasmBrowser();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported environment: neither Node.js nor Browser detected');
|
throw new Error('Unsupported environment: neither Node.js nor Browser detected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Creating Go instance...');
|
||||||
const go = new globalThis.Go();
|
const go = new globalThis.Go();
|
||||||
const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject);
|
|
||||||
|
|
||||||
|
// 详细检查 importObject
|
||||||
|
console.log('Go import object keys:', Object.keys(go.importObject));
|
||||||
|
if (go.importObject.gojs) {
|
||||||
|
console.log('gojs import keys:', Object.keys(go.importObject.gojs));
|
||||||
|
console.log('scheduleTimeoutEvent type:', typeof go.importObject.gojs['runtime.scheduleTimeoutEvent']);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Instantiating WebAssembly module...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject);
|
||||||
|
console.log('WebAssembly instantiation successful');
|
||||||
|
|
||||||
|
console.log('Running Go program...');
|
||||||
// Run Go program (don't await as it's a long-running service)
|
// Run Go program (don't await as it's a long-running service)
|
||||||
go.run(instance).catch(err => {
|
go.run(instance).catch(err => {
|
||||||
console.error('Go WASM program exit error:', err);
|
console.error('Go WASM program exit error:', err);
|
||||||
});
|
});
|
||||||
|
} catch (instantiateError) {
|
||||||
|
console.error('WebAssembly instantiation failed:', instantiateError);
|
||||||
|
console.error('Error details:', {
|
||||||
|
message: instantiateError.message,
|
||||||
|
name: instantiateError.name,
|
||||||
|
stack: instantiateError.stack
|
||||||
|
});
|
||||||
|
throw instantiateError;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for Go program to initialize and expose formatGo function
|
// Wait for Go program to initialize and expose formatGo function
|
||||||
|
console.log('Waiting for formatGo function to be available...');
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
const maxRetries = 10;
|
const maxRetries = 20; // 增加重试次数
|
||||||
|
|
||||||
while (typeof globalThis.formatGo !== 'function' && retries < maxRetries) {
|
while (typeof globalThis.formatGo !== 'function' && retries < maxRetries) {
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 200)); // 增加等待时间
|
||||||
retries++;
|
retries++;
|
||||||
|
if (retries % 5 === 0) {
|
||||||
|
console.log(`Waiting for formatGo function... (${retries}/${maxRetries})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof globalThis.formatGo !== 'function') {
|
if (typeof globalThis.formatGo !== 'function') {
|
||||||
throw new Error('Go WASM module not properly initialized - formatGo function not available');
|
throw new Error('Go WASM module not properly initialized - formatGo function not available after 20 retries');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Go WASM initialization completed successfully');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return initializePromise;
|
return initializePromise;
|
||||||
@@ -147,14 +190,14 @@ const initialize = async () => {
|
|||||||
export const languages = [
|
export const languages = [
|
||||||
{
|
{
|
||||||
name: "Go",
|
name: "Go",
|
||||||
parsers: ["go"],
|
parsers: ["go-format"],
|
||||||
extensions: [".go"],
|
extensions: [".go"],
|
||||||
vscodeLanguageIds: ["go"],
|
vscodeLanguageIds: ["go"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const parsers = {
|
export const parsers = {
|
||||||
go: {
|
"go-format": {
|
||||||
parse: (text) => text,
|
parse: (text) => text,
|
||||||
astFormat: "go-format",
|
astFormat: "go-format",
|
||||||
locStart: (node) => 0,
|
locStart: (node) => 0,
|
||||||
@@ -164,23 +207,35 @@ export const parsers = {
|
|||||||
|
|
||||||
export const printers = {
|
export const printers = {
|
||||||
"go-format": {
|
"go-format": {
|
||||||
print: async (path) => {
|
print: (path) => {
|
||||||
await initialize();
|
|
||||||
const text = path.getValue();
|
const text = path.getValue();
|
||||||
|
|
||||||
if (typeof globalThis.formatGo !== 'function') {
|
if (typeof globalThis.formatGo !== 'function') {
|
||||||
throw new Error('Go WASM module not properly initialized - formatGo function missing');
|
// 如果 formatGo 函数不可用,尝试初始化
|
||||||
|
initialize().then(() => {
|
||||||
|
// 初始化完成后,formatGo 应该可用
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Go WASM initialization failed:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果还是不可用,返回原始文本
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return globalThis.formatGo(text);
|
return globalThis.formatGo(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Go formatting failed: ${error.message}`);
|
console.error('Go formatting failed:', error);
|
||||||
|
// 返回原始文本而不是抛出错误
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export initialize function for manual initialization
|
||||||
|
export { initialize };
|
||||||
|
|
||||||
// Default export for Prettier plugin compatibility
|
// Default export for Prettier plugin compatibility
|
||||||
export default {
|
export default {
|
||||||
languages,
|
languages,
|
||||||
|
|||||||
Binary file not shown.
391
frontend/src/common/prettier/plugins/powershell/ast.ts
Normal file
391
frontend/src/common/prettier/plugins/powershell/ast.ts
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
/**
|
||||||
|
* PowerShell AST 节点定义
|
||||||
|
* 定义抽象语法树的各种节点类型
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Token } from './lexer';
|
||||||
|
|
||||||
|
export interface ASTNode {
|
||||||
|
type: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptBlockAst extends ASTNode {
|
||||||
|
type: 'ScriptBlock';
|
||||||
|
statements: StatementAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatementAst extends ASTNode {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpressionAst extends ASTNode {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管道表达式
|
||||||
|
export interface PipelineAst extends StatementAst {
|
||||||
|
type: 'Pipeline';
|
||||||
|
elements: PipelineElementAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PipelineElementAst extends ASTNode {
|
||||||
|
type: 'PipelineElement';
|
||||||
|
expression: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令表达式
|
||||||
|
export interface CommandAst extends ExpressionAst {
|
||||||
|
type: 'Command';
|
||||||
|
commandName: string;
|
||||||
|
parameters: ParameterAst[];
|
||||||
|
arguments: ExpressionAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParameterAst extends ASTNode {
|
||||||
|
type: 'Parameter';
|
||||||
|
name: string;
|
||||||
|
value?: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值表达式
|
||||||
|
export interface AssignmentAst extends StatementAst {
|
||||||
|
type: 'Assignment';
|
||||||
|
left: ExpressionAst;
|
||||||
|
operator: string;
|
||||||
|
right: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 变量表达式
|
||||||
|
export interface VariableAst extends ExpressionAst {
|
||||||
|
type: 'Variable';
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字面量表达式
|
||||||
|
export interface LiteralAst extends ExpressionAst {
|
||||||
|
type: 'Literal';
|
||||||
|
value: any;
|
||||||
|
literalType: 'String' | 'Number' | 'Boolean' | 'Null';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数组表达式
|
||||||
|
export interface ArrayAst extends ExpressionAst {
|
||||||
|
type: 'Array';
|
||||||
|
elements: ExpressionAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 哈希表表达式
|
||||||
|
export interface HashtableAst extends ExpressionAst {
|
||||||
|
type: 'Hashtable';
|
||||||
|
entries: HashtableEntryAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HashtableEntryAst extends ASTNode {
|
||||||
|
type: 'HashtableEntry';
|
||||||
|
key: ExpressionAst;
|
||||||
|
value: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数定义
|
||||||
|
export interface FunctionDefinitionAst extends StatementAst {
|
||||||
|
type: 'FunctionDefinition';
|
||||||
|
name: string;
|
||||||
|
parameters: ParameterAst[];
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制流结构
|
||||||
|
export interface IfStatementAst extends StatementAst {
|
||||||
|
type: 'IfStatement';
|
||||||
|
condition: ExpressionAst;
|
||||||
|
ifBody: ScriptBlockAst;
|
||||||
|
elseIfClauses: ElseIfClauseAst[];
|
||||||
|
elseBody?: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ElseIfClauseAst extends ASTNode {
|
||||||
|
type: 'ElseIfClause';
|
||||||
|
condition: ExpressionAst;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WhileStatementAst extends StatementAst {
|
||||||
|
type: 'WhileStatement';
|
||||||
|
condition: ExpressionAst;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForStatementAst extends StatementAst {
|
||||||
|
type: 'ForStatement';
|
||||||
|
initializer?: ExpressionAst;
|
||||||
|
condition?: ExpressionAst;
|
||||||
|
iterator?: ExpressionAst;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForEachStatementAst extends StatementAst {
|
||||||
|
type: 'ForEachStatement';
|
||||||
|
variable: VariableAst;
|
||||||
|
iterable: ExpressionAst;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchStatementAst extends StatementAst {
|
||||||
|
type: 'SwitchStatement';
|
||||||
|
value: ExpressionAst;
|
||||||
|
clauses: SwitchClauseAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchClauseAst extends ASTNode {
|
||||||
|
type: 'SwitchClause';
|
||||||
|
pattern: ExpressionAst;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TryStatementAst extends StatementAst {
|
||||||
|
type: 'TryStatement';
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
catchClauses: CatchClauseAst[];
|
||||||
|
finallyClause?: FinallyClauseAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatchClauseAst extends ASTNode {
|
||||||
|
type: 'CatchClause';
|
||||||
|
exceptionType?: string;
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinallyClauseAst extends ASTNode {
|
||||||
|
type: 'FinallyClause';
|
||||||
|
body: ScriptBlockAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二元操作表达式
|
||||||
|
export interface BinaryExpressionAst extends ExpressionAst {
|
||||||
|
type: 'BinaryExpression';
|
||||||
|
left: ExpressionAst;
|
||||||
|
operator: string;
|
||||||
|
right: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一元操作表达式
|
||||||
|
export interface UnaryExpressionAst extends ExpressionAst {
|
||||||
|
type: 'UnaryExpression';
|
||||||
|
operator: string;
|
||||||
|
operand: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 括号表达式
|
||||||
|
export interface ParenthesizedExpressionAst extends ExpressionAst {
|
||||||
|
type: 'ParenthesizedExpression';
|
||||||
|
expression: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法调用表达式
|
||||||
|
export interface MethodCallAst extends ExpressionAst {
|
||||||
|
type: 'MethodCall';
|
||||||
|
object: ExpressionAst;
|
||||||
|
methodName: string;
|
||||||
|
arguments: ExpressionAst[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 属性访问表达式
|
||||||
|
export interface PropertyAccessAst extends ExpressionAst {
|
||||||
|
type: 'PropertyAccess';
|
||||||
|
object: ExpressionAst;
|
||||||
|
propertyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 索引访问表达式
|
||||||
|
export interface IndexAccessAst extends ExpressionAst {
|
||||||
|
type: 'IndexAccess';
|
||||||
|
object: ExpressionAst;
|
||||||
|
index: ExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注释节点
|
||||||
|
export interface CommentAst extends ASTNode {
|
||||||
|
type: 'Comment';
|
||||||
|
text: string;
|
||||||
|
isMultiline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空白节点
|
||||||
|
export interface WhitespaceAst extends ASTNode {
|
||||||
|
type: 'Whitespace';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工厂函数,用于创建AST节点
|
||||||
|
export class ASTNodeFactory {
|
||||||
|
static createScriptBlock(statements: StatementAst[], start: number, end: number, line: number, column: number): ScriptBlockAst {
|
||||||
|
return {
|
||||||
|
type: 'ScriptBlock',
|
||||||
|
statements,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createPipeline(elements: PipelineElementAst[], start: number, end: number, line: number, column: number): PipelineAst {
|
||||||
|
return {
|
||||||
|
type: 'Pipeline',
|
||||||
|
elements,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createCommand(commandName: string, parameters: ParameterAst[], args: ExpressionAst[], start: number, end: number, line: number, column: number): CommandAst {
|
||||||
|
return {
|
||||||
|
type: 'Command',
|
||||||
|
commandName,
|
||||||
|
parameters,
|
||||||
|
arguments: args,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createAssignment(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): AssignmentAst {
|
||||||
|
return {
|
||||||
|
type: 'Assignment',
|
||||||
|
left,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createVariable(name: string, start: number, end: number, line: number, column: number): VariableAst {
|
||||||
|
return {
|
||||||
|
type: 'Variable',
|
||||||
|
name,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createLiteral(value: any, literalType: 'String' | 'Number' | 'Boolean' | 'Null', start: number, end: number, line: number, column: number): LiteralAst {
|
||||||
|
return {
|
||||||
|
type: 'Literal',
|
||||||
|
value,
|
||||||
|
literalType,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createBinaryExpression(left: ExpressionAst, operator: string, right: ExpressionAst, start: number, end: number, line: number, column: number): BinaryExpressionAst {
|
||||||
|
return {
|
||||||
|
type: 'BinaryExpression',
|
||||||
|
left,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createIfStatement(condition: ExpressionAst, ifBody: ScriptBlockAst, elseIfClauses: ElseIfClauseAst[], elseBody: ScriptBlockAst | undefined, start: number, end: number, line: number, column: number): IfStatementAst {
|
||||||
|
return {
|
||||||
|
type: 'IfStatement',
|
||||||
|
condition,
|
||||||
|
ifBody,
|
||||||
|
elseIfClauses,
|
||||||
|
elseBody,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createFunctionDefinition(name: string, parameters: ParameterAst[], body: ScriptBlockAst, start: number, end: number, line: number, column: number): FunctionDefinitionAst {
|
||||||
|
return {
|
||||||
|
type: 'FunctionDefinition',
|
||||||
|
name,
|
||||||
|
parameters,
|
||||||
|
body,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static createComment(text: string, isMultiline: boolean, start: number, end: number, line: number, column: number): CommentAst {
|
||||||
|
return {
|
||||||
|
type: 'Comment',
|
||||||
|
text,
|
||||||
|
isMultiline,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AST访问者模式接口
|
||||||
|
export interface ASTVisitor<T> {
|
||||||
|
visitScriptBlock(node: ScriptBlockAst): T;
|
||||||
|
visitPipeline(node: PipelineAst): T;
|
||||||
|
visitCommand(node: CommandAst): T;
|
||||||
|
visitAssignment(node: AssignmentAst): T;
|
||||||
|
visitVariable(node: VariableAst): T;
|
||||||
|
visitLiteral(node: LiteralAst): T;
|
||||||
|
visitBinaryExpression(node: BinaryExpressionAst): T;
|
||||||
|
visitIfStatement(node: IfStatementAst): T;
|
||||||
|
visitFunctionDefinition(node: FunctionDefinitionAst): T;
|
||||||
|
visitComment(node: CommentAst): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AST遍历工具类
|
||||||
|
export class ASTTraverser {
|
||||||
|
static traverse<T>(node: ASTNode, visitor: Partial<ASTVisitor<T>>): T | undefined {
|
||||||
|
switch (node.type) {
|
||||||
|
case 'ScriptBlock':
|
||||||
|
return visitor.visitScriptBlock?.(node as ScriptBlockAst);
|
||||||
|
case 'Pipeline':
|
||||||
|
return visitor.visitPipeline?.(node as PipelineAst);
|
||||||
|
case 'Command':
|
||||||
|
return visitor.visitCommand?.(node as CommandAst);
|
||||||
|
case 'Assignment':
|
||||||
|
return visitor.visitAssignment?.(node as AssignmentAst);
|
||||||
|
case 'Variable':
|
||||||
|
return visitor.visitVariable?.(node as VariableAst);
|
||||||
|
case 'Literal':
|
||||||
|
return visitor.visitLiteral?.(node as LiteralAst);
|
||||||
|
case 'BinaryExpression':
|
||||||
|
return visitor.visitBinaryExpression?.(node as BinaryExpressionAst);
|
||||||
|
case 'IfStatement':
|
||||||
|
return visitor.visitIfStatement?.(node as IfStatementAst);
|
||||||
|
case 'FunctionDefinition':
|
||||||
|
return visitor.visitFunctionDefinition?.(node as FunctionDefinitionAst);
|
||||||
|
case 'Comment':
|
||||||
|
return visitor.visitComment?.(node as CommentAst);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
/**
|
||||||
|
* PowerShell 代码生成器
|
||||||
|
* 遍历AST并根据格式化规则生成格式化的PowerShell代码
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ASTNode,
|
||||||
|
ScriptBlockAst,
|
||||||
|
StatementAst,
|
||||||
|
ExpressionAst,
|
||||||
|
PipelineAst,
|
||||||
|
CommandAst,
|
||||||
|
AssignmentAst,
|
||||||
|
VariableAst,
|
||||||
|
LiteralAst,
|
||||||
|
BinaryExpressionAst,
|
||||||
|
IfStatementAst,
|
||||||
|
FunctionDefinitionAst,
|
||||||
|
ParameterAst,
|
||||||
|
CommentAst,
|
||||||
|
PipelineElementAst,
|
||||||
|
ElseIfClauseAst,
|
||||||
|
ASTTraverser
|
||||||
|
} from './ast';
|
||||||
|
import { FormatterRules, FormatterOptions } from './formatter-rules';
|
||||||
|
|
||||||
|
export class PowerShellCodeGenerator {
|
||||||
|
private rules: FormatterRules;
|
||||||
|
private indentLevel: number = 0;
|
||||||
|
private output: string[] = [];
|
||||||
|
private currentLineLength: number = 0;
|
||||||
|
private needsNewline: boolean = false;
|
||||||
|
private lastWasComment: boolean = false;
|
||||||
|
|
||||||
|
constructor(options: Partial<FormatterOptions> = {}) {
|
||||||
|
this.rules = new FormatterRules(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成格式化的PowerShell代码
|
||||||
|
*/
|
||||||
|
public generate(ast: ScriptBlockAst, comments: CommentAst[] = []): string {
|
||||||
|
this.output = [];
|
||||||
|
this.indentLevel = 0;
|
||||||
|
this.currentLineLength = 0;
|
||||||
|
this.needsNewline = false;
|
||||||
|
this.lastWasComment = false;
|
||||||
|
|
||||||
|
// 首先处理文档开头的注释
|
||||||
|
this.generateLeadingComments(comments);
|
||||||
|
|
||||||
|
// 生成主体代码
|
||||||
|
this.generateScriptBlock(ast);
|
||||||
|
|
||||||
|
// 处理文档末尾
|
||||||
|
this.handleFinalNewline();
|
||||||
|
|
||||||
|
const result = this.output.join('');
|
||||||
|
return this.postProcess(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateScriptBlock(node: ScriptBlockAst): void {
|
||||||
|
for (let i = 0; i < node.statements.length; i++) {
|
||||||
|
const statement = node.statements[i];
|
||||||
|
const nextStatement = i < node.statements.length - 1 ? node.statements[i + 1] : null;
|
||||||
|
|
||||||
|
this.generateStatement(statement);
|
||||||
|
|
||||||
|
// 在语句之间添加适当的空行
|
||||||
|
if (nextStatement) {
|
||||||
|
this.addStatementSeparation(statement, nextStatement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateStatement(statement: StatementAst): void {
|
||||||
|
switch (statement.type) {
|
||||||
|
case 'Pipeline':
|
||||||
|
this.generatePipeline(statement as PipelineAst);
|
||||||
|
break;
|
||||||
|
case 'Assignment':
|
||||||
|
this.generateAssignment(statement as AssignmentAst);
|
||||||
|
break;
|
||||||
|
case 'IfStatement':
|
||||||
|
this.generateIfStatement(statement as IfStatementAst);
|
||||||
|
break;
|
||||||
|
case 'FunctionDefinition':
|
||||||
|
this.generateFunctionDefinition(statement as FunctionDefinitionAst);
|
||||||
|
break;
|
||||||
|
case 'RawText':
|
||||||
|
// 处理解析失败时的原始文本
|
||||||
|
this.append((statement as any).value);
|
||||||
|
return; // 不需要添加额外的换行
|
||||||
|
default:
|
||||||
|
this.append(`/* Unsupported statement type: ${statement.type} */`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ensureNewline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatePipeline(pipeline: PipelineAst): void {
|
||||||
|
if (!this.rules.formatPipelines) {
|
||||||
|
// 简单连接所有元素
|
||||||
|
for (let i = 0; i < pipeline.elements.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
this.append(' | ');
|
||||||
|
}
|
||||||
|
this.generatePipelineElement(pipeline.elements[i]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = this.rules.getPipelineStyle(pipeline.elements.length);
|
||||||
|
|
||||||
|
if (style === 'multiline') {
|
||||||
|
this.generateMultilinePipeline(pipeline);
|
||||||
|
} else {
|
||||||
|
this.generateOnelinePipeline(pipeline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateOnelinePipeline(pipeline: PipelineAst): void {
|
||||||
|
for (let i = 0; i < pipeline.elements.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
this.append(' | ');
|
||||||
|
}
|
||||||
|
this.generatePipelineElement(pipeline.elements[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMultilinePipeline(pipeline: PipelineAst): void {
|
||||||
|
for (let i = 0; i < pipeline.elements.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
this.appendLine(' |');
|
||||||
|
this.appendIndent();
|
||||||
|
}
|
||||||
|
this.generatePipelineElement(pipeline.elements[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generatePipelineElement(element: PipelineElementAst): void {
|
||||||
|
this.generateExpression(element.expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateExpression(expression: ExpressionAst): void {
|
||||||
|
switch (expression.type) {
|
||||||
|
case 'Command':
|
||||||
|
this.generateCommand(expression as CommandAst);
|
||||||
|
break;
|
||||||
|
case 'Variable':
|
||||||
|
this.generateVariable(expression as VariableAst);
|
||||||
|
break;
|
||||||
|
case 'Literal':
|
||||||
|
this.generateLiteral(expression as LiteralAst);
|
||||||
|
break;
|
||||||
|
case 'BinaryExpression':
|
||||||
|
this.generateBinaryExpression(expression as BinaryExpressionAst);
|
||||||
|
break;
|
||||||
|
case 'ParenthesizedExpression':
|
||||||
|
this.append('(');
|
||||||
|
this.generateExpression((expression as any).expression);
|
||||||
|
this.append(')');
|
||||||
|
break;
|
||||||
|
case 'Array':
|
||||||
|
this.generateArray(expression as any);
|
||||||
|
break;
|
||||||
|
case 'Hashtable':
|
||||||
|
this.generateHashtable(expression as any);
|
||||||
|
break;
|
||||||
|
case 'ScriptBlockExpression':
|
||||||
|
this.generateScriptBlockExpression(expression as any);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.append(`/* Unsupported expression type: ${expression.type} */`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCommand(command: CommandAst): void {
|
||||||
|
// 保持cmdlet名称的连字符,不进行破坏性的格式化
|
||||||
|
let commandName = command.commandName;
|
||||||
|
|
||||||
|
// 只有在明确指定要改变大小写时才进行格式化
|
||||||
|
// 但绝对不能删除连字符
|
||||||
|
if (this.rules.shouldFormatCommandCase()) {
|
||||||
|
commandName = this.rules.formatCommandCase(commandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.append(commandName);
|
||||||
|
|
||||||
|
// 生成参数
|
||||||
|
for (const param of command.parameters) {
|
||||||
|
this.append(' ');
|
||||||
|
this.generateParameter(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成位置参数
|
||||||
|
for (const arg of command.arguments) {
|
||||||
|
this.append(' ');
|
||||||
|
this.generateExpression(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateParameter(parameter: ParameterAst): void {
|
||||||
|
const paramName = this.rules.formatParameterCase(parameter.name);
|
||||||
|
this.append(paramName);
|
||||||
|
|
||||||
|
if (parameter.value) {
|
||||||
|
this.append(' ');
|
||||||
|
this.generateExpression(parameter.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateVariable(variable: VariableAst): void {
|
||||||
|
const formattedName = this.rules.formatVariableCase(variable.name);
|
||||||
|
this.append(formattedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateLiteral(literal: LiteralAst): void {
|
||||||
|
if (literal.literalType === 'String') {
|
||||||
|
const formattedString = this.rules.formatQuotes(literal.value as string);
|
||||||
|
this.append(formattedString);
|
||||||
|
} else {
|
||||||
|
this.append(String(literal.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateBinaryExpression(expression: BinaryExpressionAst): void {
|
||||||
|
this.generateExpression(expression.left);
|
||||||
|
|
||||||
|
// 根据PowerShell官方规范,属性访问操作符绝对不能加空格
|
||||||
|
if (expression.operator === '.' ||
|
||||||
|
expression.operator === '::' ||
|
||||||
|
expression.operator === '[' ||
|
||||||
|
expression.operator === ']' ||
|
||||||
|
expression.operator === '@{') {
|
||||||
|
// 属性访问是PowerShell面向对象的核心,必须保持紧凑
|
||||||
|
this.append(expression.operator);
|
||||||
|
} else {
|
||||||
|
// 使用格式化规则处理其他操作符
|
||||||
|
const formattedOperator = this.rules.formatOperatorSpacing(expression.operator);
|
||||||
|
this.append(formattedOperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generateExpression(expression.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateAssignment(assignment: AssignmentAst): void {
|
||||||
|
this.generateExpression(assignment.left);
|
||||||
|
|
||||||
|
const formattedOperator = this.rules.formatOperatorSpacing(assignment.operator);
|
||||||
|
this.append(formattedOperator);
|
||||||
|
|
||||||
|
this.generateExpression(assignment.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateIfStatement(ifStmt: IfStatementAst): void {
|
||||||
|
// if 条件
|
||||||
|
this.append('if ');
|
||||||
|
this.append(this.rules.formatParentheses(''));
|
||||||
|
this.append('(');
|
||||||
|
this.generateExpression(ifStmt.condition);
|
||||||
|
this.append(')');
|
||||||
|
|
||||||
|
// if 主体
|
||||||
|
this.append(this.rules.getBraceStart());
|
||||||
|
this.appendLine('');
|
||||||
|
this.indent();
|
||||||
|
this.generateScriptBlock(ifStmt.ifBody);
|
||||||
|
this.outdent();
|
||||||
|
this.appendIndent();
|
||||||
|
this.append('}');
|
||||||
|
|
||||||
|
// elseif 子句
|
||||||
|
for (const elseIfClause of ifStmt.elseIfClauses) {
|
||||||
|
this.generateElseIfClause(elseIfClause);
|
||||||
|
}
|
||||||
|
|
||||||
|
// else 子句
|
||||||
|
if (ifStmt.elseBody) {
|
||||||
|
this.append(' else');
|
||||||
|
this.append(this.rules.getBraceStart());
|
||||||
|
this.appendLine('');
|
||||||
|
this.indent();
|
||||||
|
this.generateScriptBlock(ifStmt.elseBody);
|
||||||
|
this.outdent();
|
||||||
|
this.appendIndent();
|
||||||
|
this.append('}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateElseIfClause(elseIf: ElseIfClauseAst): void {
|
||||||
|
this.append(' elseif (');
|
||||||
|
this.generateExpression(elseIf.condition);
|
||||||
|
this.append(')');
|
||||||
|
this.append(this.rules.getBraceStart());
|
||||||
|
this.appendLine('');
|
||||||
|
this.indent();
|
||||||
|
this.generateScriptBlock(elseIf.body);
|
||||||
|
this.outdent();
|
||||||
|
this.appendIndent();
|
||||||
|
this.append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateFunctionDefinition(func: FunctionDefinitionAst): void {
|
||||||
|
// 函数前的空行
|
||||||
|
if (this.rules.blankLinesAroundFunctions > 0) {
|
||||||
|
for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) {
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.append('function ');
|
||||||
|
this.append(func.name);
|
||||||
|
|
||||||
|
// 参数列表
|
||||||
|
if (func.parameters.length > 0) {
|
||||||
|
this.append('(');
|
||||||
|
for (let i = 0; i < func.parameters.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
this.append(this.rules.formatComma());
|
||||||
|
}
|
||||||
|
this.generateParameter(func.parameters[i]);
|
||||||
|
}
|
||||||
|
this.append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 函数体
|
||||||
|
this.append(this.rules.getBraceStart());
|
||||||
|
this.appendLine('');
|
||||||
|
this.indent();
|
||||||
|
this.generateScriptBlock(func.body);
|
||||||
|
this.outdent();
|
||||||
|
this.appendIndent();
|
||||||
|
this.append('}');
|
||||||
|
|
||||||
|
// 函数后的空行
|
||||||
|
if (this.rules.blankLinesAroundFunctions > 0) {
|
||||||
|
for (let i = 0; i < this.rules.blankLinesAroundFunctions; i++) {
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateLeadingComments(comments: CommentAst[]): void {
|
||||||
|
const leadingComments = comments.filter(c => this.isLeadingComment(c));
|
||||||
|
for (const comment of leadingComments) {
|
||||||
|
this.generateComment(comment);
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateComment(comment: CommentAst): void {
|
||||||
|
if (!this.rules.formatComments) {
|
||||||
|
this.append(comment.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.isMultiline) {
|
||||||
|
this.generateMultilineComment(comment.text);
|
||||||
|
} else {
|
||||||
|
this.generateSingleLineComment(comment.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastWasComment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateArray(arrayExpr: any): void {
|
||||||
|
this.append('@(');
|
||||||
|
if (arrayExpr.elements && arrayExpr.elements.length > 0) {
|
||||||
|
for (let i = 0; i < arrayExpr.elements.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
this.append(this.rules.formatComma());
|
||||||
|
}
|
||||||
|
this.generateExpression(arrayExpr.elements[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.append(')');
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateHashtable(hashtableExpr: any): void {
|
||||||
|
this.append('@{');
|
||||||
|
|
||||||
|
if (hashtableExpr.entries && hashtableExpr.entries.length > 0) {
|
||||||
|
// 强制使用紧凑格式,避免换行问题
|
||||||
|
for (let i = 0; i < hashtableExpr.entries.length; i++) {
|
||||||
|
const entry = hashtableExpr.entries[i];
|
||||||
|
|
||||||
|
this.generateExpression(entry.key);
|
||||||
|
this.append('=');
|
||||||
|
this.generateExpression(entry.value);
|
||||||
|
|
||||||
|
// 如果不是最后一个条目,添加分号和空格
|
||||||
|
if (i < hashtableExpr.entries.length - 1) {
|
||||||
|
this.append('; ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateScriptBlockExpression(scriptBlockExpr: any): void {
|
||||||
|
this.append('{');
|
||||||
|
|
||||||
|
// 对原始内容应用基本的格式化规则
|
||||||
|
if (scriptBlockExpr.rawContent) {
|
||||||
|
const formattedContent = this.formatScriptBlockContent(scriptBlockExpr.rawContent);
|
||||||
|
this.append(formattedContent);
|
||||||
|
} else if (scriptBlockExpr.expression) {
|
||||||
|
// 兼容旧格式
|
||||||
|
this.generateExpression(scriptBlockExpr.expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatScriptBlockContent(content: string): string {
|
||||||
|
if (!content || !content.trim()) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用PowerShell官方规范的格式化规则
|
||||||
|
let formatted = content.trim();
|
||||||
|
|
||||||
|
// 1. 保护所有属性访问操作符 - 这是最关键的
|
||||||
|
// 匹配所有形式的属性访问:$var.Property, $_.Property, $obj.Method.Property等
|
||||||
|
formatted = formatted.replace(/(\$[a-zA-Z_][a-zA-Z0-9_]*|\$_)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)/g, '$1.$2');
|
||||||
|
|
||||||
|
// 2. 保护方法调用中的点号
|
||||||
|
formatted = formatted.replace(/(\w)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g, '$1.$2(');
|
||||||
|
|
||||||
|
// 3. 确保数字单位不被分离
|
||||||
|
formatted = formatted.replace(/(\d+)\s*(KB|MB|GB|TB|PB)/gi, '$1$2');
|
||||||
|
|
||||||
|
// 4. PowerShell比较和逻辑操作符需要前后空格
|
||||||
|
const powershellOps = [
|
||||||
|
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
|
||||||
|
'-like', '-notlike', '-match', '-notmatch',
|
||||||
|
'-contains', '-notcontains', '-in', '-notin',
|
||||||
|
'-is', '-isnot', '-as', '-and', '-or', '-not', '-xor'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const op of powershellOps) {
|
||||||
|
const regex = new RegExp(`\\s*${op.replace('-', '\\-')}\\s*`, 'gi');
|
||||||
|
formatted = formatted.replace(regex, ` ${op} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 清理多余空格,但保护属性访问
|
||||||
|
formatted = formatted.replace(/\s{2,}/g, ' ').trim();
|
||||||
|
|
||||||
|
// 6. 最终检查:确保没有属性访问被破坏
|
||||||
|
formatted = formatted.replace(/(\$\w+|\$_)\s+\.\s*/g, '$1.');
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private generateSingleLineComment(text: string): void {
|
||||||
|
// 确保单行注释以 # 开头
|
||||||
|
const cleanText = text.startsWith('#') ? text : `# ${text}`;
|
||||||
|
this.append(cleanText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMultilineComment(text: string): void {
|
||||||
|
// 多行注释保持原格式
|
||||||
|
this.append(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLeadingComment(comment: CommentAst): boolean {
|
||||||
|
// 简单判断:如果注释在文档开头,就认为是前导注释
|
||||||
|
return comment.line <= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addStatementSeparation(current: StatementAst, next: StatementAst): void {
|
||||||
|
// 函数之间添加空行
|
||||||
|
if (current.type === 'FunctionDefinition' || next.type === 'FunctionDefinition') {
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制结构前添加空行
|
||||||
|
if (next.type === 'IfStatement' && !this.lastWasComment) {
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFinalNewline(): void {
|
||||||
|
if (this.rules.insertFinalNewline && this.output.length > 0) {
|
||||||
|
const lastLine = this.output[this.output.length - 1];
|
||||||
|
if (!lastLine.endsWith(this.rules.getNewline())) {
|
||||||
|
this.appendLine('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private postProcess(code: string): string {
|
||||||
|
let result = code;
|
||||||
|
|
||||||
|
// 清理多余的空行
|
||||||
|
if (this.rules.maxConsecutiveEmptyLines >= 0) {
|
||||||
|
const maxEmpty = this.rules.maxConsecutiveEmptyLines;
|
||||||
|
const emptyLinePattern = new RegExp(`(${this.rules.getNewline()}){${maxEmpty + 2},}`, 'g');
|
||||||
|
const replacement = this.rules.getNewline().repeat(maxEmpty + 1);
|
||||||
|
result = result.replace(emptyLinePattern, replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理行尾空白
|
||||||
|
if (this.rules.trimTrailingWhitespace) {
|
||||||
|
result = result.replace(/ +$/gm, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private append(text: string): void {
|
||||||
|
this.output.push(text);
|
||||||
|
this.currentLineLength += text.length;
|
||||||
|
this.needsNewline = false;
|
||||||
|
this.lastWasComment = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendLine(text: string): void {
|
||||||
|
this.output.push(text + this.rules.getNewline());
|
||||||
|
this.currentLineLength = 0;
|
||||||
|
this.needsNewline = false;
|
||||||
|
this.lastWasComment = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private appendIndent(): void {
|
||||||
|
const indent = this.rules.getIndent(this.indentLevel);
|
||||||
|
this.append(indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureNewline(): void {
|
||||||
|
if (!this.needsNewline) {
|
||||||
|
this.appendLine('');
|
||||||
|
this.needsNewline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private indent(): void {
|
||||||
|
this.indentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private outdent(): void {
|
||||||
|
this.indentLevel = Math.max(0, this.indentLevel - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldWrapLine(): boolean {
|
||||||
|
return this.currentLineLength > this.rules.printWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷函数:格式化PowerShell AST
|
||||||
|
*/
|
||||||
|
export function formatPowerShellAST(
|
||||||
|
ast: ScriptBlockAst,
|
||||||
|
comments: CommentAst[] = [],
|
||||||
|
options: Partial<FormatterOptions> = {}
|
||||||
|
): string {
|
||||||
|
const generator = new PowerShellCodeGenerator(options);
|
||||||
|
return generator.generate(ast, comments);
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* PowerShell 格式化规则引擎
|
||||||
|
* 定义各种可配置的代码格式化规则和策略
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface FormatterOptions {
|
||||||
|
// 基本格式化选项
|
||||||
|
indentSize: number; // 缩进大小
|
||||||
|
useTabsForIndentation: boolean; // 使用制表符还是空格
|
||||||
|
printWidth: number; // 行最大长度
|
||||||
|
endOfLine: 'lf' | 'crlf' | 'cr' | 'auto'; // 行尾符类型
|
||||||
|
|
||||||
|
// 空格和间距
|
||||||
|
spaceAroundOperators: boolean; // 操作符周围的空格
|
||||||
|
spaceAfterCommas: boolean; // 逗号后的空格
|
||||||
|
spaceAfterSemicolons: boolean; // 分号后的空格
|
||||||
|
spaceInsideParentheses: boolean; // 括号内的空格
|
||||||
|
spaceInsideBrackets: boolean; // 方括号内的空格
|
||||||
|
spaceInsideBraces: boolean; // 大括号内的空格
|
||||||
|
|
||||||
|
// 换行和空行
|
||||||
|
maxConsecutiveEmptyLines: number; // 最大连续空行数
|
||||||
|
insertFinalNewline: boolean; // 文件末尾插入换行符
|
||||||
|
trimTrailingWhitespace: boolean; // 删除行尾空白
|
||||||
|
blankLinesAroundFunctions: number; // 函数前后的空行数
|
||||||
|
blankLinesAroundClasses: number; // 类前后的空行数
|
||||||
|
blankLinesAroundIfStatements: boolean; // if语句前后的空行
|
||||||
|
|
||||||
|
// 括号和大括号
|
||||||
|
braceStyle: 'allman' | 'otbs' | 'stroustrup'; // 大括号风格
|
||||||
|
alwaysParenthesizeArrowFunctions: boolean; // 箭头函数总是用括号
|
||||||
|
|
||||||
|
// PowerShell特定选项
|
||||||
|
formatPipelines: boolean; // 格式化管道
|
||||||
|
pipelineStyle: 'oneline' | 'multiline' | 'auto'; // 管道风格
|
||||||
|
formatParameters: boolean; // 格式化参数
|
||||||
|
parameterAlignment: 'left' | 'right' | 'auto'; // 参数对齐方式
|
||||||
|
formatHashtables: boolean; // 格式化哈希表
|
||||||
|
hashtableStyle: 'compact' | 'expanded'; // 哈希表风格
|
||||||
|
formatArrays: boolean; // 格式化数组
|
||||||
|
arrayStyle: 'compact' | 'expanded'; // 数组风格
|
||||||
|
formatComments: boolean; // 格式化注释
|
||||||
|
commentAlignment: 'left' | 'preserve'; // 注释对齐方式
|
||||||
|
|
||||||
|
// 命名和大小写
|
||||||
|
preferredCommandCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 命令大小写
|
||||||
|
preferredParameterCase: 'lowercase' | 'uppercase' | 'pascalcase' | 'preserve'; // 参数大小写
|
||||||
|
preferredVariableCase: 'camelcase' | 'pascalcase' | 'preserve'; // 变量大小写
|
||||||
|
|
||||||
|
// 引号和字符串
|
||||||
|
quotestyle: 'single' | 'double' | 'preserve'; // 引号风格
|
||||||
|
escapeNonAscii: boolean; // 转义非ASCII字符
|
||||||
|
|
||||||
|
// 长度和换行
|
||||||
|
wrapLongLines: boolean; // 自动换行长行
|
||||||
|
wrapParameters: boolean; // 换行长参数列表
|
||||||
|
wrapArrays: boolean; // 换行长数组
|
||||||
|
wrapHashtables: boolean; // 换行长哈希表
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_OPTIONS: FormatterOptions = {
|
||||||
|
// 基本选项
|
||||||
|
indentSize: 4,
|
||||||
|
useTabsForIndentation: false,
|
||||||
|
printWidth: 120,
|
||||||
|
endOfLine: 'auto',
|
||||||
|
|
||||||
|
// 空格设置
|
||||||
|
spaceAroundOperators: true,
|
||||||
|
spaceAfterCommas: true,
|
||||||
|
spaceAfterSemicolons: true,
|
||||||
|
spaceInsideParentheses: false,
|
||||||
|
spaceInsideBrackets: false,
|
||||||
|
spaceInsideBraces: true,
|
||||||
|
|
||||||
|
// 空行设置
|
||||||
|
maxConsecutiveEmptyLines: 2,
|
||||||
|
insertFinalNewline: true,
|
||||||
|
trimTrailingWhitespace: true,
|
||||||
|
blankLinesAroundFunctions: 1,
|
||||||
|
blankLinesAroundClasses: 1,
|
||||||
|
blankLinesAroundIfStatements: false,
|
||||||
|
|
||||||
|
// 括号风格
|
||||||
|
braceStyle: 'otbs', // One True Brace Style
|
||||||
|
alwaysParenthesizeArrowFunctions: false,
|
||||||
|
|
||||||
|
// PowerShell特定
|
||||||
|
formatPipelines: true,
|
||||||
|
pipelineStyle: 'auto',
|
||||||
|
formatParameters: true,
|
||||||
|
parameterAlignment: 'left',
|
||||||
|
formatHashtables: true,
|
||||||
|
hashtableStyle: 'compact',
|
||||||
|
formatArrays: true,
|
||||||
|
arrayStyle: 'compact',
|
||||||
|
formatComments: true,
|
||||||
|
commentAlignment: 'preserve',
|
||||||
|
|
||||||
|
// 命名约定
|
||||||
|
preferredCommandCase: 'pascalcase',
|
||||||
|
preferredParameterCase: 'preserve',
|
||||||
|
preferredVariableCase: 'preserve',
|
||||||
|
|
||||||
|
// 字符串设置
|
||||||
|
quotestyle: 'preserve',
|
||||||
|
escapeNonAscii: false,
|
||||||
|
|
||||||
|
// 长度处理
|
||||||
|
wrapLongLines: true,
|
||||||
|
wrapParameters: true,
|
||||||
|
wrapArrays: true,
|
||||||
|
wrapHashtables: true
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化规则类,包含各种格式化策略的实现
|
||||||
|
*/
|
||||||
|
export class FormatterRules {
|
||||||
|
private options: FormatterOptions;
|
||||||
|
|
||||||
|
constructor(options: Partial<FormatterOptions> = {}) {
|
||||||
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缩进字符串
|
||||||
|
*/
|
||||||
|
getIndent(level: number): string {
|
||||||
|
if (level <= 0) return '';
|
||||||
|
|
||||||
|
const indentChar = this.options.useTabsForIndentation ? '\t' : ' ';
|
||||||
|
const indentSize = this.options.useTabsForIndentation ? 1 : this.options.indentSize;
|
||||||
|
|
||||||
|
return indentChar.repeat(level * indentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取换行符
|
||||||
|
*/
|
||||||
|
getNewline(): string {
|
||||||
|
switch (this.options.endOfLine) {
|
||||||
|
case 'lf': return '\n';
|
||||||
|
case 'crlf': return '\r\n';
|
||||||
|
case 'cr': return '\r';
|
||||||
|
case 'auto':
|
||||||
|
default:
|
||||||
|
// 在浏览器环境中默认使用 LF
|
||||||
|
return '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化操作符周围的空格
|
||||||
|
*/
|
||||||
|
formatOperatorSpacing(operator: string): string {
|
||||||
|
if (!this.options.spaceAroundOperators) {
|
||||||
|
return operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerShell语法中绝对不能加空格的操作符(官方规范)
|
||||||
|
const noSpaceOperators = [
|
||||||
|
'.', '::', // 属性访问和静态成员访问 - 这是PowerShell面向对象的核心
|
||||||
|
'[', ']', // 数组索引和类型转换
|
||||||
|
'(', ')', '{', '}', // 括号
|
||||||
|
'@{', // 哈希表字面量开始
|
||||||
|
';', // 哈希表和语句分隔符
|
||||||
|
'-', // cmdlet连字符(Get-ChildItem中的-)
|
||||||
|
'::' // 静态成员访问
|
||||||
|
];
|
||||||
|
|
||||||
|
if (noSpaceOperators.includes(operator)) {
|
||||||
|
return operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerShell比较操作符需要空格
|
||||||
|
const powershellOperators = ['-eq', '-ne', '-lt', '-le', '-gt', '-ge',
|
||||||
|
'-like', '-notlike', '-match', '-notmatch',
|
||||||
|
'-contains', '-notcontains', '-in', '-notin',
|
||||||
|
'-is', '-isnot', '-as', '-and', '-or', '-not', '-xor'];
|
||||||
|
|
||||||
|
if (powershellOperators.some(op => operator.toLowerCase() === op)) {
|
||||||
|
return ` ${operator} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 算术和赋值操作符需要空格
|
||||||
|
const spaceOperators = ['=', '+=', '-=', '*=', '/=', '%=', '+', '*', '/', '%'];
|
||||||
|
if (spaceOperators.includes(operator)) {
|
||||||
|
return ` ${operator} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
return operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化逗号后的空格
|
||||||
|
*/
|
||||||
|
formatComma(): string {
|
||||||
|
return this.options.spaceAfterCommas ? ', ' : ',';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化分号后的空格
|
||||||
|
*/
|
||||||
|
formatSemicolon(): string {
|
||||||
|
return this.options.spaceAfterSemicolons ? '; ' : ';';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化括号内的空格
|
||||||
|
*/
|
||||||
|
formatParentheses(content: string): string {
|
||||||
|
if (this.options.spaceInsideParentheses) {
|
||||||
|
return `( ${content} )`;
|
||||||
|
}
|
||||||
|
return `(${content})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化方括号内的空格
|
||||||
|
*/
|
||||||
|
formatBrackets(content: string): string {
|
||||||
|
if (this.options.spaceInsideBrackets) {
|
||||||
|
return `[ ${content} ]`;
|
||||||
|
}
|
||||||
|
return `[${content}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化大括号内的空格
|
||||||
|
*/
|
||||||
|
formatBraces(content: string): string {
|
||||||
|
if (this.options.spaceInsideBraces) {
|
||||||
|
return `{ ${content} }`;
|
||||||
|
}
|
||||||
|
return `{${content}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取大括号的开始位置
|
||||||
|
*/
|
||||||
|
getBraceStart(): string {
|
||||||
|
switch (this.options.braceStyle) {
|
||||||
|
case 'allman':
|
||||||
|
return this.getNewline() + '{';
|
||||||
|
case 'stroustrup':
|
||||||
|
return this.getNewline() + '{';
|
||||||
|
case 'otbs':
|
||||||
|
default:
|
||||||
|
return ' {';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化命令名的大小写
|
||||||
|
*/
|
||||||
|
formatCommandCase(command: string): string {
|
||||||
|
switch (this.options.preferredCommandCase) {
|
||||||
|
case 'lowercase':
|
||||||
|
return command.toLowerCase();
|
||||||
|
case 'uppercase':
|
||||||
|
return command.toUpperCase();
|
||||||
|
case 'pascalcase':
|
||||||
|
return this.toPascalCasePreservingHyphens(command);
|
||||||
|
case 'preserve':
|
||||||
|
default:
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否应该格式化命令大小写
|
||||||
|
*/
|
||||||
|
shouldFormatCommandCase(): boolean {
|
||||||
|
return this.options.preferredCommandCase !== 'preserve';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化参数名的大小写
|
||||||
|
*/
|
||||||
|
formatParameterCase(parameter: string): string {
|
||||||
|
switch (this.options.preferredParameterCase) {
|
||||||
|
case 'lowercase':
|
||||||
|
return parameter.toLowerCase();
|
||||||
|
case 'uppercase':
|
||||||
|
return parameter.toUpperCase();
|
||||||
|
case 'pascalcase':
|
||||||
|
return this.toPascalCase(parameter);
|
||||||
|
case 'preserve':
|
||||||
|
default:
|
||||||
|
return parameter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化变量名的大小写
|
||||||
|
*/
|
||||||
|
formatVariableCase(variable: string): string {
|
||||||
|
if (!variable.startsWith('$')) {
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variableName = variable.substring(1);
|
||||||
|
let formattedName: string;
|
||||||
|
|
||||||
|
switch (this.options.preferredVariableCase) {
|
||||||
|
case 'camelcase':
|
||||||
|
formattedName = this.toCamelCase(variableName);
|
||||||
|
break;
|
||||||
|
case 'pascalcase':
|
||||||
|
formattedName = this.toPascalCase(variableName);
|
||||||
|
break;
|
||||||
|
case 'preserve':
|
||||||
|
default:
|
||||||
|
formattedName = variableName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '$' + formattedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化字符串引号
|
||||||
|
*/
|
||||||
|
formatQuotes(value: string): string {
|
||||||
|
if (this.options.quotestyle === 'preserve') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this.extractStringContent(value);
|
||||||
|
|
||||||
|
switch (this.options.quotestyle) {
|
||||||
|
case 'single':
|
||||||
|
return `'${content.replace(/'/g, "''")}'`;
|
||||||
|
case 'double':
|
||||||
|
return `"${content.replace(/"/g, '""')}"`;
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否需要换行
|
||||||
|
*/
|
||||||
|
shouldWrapLine(line: string): boolean {
|
||||||
|
return this.options.wrapLongLines && line.length > this.options.printWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取管道样式
|
||||||
|
*/
|
||||||
|
getPipelineStyle(elementCount: number): 'oneline' | 'multiline' {
|
||||||
|
switch (this.options.pipelineStyle) {
|
||||||
|
case 'oneline':
|
||||||
|
return 'oneline';
|
||||||
|
case 'multiline':
|
||||||
|
return 'multiline';
|
||||||
|
case 'auto':
|
||||||
|
default:
|
||||||
|
return elementCount > 2 ? 'multiline' : 'oneline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取哈希表样式
|
||||||
|
*/
|
||||||
|
getHashtableStyle(entryCount: number): 'compact' | 'expanded' {
|
||||||
|
if (this.options.hashtableStyle === 'compact') {
|
||||||
|
return 'compact';
|
||||||
|
}
|
||||||
|
if (this.options.hashtableStyle === 'expanded') {
|
||||||
|
return 'expanded';
|
||||||
|
}
|
||||||
|
// auto logic: 对于小型哈希表默认使用compact,避免不必要的换行
|
||||||
|
return entryCount > 5 ? 'expanded' : 'compact';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数组样式
|
||||||
|
*/
|
||||||
|
getArrayStyle(elementCount: number): 'compact' | 'expanded' {
|
||||||
|
if (this.options.arrayStyle === 'compact') {
|
||||||
|
return 'compact';
|
||||||
|
}
|
||||||
|
if (this.options.arrayStyle === 'expanded') {
|
||||||
|
return 'expanded';
|
||||||
|
}
|
||||||
|
// auto logic could be added here
|
||||||
|
return elementCount > 5 ? 'expanded' : 'compact';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private toPascalCase(str: string): string {
|
||||||
|
return str.split(/[-_\s]/)
|
||||||
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为PascalCase但保留连字符(专门用于PowerShell cmdlet)
|
||||||
|
*/
|
||||||
|
private toPascalCasePreservingHyphens(str: string): string {
|
||||||
|
return str.split('-')
|
||||||
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||||
|
.join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
private toCamelCase(str: string): string {
|
||||||
|
const pascalCase = this.toPascalCase(str);
|
||||||
|
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractStringContent(str: string): string {
|
||||||
|
if ((str.startsWith('"') && str.endsWith('"')) ||
|
||||||
|
(str.startsWith("'") && str.endsWith("'"))) {
|
||||||
|
return str.slice(1, -1);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter methods for options
|
||||||
|
get indentSize(): number { return this.options.indentSize; }
|
||||||
|
get printWidth(): number { return this.options.printWidth; }
|
||||||
|
get maxConsecutiveEmptyLines(): number { return this.options.maxConsecutiveEmptyLines; }
|
||||||
|
get insertFinalNewline(): boolean { return this.options.insertFinalNewline; }
|
||||||
|
get trimTrailingWhitespace(): boolean { return this.options.trimTrailingWhitespace; }
|
||||||
|
get blankLinesAroundFunctions(): number { return this.options.blankLinesAroundFunctions; }
|
||||||
|
get formatPipelines(): boolean { return this.options.formatPipelines; }
|
||||||
|
get formatParameters(): boolean { return this.options.formatParameters; }
|
||||||
|
get formatHashtables(): boolean { return this.options.formatHashtables; }
|
||||||
|
get formatArrays(): boolean { return this.options.formatArrays; }
|
||||||
|
get formatComments(): boolean { return this.options.formatComments; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建规则的副本,可以重写部分选项
|
||||||
|
*/
|
||||||
|
withOptions(overrides: Partial<FormatterOptions>): FormatterRules {
|
||||||
|
return new FormatterRules({ ...this.options, ...overrides });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Prettier Plugin for PowerShell file formatting
|
* Prettier Plugin for PowerShell file formatting - Modular Version
|
||||||
*
|
*
|
||||||
* This plugin provides support for formatting PowerShell files (.ps1, .psm1, .psd1)
|
* This plugin provides support for formatting PowerShell files (.ps1, .psm1, .psd1)
|
||||||
* using PowerShell's native AST parser for accurate syntax analysis.
|
* using a modular architecture with lexer, parser, AST, and code generator.
|
||||||
*/
|
*/
|
||||||
import type { Plugin, Parser, Printer, AstPath, Doc } from 'prettier';
|
import type { Plugin, Parser, Printer, AstPath, Doc } from 'prettier';
|
||||||
import parse, { formatPowerShellCode } from './parse';
|
import { PowerShellLexer } from './lexer';
|
||||||
|
import { PowerShellParser } from './parser';
|
||||||
|
import { ScriptBlockAst, CommentAst } from './ast';
|
||||||
|
import { formatPowerShellAST } from './code-generator';
|
||||||
|
import { FormatterOptions, DEFAULT_OPTIONS } from './formatter-rules';
|
||||||
|
|
||||||
// PowerShell AST节点接口
|
// PowerShell格式化结果接口
|
||||||
interface PowerShellAstNode {
|
interface PowerShellParseResult {
|
||||||
type: string;
|
ast: ScriptBlockAst;
|
||||||
value: string;
|
comments: CommentAst[];
|
||||||
start?: number;
|
originalText: string;
|
||||||
end?: number;
|
|
||||||
parent?: PowerShellAstNode;
|
|
||||||
extent?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parserName = 'powershell';
|
const parserName = 'powershell';
|
||||||
@@ -35,62 +36,159 @@ const languages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 解析器配置
|
// 解析器配置
|
||||||
const powershellParser: Parser<PowerShellAstNode | PowerShellAstNode[]> = {
|
const powershellParser: Parser<PowerShellParseResult> = {
|
||||||
parse,
|
parse: parseCode,
|
||||||
astFormat: 'powershell',
|
astFormat: 'powershell',
|
||||||
locStart: () => 0,
|
locStart: (node: PowerShellParseResult) => 0,
|
||||||
locEnd: () => 0,
|
locEnd: (node: PowerShellParseResult) => node.originalText.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
const printPosh = (path: AstPath<PowerShellAstNode | PowerShellAstNode[]>, options: any, print: any): Doc => {
|
/**
|
||||||
const pathNode = path.node;
|
* 解析PowerShell代码
|
||||||
|
*/
|
||||||
if (Array.isArray(pathNode)) {
|
async function parseCode(text: string, parsers?: any, options?: any): Promise<PowerShellParseResult> {
|
||||||
return pathNode.map(node => handleAst(node, path, options, print)).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleAst(pathNode, path, options, print);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAst = (node: PowerShellAstNode, path: AstPath<any>, options: any, print: any): Doc => {
|
|
||||||
if (typeof node === 'undefined') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用修复后的PowerShell格式化器
|
|
||||||
try {
|
try {
|
||||||
const formattedCode = formatPowerShellCode(node.value, {
|
// 词法分析
|
||||||
indentSize: options.tabWidth || 4,
|
const lexer = new PowerShellLexer(text);
|
||||||
useTabsForIndentation: options.useTabs || false,
|
const tokens = lexer.tokenize();
|
||||||
printWidth: options.printWidth || 120,
|
|
||||||
|
// 语法分析
|
||||||
|
const parser = new PowerShellParser(tokens, text);
|
||||||
|
const ast = parser.parse();
|
||||||
|
const comments = parser.getComments();
|
||||||
|
|
||||||
|
return {
|
||||||
|
ast,
|
||||||
|
comments,
|
||||||
|
originalText: text
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('PowerShell parsing failed, using fallback:', error);
|
||||||
|
|
||||||
|
// 解析失败时,创建一个包含原始文本的简单AST
|
||||||
|
// 这样可以确保格式化失败时返回原始代码而不是空内容
|
||||||
|
return {
|
||||||
|
ast: {
|
||||||
|
type: 'ScriptBlock',
|
||||||
|
statements: [{
|
||||||
|
type: 'RawText',
|
||||||
|
value: text,
|
||||||
|
start: 0,
|
||||||
|
end: text.length,
|
||||||
|
line: 1,
|
||||||
|
column: 1
|
||||||
|
} as any],
|
||||||
|
start: 0,
|
||||||
|
end: text.length,
|
||||||
|
line: 1,
|
||||||
|
column: 1
|
||||||
|
},
|
||||||
|
comments: [],
|
||||||
|
originalText: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PowerShell代码打印器
|
||||||
|
*/
|
||||||
|
const printPowerShell = (path: AstPath<PowerShellParseResult>, options: any): Doc => {
|
||||||
|
const parseResult = path.node;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建格式化选项 - 优先保持原有格式,避免破坏PowerShell语法
|
||||||
|
const formatterOptions: Partial<FormatterOptions> = {
|
||||||
|
indentSize: options.tabWidth || DEFAULT_OPTIONS.indentSize,
|
||||||
|
useTabsForIndentation: options.useTabs || DEFAULT_OPTIONS.useTabsForIndentation,
|
||||||
|
printWidth: options.printWidth || DEFAULT_OPTIONS.printWidth,
|
||||||
spaceAroundOperators: true,
|
spaceAroundOperators: true,
|
||||||
formatComments: true,
|
|
||||||
removeExtraBlankLines: true,
|
|
||||||
formatPipelines: true,
|
formatPipelines: true,
|
||||||
formatParentheses: true,
|
formatParameters: true,
|
||||||
formatArraysAndHashtables: true,
|
formatHashtables: true,
|
||||||
|
hashtableStyle: 'compact', // 强制使用紧凑格式,避免不必要的换行
|
||||||
|
formatArrays: true,
|
||||||
|
arrayStyle: 'compact',
|
||||||
|
formatComments: true,
|
||||||
maxConsecutiveEmptyLines: 1,
|
maxConsecutiveEmptyLines: 1,
|
||||||
addBlankLinesAroundBlocks: true,
|
insertFinalNewline: true,
|
||||||
formatLongLines: true,
|
trimTrailingWhitespace: true,
|
||||||
formatFunctionDefinitions: true,
|
blankLinesAroundFunctions: 1,
|
||||||
formatPowerShellSyntax: true,
|
braceStyle: 'otbs',
|
||||||
});
|
preferredCommandCase: 'preserve', // 保持原有命令大小写,不破坏语法
|
||||||
|
preferredParameterCase: 'preserve',
|
||||||
|
preferredVariableCase: 'preserve',
|
||||||
|
quotestyle: 'preserve',
|
||||||
|
wrapLongLines: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用新的模块化格式化器
|
||||||
|
const formattedCode = formatPowerShellAST(
|
||||||
|
parseResult.ast,
|
||||||
|
parseResult.comments,
|
||||||
|
formatterOptions
|
||||||
|
);
|
||||||
|
|
||||||
return formattedCode;
|
return formattedCode;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('PowerShell formatting failed, returning original code:', error);
|
console.warn('PowerShell formatting failed, returning original code:', error);
|
||||||
return node.value || '';
|
return parseResult.originalText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打印器配置
|
// 打印器配置
|
||||||
const powershellPrinter: Printer<PowerShellAstNode | PowerShellAstNode[]> = {
|
const powershellPrinter: Printer<PowerShellParseResult> = {
|
||||||
print: printPosh,
|
print: printPowerShell,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 插件选项
|
// 插件选项配置
|
||||||
const options = {
|
const options = {
|
||||||
|
// PowerShell特定格式化选项
|
||||||
|
powershellBraceStyle: {
|
||||||
|
type: 'choice' as const,
|
||||||
|
category: 'PowerShell',
|
||||||
|
default: DEFAULT_OPTIONS.braceStyle,
|
||||||
|
description: 'PowerShell大括号样式',
|
||||||
|
choices: [
|
||||||
|
{ value: 'allman', description: 'Allman风格(大括号另起一行)' },
|
||||||
|
{ value: 'otbs', description: '1TBS风格(大括号同行)' },
|
||||||
|
{ value: 'stroustrup', description: 'Stroustrup风格' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
powershellCommandCase: {
|
||||||
|
type: 'choice' as const,
|
||||||
|
category: 'PowerShell',
|
||||||
|
default: DEFAULT_OPTIONS.preferredCommandCase,
|
||||||
|
description: 'PowerShell命令大小写风格',
|
||||||
|
choices: [
|
||||||
|
{ value: 'lowercase', description: '小写' },
|
||||||
|
{ value: 'uppercase', description: '大写' },
|
||||||
|
{ value: 'pascalcase', description: 'Pascal大小写' },
|
||||||
|
{ value: 'preserve', description: '保持原样' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
powershellPipelineStyle: {
|
||||||
|
type: 'choice' as const,
|
||||||
|
category: 'PowerShell',
|
||||||
|
default: DEFAULT_OPTIONS.pipelineStyle,
|
||||||
|
description: 'PowerShell管道样式',
|
||||||
|
choices: [
|
||||||
|
{ value: 'oneline', description: '单行' },
|
||||||
|
{ value: 'multiline', description: '多行' },
|
||||||
|
{ value: 'auto', description: '自动' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
powershellSpaceAroundOperators: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
category: 'PowerShell',
|
||||||
|
default: DEFAULT_OPTIONS.spaceAroundOperators,
|
||||||
|
description: '在操作符周围添加空格'
|
||||||
|
},
|
||||||
|
powershellMaxEmptyLines: {
|
||||||
|
type: 'int' as const,
|
||||||
|
category: 'PowerShell',
|
||||||
|
default: DEFAULT_OPTIONS.maxConsecutiveEmptyLines,
|
||||||
|
description: '最大连续空行数'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const powershellPlugin: Plugin = {
|
const powershellPlugin: Plugin = {
|
||||||
|
|||||||
722
frontend/src/common/prettier/plugins/powershell/lexer.ts
Normal file
722
frontend/src/common/prettier/plugins/powershell/lexer.ts
Normal file
@@ -0,0 +1,722 @@
|
|||||||
|
/**
|
||||||
|
* PowerShell 词法分析器 (Lexer)
|
||||||
|
* 将PowerShell代码分解为tokens,用于后续的语法分析和格式化
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum TokenType {
|
||||||
|
// 字面量
|
||||||
|
STRING = 'STRING',
|
||||||
|
NUMBER = 'NUMBER',
|
||||||
|
VARIABLE = 'VARIABLE',
|
||||||
|
|
||||||
|
// 关键字
|
||||||
|
KEYWORD = 'KEYWORD',
|
||||||
|
FUNCTION = 'FUNCTION',
|
||||||
|
|
||||||
|
// 操作符
|
||||||
|
OPERATOR = 'OPERATOR',
|
||||||
|
ASSIGNMENT = 'ASSIGNMENT',
|
||||||
|
COMPARISON = 'COMPARISON',
|
||||||
|
LOGICAL = 'LOGICAL',
|
||||||
|
ARITHMETIC = 'ARITHMETIC',
|
||||||
|
|
||||||
|
// 分隔符
|
||||||
|
LEFT_PAREN = 'LEFT_PAREN',
|
||||||
|
RIGHT_PAREN = 'RIGHT_PAREN',
|
||||||
|
LEFT_BRACE = 'LEFT_BRACE',
|
||||||
|
RIGHT_BRACE = 'RIGHT_BRACE',
|
||||||
|
LEFT_BRACKET = 'LEFT_BRACKET',
|
||||||
|
RIGHT_BRACKET = 'RIGHT_BRACKET',
|
||||||
|
SEMICOLON = 'SEMICOLON',
|
||||||
|
COMMA = 'COMMA',
|
||||||
|
DOT = 'DOT',
|
||||||
|
PIPE = 'PIPE',
|
||||||
|
|
||||||
|
// 特殊
|
||||||
|
WHITESPACE = 'WHITESPACE',
|
||||||
|
NEWLINE = 'NEWLINE',
|
||||||
|
COMMENT = 'COMMENT',
|
||||||
|
MULTILINE_COMMENT = 'MULTILINE_COMMENT',
|
||||||
|
HERE_STRING = 'HERE_STRING',
|
||||||
|
|
||||||
|
// 控制结构
|
||||||
|
IF = 'IF',
|
||||||
|
ELSE = 'ELSE',
|
||||||
|
ELSEIF = 'ELSEIF',
|
||||||
|
WHILE = 'WHILE',
|
||||||
|
FOR = 'FOR',
|
||||||
|
FOREACH = 'FOREACH',
|
||||||
|
SWITCH = 'SWITCH',
|
||||||
|
TRY = 'TRY',
|
||||||
|
CATCH = 'CATCH',
|
||||||
|
FINALLY = 'FINALLY',
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
IDENTIFIER = 'IDENTIFIER',
|
||||||
|
CMDLET = 'CMDLET',
|
||||||
|
PARAMETER = 'PARAMETER',
|
||||||
|
EOF = 'EOF',
|
||||||
|
UNKNOWN = 'UNKNOWN'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
type: TokenType;
|
||||||
|
value: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
startIndex: number;
|
||||||
|
endIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PowerShellLexer {
|
||||||
|
private code: string;
|
||||||
|
private position: number = 0;
|
||||||
|
private line: number = 1;
|
||||||
|
private column: number = 1;
|
||||||
|
private tokens: Token[] = [];
|
||||||
|
|
||||||
|
// PowerShell关键字
|
||||||
|
private readonly keywords = new Set([
|
||||||
|
'if', 'else', 'elseif', 'switch', 'while', 'for', 'foreach', 'do',
|
||||||
|
'try', 'catch', 'finally', 'throw', 'return', 'break', 'continue',
|
||||||
|
'function', 'filter', 'param', 'begin', 'process', 'end',
|
||||||
|
'class', 'enum', 'using', 'namespace', 'workflow', 'configuration',
|
||||||
|
'dynamicparam', 'exit'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// PowerShell比较操作符
|
||||||
|
private readonly comparisonOperators = new Set([
|
||||||
|
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
|
||||||
|
'-like', '-notlike', '-match', '-notmatch',
|
||||||
|
'-contains', '-notcontains', '-in', '-notin',
|
||||||
|
'-is', '-isnot', '-as'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// PowerShell逻辑操作符
|
||||||
|
private readonly logicalOperators = new Set([
|
||||||
|
'-and', '-or', '-not', '-xor', '-band', '-bor', '-bxor', '-bnot'
|
||||||
|
]);
|
||||||
|
|
||||||
|
constructor(code: string) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对代码进行词法分析,返回token数组
|
||||||
|
*/
|
||||||
|
public tokenize(): Token[] {
|
||||||
|
this.position = 0;
|
||||||
|
this.line = 1;
|
||||||
|
this.column = 1;
|
||||||
|
this.tokens = [];
|
||||||
|
|
||||||
|
while (this.position < this.code.length) {
|
||||||
|
this.skipWhitespace();
|
||||||
|
|
||||||
|
if (this.position >= this.code.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.nextToken();
|
||||||
|
if (token) {
|
||||||
|
this.tokens.push(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tokens.push({
|
||||||
|
type: TokenType.EOF,
|
||||||
|
value: '',
|
||||||
|
line: this.line,
|
||||||
|
column: this.column,
|
||||||
|
startIndex: this.position,
|
||||||
|
endIndex: this.position
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextToken(): Token | null {
|
||||||
|
const startPos = this.position;
|
||||||
|
const startLine = this.line;
|
||||||
|
const startColumn = this.column;
|
||||||
|
|
||||||
|
const char = this.code[this.position];
|
||||||
|
|
||||||
|
// 处理换行
|
||||||
|
if (char === '\n') {
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.NEWLINE, '\n', startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理注释
|
||||||
|
if (char === '#') {
|
||||||
|
return this.tokenizeComment(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理多行注释
|
||||||
|
if (char === '<' && this.peek() === '#') {
|
||||||
|
return this.tokenizeMultilineComment(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理字符串
|
||||||
|
if (char === '"' || char === "'") {
|
||||||
|
return this.tokenizeString(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理Here-String
|
||||||
|
if (char === '@' && (this.peek() === '"' || this.peek() === "'")) {
|
||||||
|
return this.tokenizeHereString(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理哈希表字面量 @{
|
||||||
|
if (char === '@' && this.peek() === '{') {
|
||||||
|
this.advance(); // skip '@'
|
||||||
|
this.advance(); // skip '{'
|
||||||
|
return this.createToken(TokenType.LEFT_BRACE, '@{', startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理变量
|
||||||
|
if (char === '$') {
|
||||||
|
return this.tokenizeVariable(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数字
|
||||||
|
if (this.isDigit(char) || (char === '.' && this.isDigit(this.peek()))) {
|
||||||
|
return this.tokenizeNumber(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理操作符和分隔符
|
||||||
|
const operatorToken = this.tokenizeOperator(startPos, startLine, startColumn);
|
||||||
|
if (operatorToken) {
|
||||||
|
return operatorToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先处理PowerShell比较操作符(以-开头)
|
||||||
|
if (char === '-' && this.isIdentifierStart(this.peek())) {
|
||||||
|
const potentialOperator = this.peekPowerShellOperator();
|
||||||
|
if (potentialOperator) {
|
||||||
|
return this.tokenizePowerShellOperator(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
// 如果不是操作符,可能是参数
|
||||||
|
return this.tokenizeParameter(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标识符(包括cmdlet和关键字)
|
||||||
|
if (this.isIdentifierStart(char)) {
|
||||||
|
return this.tokenizeIdentifier(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理PowerShell特殊字符
|
||||||
|
if (char === '?') {
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.OPERATOR, char, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理独立的减号(可能是负数或减法)
|
||||||
|
if (char === '-') {
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理其他可能的特殊字符,作为标识符处理而不是未知字符
|
||||||
|
if (this.isPrintableChar(char)) {
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.IDENTIFIER, char, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 真正的未知字符(非打印字符等)
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.UNKNOWN, char, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeComment(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '';
|
||||||
|
while (this.position < this.code.length && this.code[this.position] !== '\n') {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
return this.createToken(TokenType.COMMENT, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeMultilineComment(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '';
|
||||||
|
this.advance(); // skip '<'
|
||||||
|
this.advance(); // skip '#'
|
||||||
|
value += '<#';
|
||||||
|
|
||||||
|
while (this.position < this.code.length - 1) {
|
||||||
|
if (this.code[this.position] === '#' && this.code[this.position + 1] === '>') {
|
||||||
|
value += '#>';
|
||||||
|
this.advance();
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.MULTILINE_COMMENT, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeString(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
const quote = this.code[this.position];
|
||||||
|
let value = quote;
|
||||||
|
this.advance();
|
||||||
|
|
||||||
|
while (this.position < this.code.length) {
|
||||||
|
const char = this.code[this.position];
|
||||||
|
value += char;
|
||||||
|
|
||||||
|
if (char === quote) {
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理转义字符
|
||||||
|
if (char === '`' && quote === '"') {
|
||||||
|
this.advance();
|
||||||
|
if (this.position < this.code.length) {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.STRING, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeHereString(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
const quote = this.code[this.position + 1]; // " or '
|
||||||
|
let value = `@${quote}`;
|
||||||
|
this.advance(); // skip '@'
|
||||||
|
this.advance(); // skip quote
|
||||||
|
|
||||||
|
while (this.position < this.code.length - 1) {
|
||||||
|
if (this.code[this.position] === quote && this.code[this.position + 1] === '@') {
|
||||||
|
value += `${quote}@`;
|
||||||
|
this.advance();
|
||||||
|
this.advance();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.HERE_STRING, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeVariable(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '$';
|
||||||
|
this.advance(); // skip '$'
|
||||||
|
|
||||||
|
// 处理特殊变量如 $_, $$, $^
|
||||||
|
const specialVars = ['_', '$', '^', '?'];
|
||||||
|
if (specialVars.includes(this.code[this.position])) {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理大括号变量 ${variable name}
|
||||||
|
if (this.code[this.position] === '{') {
|
||||||
|
this.advance(); // skip '{'
|
||||||
|
value += '{';
|
||||||
|
while (this.position < this.code.length && this.code[this.position] !== '}') {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
if (this.position < this.code.length) {
|
||||||
|
value += '}';
|
||||||
|
this.advance(); // skip '}'
|
||||||
|
}
|
||||||
|
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通变量名
|
||||||
|
while (this.position < this.code.length && this.isIdentifierChar(this.code[this.position])) {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.VARIABLE, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeNumber(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '';
|
||||||
|
let hasDecimal = false;
|
||||||
|
|
||||||
|
while (this.position < this.code.length) {
|
||||||
|
const char = this.code[this.position];
|
||||||
|
|
||||||
|
if (this.isDigit(char)) {
|
||||||
|
value += char;
|
||||||
|
this.advance();
|
||||||
|
} else if (char === '.' && !hasDecimal && this.isDigit(this.peek())) {
|
||||||
|
hasDecimal = true;
|
||||||
|
value += char;
|
||||||
|
this.advance();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有PowerShell数字单位后缀(KB, MB, GB, TB, PB)
|
||||||
|
const unitPattern = /^(KB|MB|GB|TB|PB)/i;
|
||||||
|
const remainingCode = this.code.substring(this.position);
|
||||||
|
const unitMatch = remainingCode.match(unitPattern);
|
||||||
|
|
||||||
|
if (unitMatch) {
|
||||||
|
value += unitMatch[0]; // 使用 [0] 获取完整匹配
|
||||||
|
// 移动position到单位后面
|
||||||
|
for (let i = 0; i < unitMatch[0].length; i++) {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.NUMBER, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeOperator(startPos: number, startLine: number, startColumn: number): Token | null {
|
||||||
|
const char = this.code[this.position];
|
||||||
|
|
||||||
|
// 双字符操作符
|
||||||
|
const twoChar = this.code.substring(this.position, this.position + 2);
|
||||||
|
const doubleOperators = ['==', '!=', '<=', '>=', '++', '--', '+=', '-=', '*=', '/=', '%='];
|
||||||
|
|
||||||
|
if (doubleOperators.includes(twoChar)) {
|
||||||
|
this.advance();
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.OPERATOR, twoChar, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单字符操作符
|
||||||
|
switch (char) {
|
||||||
|
case '=':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.ASSIGNMENT, char, startPos, startLine, startColumn);
|
||||||
|
case '+':
|
||||||
|
case '*':
|
||||||
|
case '/':
|
||||||
|
case '%':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.ARITHMETIC, char, startPos, startLine, startColumn);
|
||||||
|
case '-':
|
||||||
|
// 不在这里处理'-',让PowerShell操作符检查优先处理
|
||||||
|
return null;
|
||||||
|
case '(':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.LEFT_PAREN, char, startPos, startLine, startColumn);
|
||||||
|
case ')':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.RIGHT_PAREN, char, startPos, startLine, startColumn);
|
||||||
|
case '{':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.LEFT_BRACE, char, startPos, startLine, startColumn);
|
||||||
|
case '}':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.RIGHT_BRACE, char, startPos, startLine, startColumn);
|
||||||
|
case '[':
|
||||||
|
// 检查是否是PowerShell类型转换 [type]
|
||||||
|
const typePattern = this.peekTypeConversion();
|
||||||
|
if (typePattern) {
|
||||||
|
return this.tokenizeTypeConversion(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.LEFT_BRACKET, char, startPos, startLine, startColumn);
|
||||||
|
case ']':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.RIGHT_BRACKET, char, startPos, startLine, startColumn);
|
||||||
|
case ';':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.SEMICOLON, char, startPos, startLine, startColumn);
|
||||||
|
case ',':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.COMMA, char, startPos, startLine, startColumn);
|
||||||
|
case '.':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.DOT, char, startPos, startLine, startColumn);
|
||||||
|
case '|':
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.PIPE, char, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeIdentifier(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
// 改进的标识符识别,支持PowerShell cmdlet格式(动词-名词)
|
||||||
|
while (this.position < this.code.length) {
|
||||||
|
const char = this.code[this.position];
|
||||||
|
|
||||||
|
if (this.isIdentifierChar(char)) {
|
||||||
|
value += char;
|
||||||
|
this.advance();
|
||||||
|
} else if (char === '-' && value.length > 0 && this.isIdentifierStart(this.peek())) {
|
||||||
|
// 检查是否是cmdlet格式(动词-名词)
|
||||||
|
const nextPart = this.peekIdentifierPart();
|
||||||
|
if (nextPart && !this.isPowerShellOperator('-' + nextPart)) {
|
||||||
|
// 这是cmdlet名字的一部分,继续
|
||||||
|
value += char;
|
||||||
|
this.advance();
|
||||||
|
} else {
|
||||||
|
// 这可能是操作符,停止
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerValue = value.toLowerCase();
|
||||||
|
|
||||||
|
// 检查是否是关键字
|
||||||
|
if (this.keywords.has(lowerValue)) {
|
||||||
|
return this.createToken(this.getKeywordTokenType(lowerValue), value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是函数(以动词-名词格式)
|
||||||
|
if (this.isCmdletName(value)) {
|
||||||
|
return this.createToken(TokenType.CMDLET, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.IDENTIFIER, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeParameter(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
while (this.position < this.code.length && (this.isIdentifierChar(this.code[this.position]) || this.code[this.position] === '-')) {
|
||||||
|
value += this.code[this.position];
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerValue = value.toLowerCase();
|
||||||
|
|
||||||
|
// 检查是否是比较操作符
|
||||||
|
if (this.comparisonOperators.has(lowerValue)) {
|
||||||
|
return this.createToken(TokenType.COMPARISON, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是逻辑操作符
|
||||||
|
if (this.logicalOperators.has(lowerValue)) {
|
||||||
|
return this.createToken(TokenType.LOGICAL, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.PARAMETER, value, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getKeywordTokenType(keyword: string): TokenType {
|
||||||
|
switch (keyword) {
|
||||||
|
case 'if': return TokenType.IF;
|
||||||
|
case 'else': return TokenType.ELSE;
|
||||||
|
case 'elseif': return TokenType.ELSEIF;
|
||||||
|
case 'while': return TokenType.WHILE;
|
||||||
|
case 'for': return TokenType.FOR;
|
||||||
|
case 'foreach': return TokenType.FOREACH;
|
||||||
|
case 'switch': return TokenType.SWITCH;
|
||||||
|
case 'try': return TokenType.TRY;
|
||||||
|
case 'catch': return TokenType.CATCH;
|
||||||
|
case 'finally': return TokenType.FINALLY;
|
||||||
|
case 'function': return TokenType.FUNCTION;
|
||||||
|
default: return TokenType.KEYWORD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCmdletName(name: string): boolean {
|
||||||
|
// PowerShell cmdlet通常遵循 Verb-Noun 格式,可能包含多个连字符
|
||||||
|
const verbNounPattern = /^[A-Za-z]+(-[A-Za-z]+)+$/;
|
||||||
|
return verbNounPattern.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private peekPowerShellOperator(): string | null {
|
||||||
|
// 检查是否是PowerShell比较或逻辑操作符
|
||||||
|
const operatorPatterns = [
|
||||||
|
'-eq', '-ne', '-lt', '-le', '-gt', '-ge',
|
||||||
|
'-like', '-notlike', '-match', '-notmatch',
|
||||||
|
'-contains', '-notcontains', '-in', '-notin',
|
||||||
|
'-is', '-isnot', '-as',
|
||||||
|
'-and', '-or', '-not', '-xor',
|
||||||
|
'-band', '-bor', '-bxor', '-bnot'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const op of operatorPatterns) {
|
||||||
|
if (this.matchesOperator(op)) {
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchesOperator(operator: string): boolean {
|
||||||
|
if (this.position + operator.length > this.code.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const substr = this.code.substring(this.position, this.position + operator.length);
|
||||||
|
if (substr.toLowerCase() !== operator.toLowerCase()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保操作符后面不是字母数字字符(避免匹配部分单词)
|
||||||
|
const nextChar = this.position + operator.length < this.code.length
|
||||||
|
? this.code[this.position + operator.length]
|
||||||
|
: ' ';
|
||||||
|
return !this.isIdentifierChar(nextChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizePowerShellOperator(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
const operator = this.peekPowerShellOperator();
|
||||||
|
if (!operator) {
|
||||||
|
// 如果不是操作符,作为参数处理
|
||||||
|
return this.tokenizeParameter(startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费操作符字符
|
||||||
|
for (let i = 0; i < operator.length; i++) {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerOp = operator.toLowerCase();
|
||||||
|
|
||||||
|
// 确定操作符类型
|
||||||
|
if (this.comparisonOperators.has(lowerOp)) {
|
||||||
|
return this.createToken(TokenType.COMPARISON, operator, startPos, startLine, startColumn);
|
||||||
|
} else if (this.logicalOperators.has(lowerOp)) {
|
||||||
|
return this.createToken(TokenType.LOGICAL, operator, startPos, startLine, startColumn);
|
||||||
|
} else {
|
||||||
|
return this.createToken(TokenType.OPERATOR, operator, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private peekIdentifierPart(): string | null {
|
||||||
|
if (this.position + 1 >= this.code.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
let pos = this.position + 1; // 跳过连字符
|
||||||
|
|
||||||
|
while (pos < this.code.length && this.isIdentifierChar(this.code[pos])) {
|
||||||
|
result += this.code[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.length > 0 ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPowerShellOperator(text: string): boolean {
|
||||||
|
const lowerText = text.toLowerCase();
|
||||||
|
return this.comparisonOperators.has(lowerText) || this.logicalOperators.has(lowerText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private peekTypeConversion(): string | null {
|
||||||
|
// 检查是否是PowerShell类型转换,如 [int], [string], [datetime] 等
|
||||||
|
if (this.code[this.position] !== '[') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = this.position + 1; // 跳过 '['
|
||||||
|
let typeContent = '';
|
||||||
|
|
||||||
|
// 查找类型名称
|
||||||
|
while (pos < this.code.length && this.code[pos] !== ']') {
|
||||||
|
typeContent += this.code[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= this.code.length || this.code[pos] !== ']') {
|
||||||
|
return null; // 没有找到匹配的 ']'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是有效的PowerShell类型
|
||||||
|
const validTypes = [
|
||||||
|
'int', 'int32', 'int64', 'string', 'bool', 'boolean', 'char', 'byte',
|
||||||
|
'double', 'float', 'decimal', 'long', 'short', 'datetime', 'timespan',
|
||||||
|
'array', 'hashtable', 'object', 'psobject', 'xml', 'scriptblock',
|
||||||
|
'guid', 'uri', 'version', 'regex', 'mailaddress', 'ipaddress'
|
||||||
|
];
|
||||||
|
|
||||||
|
const lowerType = typeContent.toLowerCase().trim();
|
||||||
|
if (validTypes.includes(lowerType) || lowerType.includes('.')) {
|
||||||
|
return `[${typeContent}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private tokenizeTypeConversion(startPos: number, startLine: number, startColumn: number): Token {
|
||||||
|
const typeConversion = this.peekTypeConversion();
|
||||||
|
if (!typeConversion) {
|
||||||
|
// 这不应该发生,但作为安全措施
|
||||||
|
this.advance();
|
||||||
|
return this.createToken(TokenType.LEFT_BRACKET, '[', startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消费整个类型转换
|
||||||
|
for (let i = 0; i < typeConversion.length; i++) {
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createToken(TokenType.IDENTIFIER, typeConversion, startPos, startLine, startColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isIdentifierStart(char: string): boolean {
|
||||||
|
return /[a-zA-Z_]/.test(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isIdentifierChar(char: string): boolean {
|
||||||
|
return /[a-zA-Z0-9_]/.test(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isDigit(char: string): boolean {
|
||||||
|
return char >= '0' && char <= '9';
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPrintableChar(char: string): boolean {
|
||||||
|
// 检查是否为可打印字符(非控制字符)
|
||||||
|
const charCode = char.charCodeAt(0);
|
||||||
|
return charCode >= 32 && charCode <= 126;
|
||||||
|
}
|
||||||
|
|
||||||
|
private advance(): void {
|
||||||
|
if (this.position < this.code.length) {
|
||||||
|
if (this.code[this.position] === '\n') {
|
||||||
|
this.line++;
|
||||||
|
this.column = 1;
|
||||||
|
} else {
|
||||||
|
this.column++;
|
||||||
|
}
|
||||||
|
this.position++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private peek(): string {
|
||||||
|
return this.position + 1 < this.code.length ? this.code[this.position + 1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private skipWhitespace(): void {
|
||||||
|
while (this.position < this.code.length) {
|
||||||
|
const char = this.code[this.position];
|
||||||
|
if (char === ' ' || char === '\t' || char === '\r') {
|
||||||
|
this.advance();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createToken(type: TokenType, value: string, startPos: number, line: number, column: number): Token {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
line,
|
||||||
|
column,
|
||||||
|
startIndex: startPos,
|
||||||
|
endIndex: this.position
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,787 +0,0 @@
|
|||||||
/**
|
|
||||||
* PowerShell 代码解析器和格式化器
|
|
||||||
*/
|
|
||||||
|
|
||||||
// PowerShell AST节点类型
|
|
||||||
interface PowerShellAstNode {
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
start?: number;
|
|
||||||
end?: number;
|
|
||||||
parent?: PowerShellAstNode;
|
|
||||||
extent?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析器函数类型
|
|
||||||
type PowerShellParserResult = PowerShellAstNode | PowerShellAstNode[];
|
|
||||||
|
|
||||||
// PowerShell格式化选项
|
|
||||||
export interface PowerShellFormatterOptions {
|
|
||||||
/** 缩进大小,默认为4 */
|
|
||||||
indentSize?: number;
|
|
||||||
/** 使用制表符还是空格,默认为空格 */
|
|
||||||
useTabsForIndentation?: boolean;
|
|
||||||
/** 行最大长度,默认为120 */
|
|
||||||
printWidth?: number;
|
|
||||||
/** 是否在操作符周围添加空格,默认为true */
|
|
||||||
spaceAroundOperators?: boolean;
|
|
||||||
/** 是否格式化注释,默认为true */
|
|
||||||
formatComments?: boolean;
|
|
||||||
/** 是否去除多余的空行,默认为true */
|
|
||||||
removeExtraBlankLines?: boolean;
|
|
||||||
/** 是否格式化管道符,默认为true */
|
|
||||||
formatPipelines?: boolean;
|
|
||||||
/** 是否格式化括号内空格,默认为true */
|
|
||||||
formatParentheses?: boolean;
|
|
||||||
/** 是否格式化数组和哈希表,默认为true */
|
|
||||||
formatArraysAndHashtables?: boolean;
|
|
||||||
/** 最大连续空行数,默认为1 */
|
|
||||||
maxConsecutiveEmptyLines?: number;
|
|
||||||
/** 是否在代码块前后添加空行,默认为true */
|
|
||||||
addBlankLinesAroundBlocks?: boolean;
|
|
||||||
/** 是否格式化长行(自动换行),默认为true */
|
|
||||||
formatLongLines?: boolean;
|
|
||||||
/** 是否格式化函数定义,默认为true */
|
|
||||||
formatFunctionDefinitions?: boolean;
|
|
||||||
/** 是否格式化PowerShell特有语法(switch、try-catch、param等),默认为true */
|
|
||||||
formatPowerShellSyntax?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PowerShell代码格式化器 - 修复版本
|
|
||||||
*/
|
|
||||||
class PowerShellFormatter {
|
|
||||||
private options: Required<PowerShellFormatterOptions>;
|
|
||||||
|
|
||||||
constructor(options: PowerShellFormatterOptions = {}) {
|
|
||||||
this.options = {
|
|
||||||
indentSize: options.indentSize ?? 4,
|
|
||||||
useTabsForIndentation: options.useTabsForIndentation ?? false,
|
|
||||||
printWidth: options.printWidth ?? 120,
|
|
||||||
spaceAroundOperators: options.spaceAroundOperators ?? true,
|
|
||||||
formatComments: options.formatComments ?? true,
|
|
||||||
removeExtraBlankLines: options.removeExtraBlankLines ?? true,
|
|
||||||
formatPipelines: options.formatPipelines ?? true,
|
|
||||||
formatParentheses: options.formatParentheses ?? true,
|
|
||||||
formatArraysAndHashtables: options.formatArraysAndHashtables ?? true,
|
|
||||||
maxConsecutiveEmptyLines: options.maxConsecutiveEmptyLines ?? 1,
|
|
||||||
addBlankLinesAroundBlocks: options.addBlankLinesAroundBlocks ?? true,
|
|
||||||
formatLongLines: options.formatLongLines ?? true,
|
|
||||||
formatFunctionDefinitions: options.formatFunctionDefinitions ?? true,
|
|
||||||
formatPowerShellSyntax: options.formatPowerShellSyntax ?? true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化PowerShell代码
|
|
||||||
*/
|
|
||||||
format(code: string): string {
|
|
||||||
if (!code || code.trim().length === 0) {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
let formattedLines = this.formatLines(lines);
|
|
||||||
|
|
||||||
// 处理多余空行
|
|
||||||
if (this.options.removeExtraBlankLines) {
|
|
||||||
formattedLines = this.removeExtraBlankLines(formattedLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在代码块前后添加适当的空行
|
|
||||||
if (this.options.addBlankLinesAroundBlocks) {
|
|
||||||
formattedLines = this.addBlankLinesAroundBlocks(formattedLines);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedLines.join('\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('PowerShell formatting failed:', error);
|
|
||||||
return code; // 返回原始代码
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatLines(lines: string[]): string[] {
|
|
||||||
const result: string[] = [];
|
|
||||||
let indentLevel = 0;
|
|
||||||
let inMultiLineComment = false;
|
|
||||||
let inHereString = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
|
|
||||||
// 检查 Here-String (@"..."@ 或 @'...'@)
|
|
||||||
if (trimmedLine.startsWith('@"') || trimmedLine.startsWith("@'")) {
|
|
||||||
if (!trimmedLine.endsWith('"@') && !trimmedLine.endsWith("'@")) {
|
|
||||||
inHereString = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (inHereString && (trimmedLine.endsWith('"@') || trimmedLine.endsWith("'@"))) {
|
|
||||||
inHereString = false;
|
|
||||||
result.push(line); // Here-String 结束行保持原样
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在 Here-String 内部不处理
|
|
||||||
if (inHereString) {
|
|
||||||
result.push(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查多行注释
|
|
||||||
if (trimmedLine.includes('<#')) {
|
|
||||||
inMultiLineComment = true;
|
|
||||||
}
|
|
||||||
if (trimmedLine.includes('#>')) {
|
|
||||||
inMultiLineComment = false;
|
|
||||||
result.push(this.getIndent(indentLevel) + trimmedLine);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inMultiLineComment) {
|
|
||||||
result.push(this.getIndent(indentLevel) + trimmedLine);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理空行
|
|
||||||
if (trimmedLine.length === 0) {
|
|
||||||
result.push('');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要减少缩进级别
|
|
||||||
if (this.shouldDecreaseIndent(trimmedLine)) {
|
|
||||||
indentLevel = Math.max(0, indentLevel - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化当前行
|
|
||||||
const formattedLine = this.formatLine(trimmedLine);
|
|
||||||
result.push(this.getIndent(indentLevel) + formattedLine);
|
|
||||||
|
|
||||||
// 检查是否需要增加缩进级别
|
|
||||||
if (this.shouldIncreaseIndent(trimmedLine)) {
|
|
||||||
indentLevel++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatLine(line: string): string {
|
|
||||||
let formatted = line;
|
|
||||||
|
|
||||||
// 处理操作符周围的空格
|
|
||||||
if (this.options.spaceAroundOperators) {
|
|
||||||
formatted = this.addSpacesAroundOperators(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化注释
|
|
||||||
if (this.options.formatComments) {
|
|
||||||
formatted = this.formatComment(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化管道符
|
|
||||||
if (this.options.formatPipelines) {
|
|
||||||
formatted = this.formatPipelines(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化括号
|
|
||||||
if (this.options.formatParentheses) {
|
|
||||||
formatted = this.formatParentheses(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化数组和哈希表
|
|
||||||
if (this.options.formatArraysAndHashtables) {
|
|
||||||
formatted = this.formatArraysAndHashtables(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化函数定义
|
|
||||||
if (this.options.formatFunctionDefinitions) {
|
|
||||||
formatted = this.formatFunctionDefinitions(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化PowerShell特有语法
|
|
||||||
if (this.options.formatPowerShellSyntax) {
|
|
||||||
formatted = this.formatPowerShellSyntax(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化长行(如果需要)
|
|
||||||
if (this.options.formatLongLines && formatted.length > this.options.printWidth) {
|
|
||||||
formatted = this.formatLongLine(formatted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addSpacesAroundOperators(line: string): string {
|
|
||||||
// 定义PowerShell操作符映射 - 修复版本
|
|
||||||
const operatorMappings = [
|
|
||||||
// 赋值操作符
|
|
||||||
{ pattern: /\s*=\s*/g, replacement: ' = ' },
|
|
||||||
{ pattern: /\s*\+=\s*/g, replacement: ' += ' },
|
|
||||||
{ pattern: /\s*-=\s*/g, replacement: ' -= ' },
|
|
||||||
{ pattern: /\s*\*=\s*/g, replacement: ' *= ' },
|
|
||||||
{ pattern: /\s*\/=\s*/g, replacement: ' /= ' },
|
|
||||||
{ pattern: /\s*%=\s*/g, replacement: ' %= ' },
|
|
||||||
|
|
||||||
// 算术操作符 (避免与参数冲突)
|
|
||||||
{ pattern: /(\w)\s*\+\s*(\w)/g, replacement: '$1 + $2' },
|
|
||||||
{ pattern: /(\w)\s*-\s*(\w)/g, replacement: '$1 - $2' },
|
|
||||||
{ pattern: /(\w)\s*\*\s*(\w)/g, replacement: '$1 * $2' },
|
|
||||||
{ pattern: /(\w)\s*\/\s*(\w)/g, replacement: '$1 / $2' },
|
|
||||||
{ pattern: /(\w)\s*%\s*(\w)/g, replacement: '$1 % $2' },
|
|
||||||
|
|
||||||
// 比较操作符
|
|
||||||
{ pattern: /\s*-eq\s*/g, replacement: ' -eq ' },
|
|
||||||
{ pattern: /\s*-ne\s*/g, replacement: ' -ne ' },
|
|
||||||
{ pattern: /\s*-lt\s*/g, replacement: ' -lt ' },
|
|
||||||
{ pattern: /\s*-le\s*/g, replacement: ' -le ' },
|
|
||||||
{ pattern: /\s*-gt\s*/g, replacement: ' -gt ' },
|
|
||||||
{ pattern: /\s*-ge\s*/g, replacement: ' -ge ' },
|
|
||||||
{ pattern: /\s*-like\s*/g, replacement: ' -like ' },
|
|
||||||
{ pattern: /\s*-notlike\s*/g, replacement: ' -notlike ' },
|
|
||||||
{ pattern: /\s*-match\s*/g, replacement: ' -match ' },
|
|
||||||
{ pattern: /\s*-notmatch\s*/g, replacement: ' -notmatch ' },
|
|
||||||
{ pattern: /\s*-contains\s*/g, replacement: ' -contains ' },
|
|
||||||
{ pattern: /\s*-notcontains\s*/g, replacement: ' -notcontains ' },
|
|
||||||
{ pattern: /\s*-in\s*/g, replacement: ' -in ' },
|
|
||||||
{ pattern: /\s*-notin\s*/g, replacement: ' -notin ' },
|
|
||||||
|
|
||||||
// 逻辑操作符
|
|
||||||
{ pattern: /\s*-and\s*/g, replacement: ' -and ' },
|
|
||||||
{ pattern: /\s*-or\s*/g, replacement: ' -or ' },
|
|
||||||
{ pattern: /\s*-not\s*/g, replacement: ' -not ' },
|
|
||||||
{ pattern: /\s*-xor\s*/g, replacement: ' -xor ' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 先保护字符串字面量
|
|
||||||
const strings: string[] = [];
|
|
||||||
let stringIndex = 0;
|
|
||||||
|
|
||||||
// 保护双引号字符串
|
|
||||||
result = result.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, (match) => {
|
|
||||||
const placeholder = `__STRING_${stringIndex++}__`;
|
|
||||||
strings.push(match);
|
|
||||||
return placeholder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保护单引号字符串
|
|
||||||
result = result.replace(/'([^'\\]*(\\.[^'\\]*)*)'/g, (match) => {
|
|
||||||
const placeholder = `__STRING_${stringIndex++}__`;
|
|
||||||
strings.push(match);
|
|
||||||
return placeholder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 应用操作符格式化
|
|
||||||
operatorMappings.forEach(({ pattern, replacement }) => {
|
|
||||||
result = result.replace(pattern, replacement);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清理多余的空格,但不要合并所有空格
|
|
||||||
result = result.replace(/\s{2,}/g, ' ');
|
|
||||||
|
|
||||||
// 还原字符串
|
|
||||||
strings.forEach((str, index) => {
|
|
||||||
result = result.replace(`__STRING_${index}__`, str);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeExtraBlankLines(lines: string[]): string[] {
|
|
||||||
const result: string[] = [];
|
|
||||||
let consecutiveEmptyLines = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
const isEmptyLine = line.trim() === '';
|
|
||||||
|
|
||||||
if (isEmptyLine) {
|
|
||||||
consecutiveEmptyLines++;
|
|
||||||
// 只保留指定数量的连续空行
|
|
||||||
if (consecutiveEmptyLines <= this.options.maxConsecutiveEmptyLines) {
|
|
||||||
result.push(line);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
consecutiveEmptyLines = 0;
|
|
||||||
result.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 去除文件开头和结尾的空行
|
|
||||||
while (result.length > 0 && result[0].trim() === '') {
|
|
||||||
result.shift();
|
|
||||||
}
|
|
||||||
while (result.length > 0 && result[result.length - 1].trim() === '') {
|
|
||||||
result.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatPipelines(line: string): string {
|
|
||||||
// 格式化管道符 |
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 保护字符串字面量
|
|
||||||
const strings: string[] = [];
|
|
||||||
let stringIndex = 0;
|
|
||||||
|
|
||||||
result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => {
|
|
||||||
const placeholder = `__STRING_${stringIndex++}__`;
|
|
||||||
strings.push(match);
|
|
||||||
return placeholder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 格式化管道符,确保前后有空格
|
|
||||||
result = result.replace(/\s*\|\s*/g, ' | ');
|
|
||||||
|
|
||||||
// 还原字符串
|
|
||||||
strings.forEach((str, index) => {
|
|
||||||
result = result.replace(`__STRING_${index}__`, str);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatParentheses(line: string): string {
|
|
||||||
// 格式化括号内的空格
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 保护字符串字面量
|
|
||||||
const strings: string[] = [];
|
|
||||||
let stringIndex = 0;
|
|
||||||
|
|
||||||
result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => {
|
|
||||||
const placeholder = `__STRING_${stringIndex++}__`;
|
|
||||||
strings.push(match);
|
|
||||||
return placeholder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 格式化括号:( 后和 ) 前不要多余空格,但参数之间要有空格
|
|
||||||
result = result.replace(/\(\s+/g, '(');
|
|
||||||
result = result.replace(/\s+\)/g, ')');
|
|
||||||
|
|
||||||
// 格式化逗号:逗号后加空格
|
|
||||||
result = result.replace(/,\s*/g, ', ');
|
|
||||||
|
|
||||||
// 格式化分号:分号后加空格
|
|
||||||
result = result.replace(/;\s*/g, '; ');
|
|
||||||
|
|
||||||
// 还原字符串
|
|
||||||
strings.forEach((str, index) => {
|
|
||||||
result = result.replace(`__STRING_${index}__`, str);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatArraysAndHashtables(line: string): string {
|
|
||||||
// 格式化数组 @() 和哈希表 @{}
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 保护字符串字面量
|
|
||||||
const strings: string[] = [];
|
|
||||||
let stringIndex = 0;
|
|
||||||
|
|
||||||
result = result.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, (match) => {
|
|
||||||
const placeholder = `__STRING_${stringIndex++}__`;
|
|
||||||
strings.push(match);
|
|
||||||
return placeholder;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 格式化数组符号
|
|
||||||
result = result.replace(/@\(\s*/g, '@(');
|
|
||||||
result = result.replace(/\s*\)/g, ')');
|
|
||||||
|
|
||||||
// 格式化哈希表符号
|
|
||||||
result = result.replace(/@\{\s*/g, '@{');
|
|
||||||
result = result.replace(/\s*\}/g, '}');
|
|
||||||
|
|
||||||
// 格式化方括号
|
|
||||||
result = result.replace(/\[\s+/g, '[');
|
|
||||||
result = result.replace(/\s+\]/g, ']');
|
|
||||||
|
|
||||||
// 还原字符串
|
|
||||||
strings.forEach((str, index) => {
|
|
||||||
result = result.replace(`__STRING_${index}__`, str);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private addBlankLinesAroundBlocks(lines: string[]): string[] {
|
|
||||||
const result: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
const previousLine = i > 0 ? lines[i - 1].trim() : '';
|
|
||||||
const nextLine = i < lines.length - 1 ? lines[i + 1].trim() : '';
|
|
||||||
|
|
||||||
// 在函数定义前添加空行(除非是文件开头或前面已经有空行)
|
|
||||||
if (this.isFunctionDefinition(trimmedLine)) {
|
|
||||||
if (i > 0 && previousLine !== '' && result.length > 0) {
|
|
||||||
result.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在控制结构前添加空行(if, while, for, foreach, switch等)
|
|
||||||
if (this.isControlStructure(trimmedLine)) {
|
|
||||||
if (i > 0 && previousLine !== '' && !this.isElseOrElseIf(trimmedLine) && result.length > 0) {
|
|
||||||
result.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(line);
|
|
||||||
|
|
||||||
// 在函数定义后的右大括号后添加空行
|
|
||||||
if (trimmedLine === '}' && this.isPreviousLineFunctionEnd(lines, i)) {
|
|
||||||
if (i < lines.length - 1 && nextLine !== '') {
|
|
||||||
result.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatFunctionDefinitions(line: string): string {
|
|
||||||
// 格式化函数定义
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 函数定义的模式匹配
|
|
||||||
const functionPattern = /^(\s*)(function\s+)([^\s\(]+)(\s*)(\(.*?\))(\s*)(\{?)(.*)$/i;
|
|
||||||
const match = result.match(functionPattern);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [, indent, funcKeyword, funcName, , params, , openBrace, rest] = match;
|
|
||||||
|
|
||||||
// 标准化函数定义格式:function FunctionName(Parameters) {
|
|
||||||
if (openBrace === '{' && rest.trim() === '') {
|
|
||||||
// 单独一行的开括号
|
|
||||||
result = `${indent}${funcKeyword}${funcName}${params} {`;
|
|
||||||
} else if (openBrace === '' && rest.trim().startsWith('{')) {
|
|
||||||
// 括号在下一行,移到同一行
|
|
||||||
result = `${indent}${funcKeyword}${funcName}${params} {`;
|
|
||||||
} else {
|
|
||||||
// 标准格式化
|
|
||||||
result = `${indent}${funcKeyword}${funcName}${params} {`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatLongLine(line: string): string {
|
|
||||||
// 处理长行,在适当的位置换行
|
|
||||||
if (line.length <= this.options.printWidth) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indent = line.match(/^\s*/)?.[0] || '';
|
|
||||||
const content = line.trim();
|
|
||||||
|
|
||||||
// 查找可以换行的位置:管道符、逗号、操作符等
|
|
||||||
const breakPoints = [
|
|
||||||
{ char: ' | ', priority: 1 },
|
|
||||||
{ char: ', ', priority: 2 },
|
|
||||||
{ char: ' -and ', priority: 3 },
|
|
||||||
{ char: ' -or ', priority: 3 },
|
|
||||||
{ char: ' = ', priority: 4 },
|
|
||||||
{ char: ' + ', priority: 5 },
|
|
||||||
{ char: ' -', priority: 6 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 寻找最佳换行点
|
|
||||||
for (const breakPoint of breakPoints) {
|
|
||||||
const index = content.lastIndexOf(breakPoint.char, this.options.printWidth - indent.length);
|
|
||||||
if (index > 0) {
|
|
||||||
const firstPart = content.substring(0, index + breakPoint.char.length).trim();
|
|
||||||
const secondPart = content.substring(index + breakPoint.char.length).trim();
|
|
||||||
|
|
||||||
if (secondPart.length > 0) {
|
|
||||||
const continuationIndent = indent + ' '; // 额外缩进
|
|
||||||
return `${indent}${firstPart}\n${continuationIndent}${secondPart}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return line; // 如果找不到合适的断点,保持原样
|
|
||||||
}
|
|
||||||
|
|
||||||
private isFunctionDefinition(line: string): boolean {
|
|
||||||
return /^\s*function\s+\w+/i.test(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isControlStructure(line: string): boolean {
|
|
||||||
const controlKeywords = [
|
|
||||||
/^\s*if\s*\(/i,
|
|
||||||
/^\s*while\s*\(/i,
|
|
||||||
/^\s*for\s*\(/i,
|
|
||||||
/^\s*foreach\s*\(/i,
|
|
||||||
/^\s*switch\s*\(/i,
|
|
||||||
/^\s*try\s*\{?$/i,
|
|
||||||
/^\s*do\s*\{?$/i
|
|
||||||
];
|
|
||||||
|
|
||||||
return controlKeywords.some(pattern => pattern.test(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
private isElseOrElseIf(line: string): boolean {
|
|
||||||
return /^\s*(else|elseif)\b/i.test(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPreviousLineFunctionEnd(lines: string[], currentIndex: number): boolean {
|
|
||||||
// 检查是否是函数结束的大括号
|
|
||||||
let braceCount = 0;
|
|
||||||
let foundFunction = false;
|
|
||||||
|
|
||||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
||||||
const line = lines[i].trim();
|
|
||||||
|
|
||||||
if (line.includes('}')) {
|
|
||||||
braceCount += (line.match(/\}/g) || []).length;
|
|
||||||
}
|
|
||||||
if (line.includes('{')) {
|
|
||||||
braceCount -= (line.match(/\{/g) || []).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isFunctionDefinition(line)) {
|
|
||||||
foundFunction = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (braceCount < 0) break; // 遇到不匹配的括号
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundFunction && braceCount === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatPowerShellSyntax(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 格式化switch语句
|
|
||||||
result = this.formatSwitchStatement(result);
|
|
||||||
|
|
||||||
// 格式化try-catch-finally语句
|
|
||||||
result = this.formatTryCatchFinally(result);
|
|
||||||
|
|
||||||
// 格式化param块
|
|
||||||
result = this.formatParamBlock(result);
|
|
||||||
|
|
||||||
// 格式化PowerShell操作符和关键字
|
|
||||||
result = this.formatPowerShellKeywords(result);
|
|
||||||
|
|
||||||
// 格式化变量声明
|
|
||||||
result = this.formatVariableDeclarations(result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatSwitchStatement(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 格式化switch语句: switch ($variable) {
|
|
||||||
const switchPattern = /^(\s*)(switch)\s*(\(.*?\))\s*(\{?)(.*)$/i;
|
|
||||||
const match = result.match(switchPattern);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [, indent, switchKeyword, condition, brace, rest] = match;
|
|
||||||
result = `${indent}${switchKeyword} ${condition} {`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化switch case标签
|
|
||||||
if (/^\s*\{?\s*[^}]*\s*\{\s*$/.test(result) || /^\s*default\s*\{\s*$/.test(result)) {
|
|
||||||
result = result.replace(/\s*\{\s*$/, ' {');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatTryCatchFinally(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 格式化try语句
|
|
||||||
if (/^\s*try\s*\{?/i.test(result)) {
|
|
||||||
result = result.replace(/^(\s*)(try)\s*\{?(.*)$/i, '$1$2 {$3');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化catch语句
|
|
||||||
const catchPattern = /^(\s*)(catch)\s*(\[[^\]]*\])?\s*\{?(.*)$/i;
|
|
||||||
const catchMatch = result.match(catchPattern);
|
|
||||||
if (catchMatch) {
|
|
||||||
const [, indent, catchKeyword, exceptionType, rest] = catchMatch;
|
|
||||||
const exception = exceptionType || '';
|
|
||||||
result = `${indent}${catchKeyword}${exception} {${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化finally语句
|
|
||||||
if (/^\s*finally\s*\{?/i.test(result)) {
|
|
||||||
result = result.replace(/^(\s*)(finally)\s*\{?(.*)$/i, '$1$2 {$3');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatParamBlock(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 格式化param块开始
|
|
||||||
if (/^\s*param\s*\(/i.test(result)) {
|
|
||||||
result = result.replace(/^(\s*)(param)\s*\(/i, '$1$2(');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化参数属性
|
|
||||||
const paramAttributePattern = /^\s*(\[Parameter\([^\]]*\)\])\s*(.*)$/i;
|
|
||||||
const attrMatch = result.match(paramAttributePattern);
|
|
||||||
if (attrMatch) {
|
|
||||||
const [, attribute, rest] = attrMatch;
|
|
||||||
result = ` ${attribute}\n ${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatPowerShellKeywords(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// PowerShell关键字列表
|
|
||||||
const keywords = [
|
|
||||||
'begin', 'process', 'end', 'filter', 'class', 'enum',
|
|
||||||
'using', 'namespace', 'return', 'throw', 'break', 'continue',
|
|
||||||
'exit', 'param', 'dynamicparam', 'workflow', 'configuration'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 确保关键字后有适当的空格
|
|
||||||
keywords.forEach(keyword => {
|
|
||||||
const pattern = new RegExp(`\\b(${keyword})\\s*`, 'gi');
|
|
||||||
result = result.replace(pattern, `$1 `);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 特殊处理某些关键字
|
|
||||||
result = result.replace(/\breturn\s+/gi, 'return ');
|
|
||||||
result = result.replace(/\bthrow\s+/gi, 'throw ');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatVariableDeclarations(line: string): string {
|
|
||||||
let result = line;
|
|
||||||
|
|
||||||
// 格式化变量类型声明: [string]$variable = value
|
|
||||||
const typeVarPattern = /^(\s*)(\[[^\]]+\])\s*(\$\w+)\s*(=.*)?$/;
|
|
||||||
const match = result.match(typeVarPattern);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [, indent, type, variable, assignment] = match;
|
|
||||||
result = `${indent}${type}${variable}${assignment || ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatComment(line: string): string {
|
|
||||||
// 单行注释格式化
|
|
||||||
if (line.includes('#') && !line.includes('<#') && !line.includes('#>')) {
|
|
||||||
const commentIndex = line.indexOf('#');
|
|
||||||
const beforeComment = line.substring(0, commentIndex).trim();
|
|
||||||
const comment = line.substring(commentIndex).trim();
|
|
||||||
|
|
||||||
if (beforeComment.length > 0) {
|
|
||||||
// 确保注释前有适当的空格
|
|
||||||
return `${beforeComment} ${comment}`;
|
|
||||||
}
|
|
||||||
return comment;
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldIncreaseIndent(line: string): boolean {
|
|
||||||
// PowerShell 块开始模式
|
|
||||||
const blockStartPatterns = [
|
|
||||||
/\{\s*$/, // 单独的大括号
|
|
||||||
/\bif\s*\([^)]*\)\s*\{\s*$/i,
|
|
||||||
/\belseif\s*\([^)]*\)\s*\{\s*$/i,
|
|
||||||
/\belse\s*\{\s*$/i,
|
|
||||||
/\bwhile\s*\([^)]*\)\s*\{\s*$/i,
|
|
||||||
/\bfor\s*\([^)]*\)\s*\{\s*$/i,
|
|
||||||
/\bforeach\s*\([^)]*\)\s*\{\s*$/i,
|
|
||||||
/\bdo\s*\{\s*$/i,
|
|
||||||
/\btry\s*\{\s*$/i,
|
|
||||||
/\bcatch\s*(\[[^\]]*\])?\s*\{\s*$/i,
|
|
||||||
/\bfinally\s*\{\s*$/i,
|
|
||||||
/\bfunction\s+\w+.*\{\s*$/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
return blockStartPatterns.some(pattern => pattern.test(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldDecreaseIndent(line: string): boolean {
|
|
||||||
// PowerShell 块结束模式
|
|
||||||
const blockEndPatterns = [
|
|
||||||
/^\s*\}\s*$/, // 单独的右大括号
|
|
||||||
/^\s*\}\s*else/i,
|
|
||||||
/^\s*\}\s*elseif/i,
|
|
||||||
/^\s*\}\s*catch/i,
|
|
||||||
/^\s*\}\s*finally/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
return blockEndPatterns.some(pattern => pattern.test(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
private getIndent(level: number): string {
|
|
||||||
if (level <= 0) return '';
|
|
||||||
const indentChar = this.options.useTabsForIndentation ? '\t' : ' ';
|
|
||||||
const indentSize = this.options.useTabsForIndentation ? 1 : this.options.indentSize;
|
|
||||||
return indentChar.repeat(level * indentSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 便捷的格式化函数
|
|
||||||
*/
|
|
||||||
export function formatPowerShellCode(
|
|
||||||
code: string,
|
|
||||||
options?: PowerShellFormatterOptions
|
|
||||||
): string {
|
|
||||||
const formatter = new PowerShellFormatter(options);
|
|
||||||
return formatter.format(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PowerShell代码解析器
|
|
||||||
*/
|
|
||||||
const parser = (scriptContent: string): Promise<PowerShellParserResult> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
try {
|
|
||||||
const astNode: PowerShellAstNode = {
|
|
||||||
type: 'ScriptBlockAst',
|
|
||||||
value: scriptContent,
|
|
||||||
start: 0,
|
|
||||||
end: scriptContent.length
|
|
||||||
};
|
|
||||||
|
|
||||||
resolve(astNode);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('PowerShell parsing fallback used:', error);
|
|
||||||
resolve({
|
|
||||||
type: 'ScriptBlockAst',
|
|
||||||
value: scriptContent,
|
|
||||||
start: 0,
|
|
||||||
end: scriptContent.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prettier解析函数
|
|
||||||
*/
|
|
||||||
const parse = async (scriptContent: string, parsers?: any, opts?: any): Promise<PowerShellParserResult> => {
|
|
||||||
return await parser(scriptContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parse;
|
|
||||||
821
frontend/src/common/prettier/plugins/powershell/parser.ts
Normal file
821
frontend/src/common/prettier/plugins/powershell/parser.ts
Normal file
@@ -0,0 +1,821 @@
|
|||||||
|
/**
|
||||||
|
* PowerShell 语法分析器 (Parser)
|
||||||
|
* 将词法分析器产生的tokens转换为抽象语法树(AST)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Token, TokenType } from './lexer';
|
||||||
|
import {
|
||||||
|
ASTNode,
|
||||||
|
ScriptBlockAst,
|
||||||
|
StatementAst,
|
||||||
|
ExpressionAst,
|
||||||
|
PipelineAst,
|
||||||
|
CommandAst,
|
||||||
|
AssignmentAst,
|
||||||
|
VariableAst,
|
||||||
|
LiteralAst,
|
||||||
|
BinaryExpressionAst,
|
||||||
|
IfStatementAst,
|
||||||
|
FunctionDefinitionAst,
|
||||||
|
ParameterAst,
|
||||||
|
ASTNodeFactory,
|
||||||
|
CommentAst,
|
||||||
|
PipelineElementAst,
|
||||||
|
ElseIfClauseAst,
|
||||||
|
UnaryExpressionAst,
|
||||||
|
ParenthesizedExpressionAst
|
||||||
|
} from './ast';
|
||||||
|
|
||||||
|
export class PowerShellParser {
|
||||||
|
private tokens: Token[];
|
||||||
|
private currentIndex: number = 0;
|
||||||
|
private comments: CommentAst[] = [];
|
||||||
|
|
||||||
|
private originalCode: string;
|
||||||
|
|
||||||
|
constructor(tokens: Token[], originalCode: string = '') {
|
||||||
|
this.tokens = tokens;
|
||||||
|
this.currentIndex = 0;
|
||||||
|
this.originalCode = originalCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析tokens生成AST
|
||||||
|
*/
|
||||||
|
public parse(): ScriptBlockAst {
|
||||||
|
const statements: StatementAst[] = [];
|
||||||
|
|
||||||
|
while (!this.isAtEnd()) {
|
||||||
|
// 跳过空白和换行
|
||||||
|
this.skipWhitespaceAndNewlines();
|
||||||
|
|
||||||
|
if (this.isAtEnd()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理注释
|
||||||
|
if (this.match(TokenType.COMMENT, TokenType.MULTILINE_COMMENT)) {
|
||||||
|
const comment = this.parseComment();
|
||||||
|
this.comments.push(comment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statement = this.parseStatement();
|
||||||
|
if (statement) {
|
||||||
|
statements.push(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = this.tokens.length > 0 ? this.tokens[0].startIndex : 0;
|
||||||
|
const end = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1].endIndex : 0;
|
||||||
|
const line = this.tokens.length > 0 ? this.tokens[0].line : 1;
|
||||||
|
const column = this.tokens.length > 0 ? this.tokens[0].column : 1;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createScriptBlock(statements, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getComments(): CommentAst[] {
|
||||||
|
return this.comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseStatement(): StatementAst | null {
|
||||||
|
// 函数定义
|
||||||
|
if (this.check(TokenType.FUNCTION)) {
|
||||||
|
return this.parseFunctionDefinition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 控制流语句
|
||||||
|
if (this.check(TokenType.IF)) {
|
||||||
|
return this.parseIfStatement();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值或管道
|
||||||
|
return this.parsePipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFunctionDefinition(): FunctionDefinitionAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
this.consume(TokenType.FUNCTION, "Expected 'function'");
|
||||||
|
|
||||||
|
// 函数名可能是CMDLET类型(如Get-Something)或IDENTIFIER
|
||||||
|
let nameToken: Token;
|
||||||
|
if (this.check(TokenType.CMDLET)) {
|
||||||
|
nameToken = this.consume(TokenType.CMDLET, "Expected function name");
|
||||||
|
} else {
|
||||||
|
nameToken = this.consume(TokenType.IDENTIFIER, "Expected function name");
|
||||||
|
}
|
||||||
|
const name = nameToken.value;
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
const parameters: ParameterAst[] = [];
|
||||||
|
if (this.match(TokenType.LEFT_PAREN)) {
|
||||||
|
if (!this.check(TokenType.RIGHT_PAREN)) {
|
||||||
|
do {
|
||||||
|
const param = this.parseParameter();
|
||||||
|
if (param) {
|
||||||
|
parameters.push(param);
|
||||||
|
}
|
||||||
|
} while (this.match(TokenType.COMMA));
|
||||||
|
}
|
||||||
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析函数体
|
||||||
|
const body = this.parseScriptBlock();
|
||||||
|
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createFunctionDefinition(name, parameters, body, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseIfStatement(): IfStatementAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
this.consume(TokenType.IF, "Expected 'if'");
|
||||||
|
|
||||||
|
// PowerShell的if语句可能有括号,也可能没有
|
||||||
|
const hasParens = this.check(TokenType.LEFT_PAREN);
|
||||||
|
if (hasParens) {
|
||||||
|
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'if'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition = this.parseExpression();
|
||||||
|
|
||||||
|
if (hasParens) {
|
||||||
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after if condition");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ifBody = this.parseScriptBlock();
|
||||||
|
|
||||||
|
const elseIfClauses: ElseIfClauseAst[] = [];
|
||||||
|
let elseBody: ScriptBlockAst | undefined;
|
||||||
|
|
||||||
|
// 处理 elseif 子句
|
||||||
|
while (this.match(TokenType.ELSEIF)) {
|
||||||
|
const elseIfStart = this.previous().startIndex;
|
||||||
|
const elseIfLine = this.previous().line;
|
||||||
|
const elseIfColumn = this.previous().column;
|
||||||
|
|
||||||
|
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'elseif'");
|
||||||
|
const elseIfCondition = this.parseExpression();
|
||||||
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after elseif condition");
|
||||||
|
const elseIfBody = this.parseScriptBlock();
|
||||||
|
|
||||||
|
const elseIfEnd = this.previous().endIndex;
|
||||||
|
|
||||||
|
elseIfClauses.push({
|
||||||
|
type: 'ElseIfClause',
|
||||||
|
condition: elseIfCondition,
|
||||||
|
body: elseIfBody,
|
||||||
|
start: elseIfStart,
|
||||||
|
end: elseIfEnd,
|
||||||
|
line: elseIfLine,
|
||||||
|
column: elseIfColumn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 else 子句
|
||||||
|
if (this.match(TokenType.ELSE)) {
|
||||||
|
elseBody = this.parseScriptBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createIfStatement(condition, ifBody, elseIfClauses, elseBody, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePipeline(): PipelineAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
const elements: PipelineElementAst[] = [];
|
||||||
|
|
||||||
|
// 解析第一个元素
|
||||||
|
const firstElement = this.parsePipelineElement();
|
||||||
|
elements.push(firstElement);
|
||||||
|
|
||||||
|
// 解析管道链
|
||||||
|
while (this.match(TokenType.PIPE)) {
|
||||||
|
const element = this.parsePipelineElement();
|
||||||
|
elements.push(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createPipeline(elements, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePipelineElement(): PipelineElementAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
const expression = this.parseAssignment();
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'PipelineElement',
|
||||||
|
expression,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAssignment(): ExpressionAst {
|
||||||
|
const expr = this.parseLogicalOr();
|
||||||
|
|
||||||
|
if (this.match(TokenType.ASSIGNMENT)) {
|
||||||
|
const operator = this.previous().value;
|
||||||
|
const right = this.parseAssignment();
|
||||||
|
|
||||||
|
return ASTNodeFactory.createAssignment(
|
||||||
|
expr,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseLogicalOr(): ExpressionAst {
|
||||||
|
let expr = this.parseLogicalAnd();
|
||||||
|
|
||||||
|
while (this.match(TokenType.LOGICAL)) {
|
||||||
|
const operator = this.previous().value.toLowerCase();
|
||||||
|
if (operator === '-or' || operator === '-xor') {
|
||||||
|
const right = this.parseLogicalAnd();
|
||||||
|
expr = ASTNodeFactory.createBinaryExpression(
|
||||||
|
expr,
|
||||||
|
this.previous().value, // 使用原始大小写
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 如果不是预期的操作符,回退
|
||||||
|
this.currentIndex--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseLogicalAnd(): ExpressionAst {
|
||||||
|
let expr = this.parseComparison();
|
||||||
|
|
||||||
|
while (this.match(TokenType.LOGICAL)) {
|
||||||
|
const operator = this.previous().value.toLowerCase();
|
||||||
|
if (operator === '-and') {
|
||||||
|
const right = this.parseComparison();
|
||||||
|
expr = ASTNodeFactory.createBinaryExpression(
|
||||||
|
expr,
|
||||||
|
this.previous().value, // 使用原始大小写
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 如果不是预期的操作符,回退
|
||||||
|
this.currentIndex--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComparison(): ExpressionAst {
|
||||||
|
let expr = this.parseArithmetic();
|
||||||
|
|
||||||
|
while (this.match(TokenType.COMPARISON)) {
|
||||||
|
const operator = this.previous().value;
|
||||||
|
const right = this.parseArithmetic();
|
||||||
|
expr = ASTNodeFactory.createBinaryExpression(
|
||||||
|
expr,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseArithmetic(): ExpressionAst {
|
||||||
|
let expr = this.parseMultiplicative();
|
||||||
|
|
||||||
|
while (this.match(TokenType.ARITHMETIC)) {
|
||||||
|
const token = this.previous();
|
||||||
|
if (token.value === '+' || token.value === '-') {
|
||||||
|
const operator = token.value;
|
||||||
|
const right = this.parseMultiplicative();
|
||||||
|
expr = ASTNodeFactory.createBinaryExpression(
|
||||||
|
expr,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseMultiplicative(): ExpressionAst {
|
||||||
|
let expr = this.parseUnary();
|
||||||
|
|
||||||
|
while (this.match(TokenType.ARITHMETIC)) {
|
||||||
|
const token = this.previous();
|
||||||
|
if (token.value === '*' || token.value === '/' || token.value === '%') {
|
||||||
|
const operator = token.value;
|
||||||
|
const right = this.parseUnary();
|
||||||
|
expr = ASTNodeFactory.createBinaryExpression(
|
||||||
|
expr,
|
||||||
|
operator,
|
||||||
|
right,
|
||||||
|
expr.start,
|
||||||
|
right.end,
|
||||||
|
expr.line,
|
||||||
|
expr.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseUnary(): ExpressionAst {
|
||||||
|
if (this.match(TokenType.LOGICAL)) {
|
||||||
|
const token = this.previous();
|
||||||
|
const operator = token.value.toLowerCase();
|
||||||
|
if (operator === '-not') {
|
||||||
|
const operand = this.parseUnary();
|
||||||
|
return {
|
||||||
|
type: 'UnaryExpression',
|
||||||
|
operator: token.value, // 使用原始大小写
|
||||||
|
operand,
|
||||||
|
start: token.startIndex,
|
||||||
|
end: operand.end,
|
||||||
|
line: token.line,
|
||||||
|
column: token.column
|
||||||
|
} as UnaryExpressionAst;
|
||||||
|
} else {
|
||||||
|
// 如果不是-not,回退token
|
||||||
|
this.currentIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理算术一元操作符(+, -)
|
||||||
|
if (this.match(TokenType.ARITHMETIC)) {
|
||||||
|
const token = this.previous();
|
||||||
|
if (token.value === '+' || token.value === '-') {
|
||||||
|
const operand = this.parseUnary();
|
||||||
|
return {
|
||||||
|
type: 'UnaryExpression',
|
||||||
|
operator: token.value,
|
||||||
|
operand,
|
||||||
|
start: token.startIndex,
|
||||||
|
end: operand.end,
|
||||||
|
line: token.line,
|
||||||
|
column: token.column
|
||||||
|
} as UnaryExpressionAst;
|
||||||
|
} else {
|
||||||
|
// 如果不是一元操作符,回退
|
||||||
|
this.currentIndex--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parsePrimary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePrimary(): ExpressionAst {
|
||||||
|
// 变量
|
||||||
|
if (this.match(TokenType.VARIABLE)) {
|
||||||
|
const token = this.previous();
|
||||||
|
return ASTNodeFactory.createVariable(
|
||||||
|
token.value,
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串字面量
|
||||||
|
if (this.match(TokenType.STRING, TokenType.HERE_STRING)) {
|
||||||
|
const token = this.previous();
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
token.value,
|
||||||
|
'String',
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字字面量
|
||||||
|
if (this.match(TokenType.NUMBER)) {
|
||||||
|
const token = this.previous();
|
||||||
|
const value = parseFloat(token.value);
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
value,
|
||||||
|
'Number',
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 命令调用 - 扩展支持更多token类型
|
||||||
|
if (this.match(TokenType.CMDLET, TokenType.IDENTIFIER)) {
|
||||||
|
return this.parseCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理看起来像cmdlet但被错误标记的标识符
|
||||||
|
if (this.check(TokenType.IDENTIFIER) && this.current().value.includes('-')) {
|
||||||
|
this.advance();
|
||||||
|
return this.parseCommand();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 哈希表 @{...}
|
||||||
|
if (this.check(TokenType.LEFT_BRACE) && this.current().value === '@{') {
|
||||||
|
return this.parseHashtable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本块表达式 {...} - 已在parseHashtableValue中处理
|
||||||
|
// 这里不需要处理,因为独立的脚本块很少见
|
||||||
|
|
||||||
|
// 括号表达式
|
||||||
|
if (this.match(TokenType.LEFT_PAREN)) {
|
||||||
|
const expr = this.parseExpression();
|
||||||
|
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression");
|
||||||
|
return {
|
||||||
|
type: 'ParenthesizedExpression',
|
||||||
|
expression: expr,
|
||||||
|
start: this.previous().startIndex,
|
||||||
|
end: this.previous().endIndex,
|
||||||
|
line: this.previous().line,
|
||||||
|
column: this.previous().column
|
||||||
|
} as ParenthesizedExpressionAst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于不认识的token,作为普通标识符处理而不是抛出异常
|
||||||
|
const token = this.advance();
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
token.value,
|
||||||
|
'String', // 将未识别的token作为字符串处理
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCommand(): CommandAst {
|
||||||
|
const start = this.previous().startIndex;
|
||||||
|
const line = this.previous().line;
|
||||||
|
const column = this.previous().column;
|
||||||
|
const commandName = this.previous().value;
|
||||||
|
|
||||||
|
const parameters: ParameterAst[] = [];
|
||||||
|
const args: ExpressionAst[] = [];
|
||||||
|
|
||||||
|
// 解析参数和参数值
|
||||||
|
while (!this.isAtEnd() &&
|
||||||
|
!this.check(TokenType.PIPE) &&
|
||||||
|
!this.check(TokenType.NEWLINE) &&
|
||||||
|
!this.check(TokenType.SEMICOLON) &&
|
||||||
|
!this.check(TokenType.RIGHT_PAREN) &&
|
||||||
|
!this.check(TokenType.RIGHT_BRACE)) {
|
||||||
|
|
||||||
|
if (this.match(TokenType.PARAMETER)) {
|
||||||
|
const paramToken = this.previous();
|
||||||
|
const param: ParameterAst = {
|
||||||
|
type: 'Parameter',
|
||||||
|
name: paramToken.value,
|
||||||
|
start: paramToken.startIndex,
|
||||||
|
end: paramToken.endIndex,
|
||||||
|
line: paramToken.line,
|
||||||
|
column: paramToken.column
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查参数是否有值
|
||||||
|
if (!this.check(TokenType.PARAMETER) &&
|
||||||
|
!this.check(TokenType.PIPE) &&
|
||||||
|
!this.check(TokenType.NEWLINE) &&
|
||||||
|
!this.check(TokenType.SEMICOLON)) {
|
||||||
|
param.value = this.parsePrimary();
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.push(param);
|
||||||
|
} else {
|
||||||
|
// 位置参数
|
||||||
|
const arg = this.parsePrimary();
|
||||||
|
args.push(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createCommand(commandName, parameters, args, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseParameter(): ParameterAst | null {
|
||||||
|
if (this.match(TokenType.PARAMETER)) {
|
||||||
|
const token = this.previous();
|
||||||
|
const param: ParameterAst = {
|
||||||
|
type: 'Parameter',
|
||||||
|
name: token.value,
|
||||||
|
start: token.startIndex,
|
||||||
|
end: token.endIndex,
|
||||||
|
line: token.line,
|
||||||
|
column: token.column
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查是否有参数值
|
||||||
|
if (this.match(TokenType.ASSIGNMENT)) {
|
||||||
|
param.value = this.parseExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
return param;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseScriptBlock(): ScriptBlockAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
this.consume(TokenType.LEFT_BRACE, "Expected '{'");
|
||||||
|
|
||||||
|
const statements: StatementAst[] = [];
|
||||||
|
|
||||||
|
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
|
||||||
|
this.skipWhitespaceAndNewlines();
|
||||||
|
|
||||||
|
if (this.check(TokenType.RIGHT_BRACE)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statement = this.parseStatement();
|
||||||
|
if (statement) {
|
||||||
|
statements.push(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.consume(TokenType.RIGHT_BRACE, "Expected '}'");
|
||||||
|
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createScriptBlock(statements, start, end, line, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseExpression(): ExpressionAst {
|
||||||
|
return this.parseAssignment();
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseComment(): CommentAst {
|
||||||
|
const token = this.previous();
|
||||||
|
const isMultiline = token.type === TokenType.MULTILINE_COMMENT;
|
||||||
|
|
||||||
|
return ASTNodeFactory.createComment(
|
||||||
|
token.value,
|
||||||
|
isMultiline,
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法
|
||||||
|
private match(...types: TokenType[]): boolean {
|
||||||
|
for (const type of types) {
|
||||||
|
if (this.check(type)) {
|
||||||
|
this.advance();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private check(type: TokenType): boolean {
|
||||||
|
if (this.isAtEnd()) return false;
|
||||||
|
return this.current().type === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private advance(): Token {
|
||||||
|
if (!this.isAtEnd()) this.currentIndex++;
|
||||||
|
return this.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAtEnd(): boolean {
|
||||||
|
return this.currentIndex >= this.tokens.length || this.current().type === TokenType.EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
private current(): Token {
|
||||||
|
if (this.currentIndex >= this.tokens.length) {
|
||||||
|
return this.tokens[this.tokens.length - 1];
|
||||||
|
}
|
||||||
|
return this.tokens[this.currentIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
private previous(): Token {
|
||||||
|
return this.tokens[this.currentIndex - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private consume(type: TokenType, message: string): Token {
|
||||||
|
if (this.check(type)) return this.advance();
|
||||||
|
|
||||||
|
const current = this.current();
|
||||||
|
throw new Error(`${message}. Got ${current.type}(${current.value}) at line ${current.line}, column ${current.column}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private parseHashtable(): ExpressionAst {
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
// 消费 @{
|
||||||
|
this.advance();
|
||||||
|
|
||||||
|
const entries: any[] = [];
|
||||||
|
|
||||||
|
// 解析哈希表内容
|
||||||
|
if (!this.check(TokenType.RIGHT_BRACE)) {
|
||||||
|
do {
|
||||||
|
// 解析键 - 只接受简单的标识符或字符串
|
||||||
|
const key = this.parseHashtableKey();
|
||||||
|
|
||||||
|
// 消费 =
|
||||||
|
this.consume(TokenType.ASSIGNMENT, "Expected '=' after hashtable key");
|
||||||
|
|
||||||
|
// 解析值
|
||||||
|
const value = this.parseHashtableValue();
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
type: 'HashtableEntry',
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
start: key.start,
|
||||||
|
end: value.end,
|
||||||
|
line: key.line,
|
||||||
|
column: key.column
|
||||||
|
});
|
||||||
|
|
||||||
|
} while (this.match(TokenType.SEMICOLON));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after hashtable entries");
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Hashtable',
|
||||||
|
entries,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseHashtableKey(): ExpressionAst {
|
||||||
|
// 哈希表键只能是简单的标识符或字符串
|
||||||
|
if (this.match(TokenType.STRING, TokenType.HERE_STRING)) {
|
||||||
|
const token = this.previous();
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
token.value,
|
||||||
|
'String',
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接受各种可能的标识符类型作为哈希表键
|
||||||
|
if (this.match(TokenType.IDENTIFIER, TokenType.CMDLET, TokenType.KEYWORD)) {
|
||||||
|
const token = this.previous();
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
token.value,
|
||||||
|
'String',
|
||||||
|
token.startIndex,
|
||||||
|
token.endIndex,
|
||||||
|
token.line,
|
||||||
|
token.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于任何其他类型的token,尝试作为字面量处理
|
||||||
|
const currentToken = this.current();
|
||||||
|
this.advance();
|
||||||
|
return ASTNodeFactory.createLiteral(
|
||||||
|
currentToken.value,
|
||||||
|
'String',
|
||||||
|
currentToken.startIndex,
|
||||||
|
currentToken.endIndex,
|
||||||
|
currentToken.line,
|
||||||
|
currentToken.column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseHashtableValue(): ExpressionAst {
|
||||||
|
// 哈希表值可以是任何表达式
|
||||||
|
if (this.check(TokenType.LEFT_BRACE)) {
|
||||||
|
// 这是一个脚本块 {expression} - 完全绕过复杂解析
|
||||||
|
const start = this.current().startIndex;
|
||||||
|
const line = this.current().line;
|
||||||
|
const column = this.current().column;
|
||||||
|
|
||||||
|
// 直接从原始代码中提取脚本块内容
|
||||||
|
const startPos = this.current().startIndex;
|
||||||
|
this.advance(); // 消费 {
|
||||||
|
|
||||||
|
let braceLevel = 1;
|
||||||
|
let endPos = this.current().startIndex;
|
||||||
|
|
||||||
|
// 找到匹配的右大括号位置
|
||||||
|
while (!this.isAtEnd() && braceLevel > 0) {
|
||||||
|
const token = this.current();
|
||||||
|
if (token.type === TokenType.LEFT_BRACE) {
|
||||||
|
braceLevel++;
|
||||||
|
} else if (token.type === TokenType.RIGHT_BRACE) {
|
||||||
|
braceLevel--;
|
||||||
|
if (braceLevel === 0) {
|
||||||
|
endPos = token.startIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after script block");
|
||||||
|
const end = this.previous().endIndex;
|
||||||
|
|
||||||
|
// 从原始代码中提取内容(从 { 后到 } 前)
|
||||||
|
const rawContent = this.getOriginalCodeSlice(startPos + 1, endPos);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'ScriptBlockExpression',
|
||||||
|
rawContent: rawContent.trim(), // 去掉首尾空白
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
line,
|
||||||
|
column
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于其他值,使用简单的解析
|
||||||
|
return this.parsePrimary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOriginalCodeSlice(start: number, end: number): string {
|
||||||
|
// 直接从原始代码中提取片段
|
||||||
|
if (this.originalCode) {
|
||||||
|
return this.originalCode.substring(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到基于token重建(如果没有原始代码)
|
||||||
|
let result = '';
|
||||||
|
for (const token of this.tokens) {
|
||||||
|
if (token.startIndex >= start && token.endIndex <= end) {
|
||||||
|
result += token.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private skipWhitespaceAndNewlines(): void {
|
||||||
|
while (this.match(TokenType.WHITESPACE, TokenType.NEWLINE)) {
|
||||||
|
// 继续跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
frontend/src/common/prettier/plugins/toml/index.ts
Normal file
72
frontend/src/common/prettier/plugins/toml/index.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Prettier Plugin for TOML file formatting
|
||||||
|
*
|
||||||
|
* This plugin provides support for formatting TOML (Tom's Obvious, Minimal Language) files
|
||||||
|
* using the @toml-tools/parser and custom beautifier.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Plugin, Parser, Printer, SupportLanguage, SupportOption } from 'prettier';
|
||||||
|
import { parse } from '@toml-tools/parser';
|
||||||
|
import { locStart, locEnd } from './loc';
|
||||||
|
import { print } from './printer';
|
||||||
|
import type { TomlDocument, TomlCstNode } from './types';
|
||||||
|
|
||||||
|
const parserName = 'toml';
|
||||||
|
|
||||||
|
// https://prettier.io/docs/en/plugins.html#languages
|
||||||
|
const languages: SupportLanguage[] = [
|
||||||
|
{
|
||||||
|
extensions: ['.toml'],
|
||||||
|
name: 'Toml',
|
||||||
|
parsers: [parserName],
|
||||||
|
filenames: ['Cargo.lock', 'Gopkg.lock'],
|
||||||
|
tmScope: 'source.toml',
|
||||||
|
aceMode: 'toml',
|
||||||
|
codemirrorMode: 'toml',
|
||||||
|
codemirrorMimeType: 'text/x-toml',
|
||||||
|
linguistLanguageId: 365,
|
||||||
|
vscodeLanguageIds: ['toml'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// https://prettier.io/docs/en/plugins.html#parsers
|
||||||
|
const tomlParser: Parser<TomlDocument> = {
|
||||||
|
astFormat: 'toml-cst',
|
||||||
|
parse: (text: string): TomlDocument => {
|
||||||
|
try {
|
||||||
|
return parse(text) as TomlDocument;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('TOML parsing error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
locStart,
|
||||||
|
locEnd,
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://prettier.io/docs/en/plugins.html#printers
|
||||||
|
const tomlPrinter: Printer<TomlCstNode> = {
|
||||||
|
print,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plugin options
|
||||||
|
const options: Record<string, SupportOption> = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Plugin definition
|
||||||
|
const tomlPlugin: Plugin = {
|
||||||
|
languages,
|
||||||
|
parsers: {
|
||||||
|
[parserName]: tomlParser,
|
||||||
|
},
|
||||||
|
printers: {
|
||||||
|
'toml-cst': tomlPrinter,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tomlPlugin;
|
||||||
|
export { languages };
|
||||||
|
export const parsers = tomlPlugin.parsers;
|
||||||
|
export const printers = tomlPlugin.printers;
|
||||||
82
frontend/src/common/prettier/plugins/toml/loc.ts
Normal file
82
frontend/src/common/prettier/plugins/toml/loc.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Location utilities for TOML CST nodes
|
||||||
|
* These functions help Prettier determine the location of nodes for formatting
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { TomlCstNode } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the start location of a CST node
|
||||||
|
* @param cstNode - The TOML CST node
|
||||||
|
* @returns The start offset of the node
|
||||||
|
*/
|
||||||
|
export function locStart(cstNode: TomlCstNode): number {
|
||||||
|
if (!cstNode) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has a direct startOffset, use it
|
||||||
|
if (typeof cstNode.startOffset === 'number') {
|
||||||
|
return cstNode.startOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has children, find the earliest start offset
|
||||||
|
if (cstNode.children) {
|
||||||
|
let minOffset = Infinity;
|
||||||
|
for (const key in cstNode.children) {
|
||||||
|
const childrenArray = cstNode.children[key];
|
||||||
|
if (Array.isArray(childrenArray)) {
|
||||||
|
for (const child of childrenArray) {
|
||||||
|
const childStart = locStart(child);
|
||||||
|
if (childStart < minOffset) {
|
||||||
|
minOffset = childStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minOffset === Infinity ? 0 : minOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the end location of a CST node
|
||||||
|
* @param cstNode - The TOML CST node
|
||||||
|
* @returns The end offset of the node
|
||||||
|
*/
|
||||||
|
export function locEnd(cstNode: TomlCstNode): number {
|
||||||
|
if (!cstNode) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has a direct endOffset, use it
|
||||||
|
if (typeof cstNode.endOffset === 'number') {
|
||||||
|
return cstNode.endOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has children, find the latest end offset
|
||||||
|
if (cstNode.children) {
|
||||||
|
let maxOffset = -1;
|
||||||
|
for (const key in cstNode.children) {
|
||||||
|
const childrenArray = cstNode.children[key];
|
||||||
|
if (Array.isArray(childrenArray)) {
|
||||||
|
for (const child of childrenArray) {
|
||||||
|
const childEnd = locEnd(child);
|
||||||
|
if (childEnd > maxOffset) {
|
||||||
|
maxOffset = childEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxOffset === -1 ? 0 : maxOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the node has an image (token), return the length
|
||||||
|
if (cstNode.image) {
|
||||||
|
const startOffset = locStart(cstNode);
|
||||||
|
return startOffset + cstNode.image.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
284
frontend/src/common/prettier/plugins/toml/printer-utils.ts
Normal file
284
frontend/src/common/prettier/plugins/toml/printer-utils.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for TOML printer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { TomlCstNode, TomlComment, TomlContext } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim trailing whitespace from comment text
|
||||||
|
* @param commentText - The comment text to trim
|
||||||
|
* @returns Trimmed comment text
|
||||||
|
*/
|
||||||
|
export function trimComment(commentText: string): string {
|
||||||
|
return commentText.replace(/[ \t]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a quoted string can be unquoted
|
||||||
|
* @param quotedText - The quoted text to check
|
||||||
|
* @returns Whether the text can be unquoted
|
||||||
|
*/
|
||||||
|
export function canUnquote(quotedText: string): boolean {
|
||||||
|
// Remove quotes if present
|
||||||
|
let text = quotedText;
|
||||||
|
if (text.startsWith('"') && text.endsWith('"')) {
|
||||||
|
text = text.slice(1, -1);
|
||||||
|
} else if (text.startsWith("'") && text.endsWith("'")) {
|
||||||
|
text = text.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string needs quotes
|
||||||
|
if (text.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the string is a valid unquoted key
|
||||||
|
// TOML unquoted keys can contain:
|
||||||
|
// - A-Z, a-z, 0-9, _, -
|
||||||
|
const unquotedKeyRegex = /^[A-Za-z0-9_-]+$/;
|
||||||
|
|
||||||
|
// Additional checks for values that might be confused with other TOML types
|
||||||
|
if (unquotedKeyRegex.test(text)) {
|
||||||
|
// Don't unquote strings that look like booleans
|
||||||
|
if (text === 'true' || text === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't unquote strings that look like numbers
|
||||||
|
if (/^[+-]?(\d+\.?\d*|\d*\.\d+)([eE][+-]?\d+)?$/.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't unquote strings that look like dates/times
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a key needs quotes
|
||||||
|
* @param keyText - The key text to check
|
||||||
|
* @returns Whether the key needs quotes
|
||||||
|
*/
|
||||||
|
export function keyNeedsQuotes(keyText: string): boolean {
|
||||||
|
return !canUnquote(`"${keyText}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a key, adding or removing quotes as needed
|
||||||
|
* @param keyText - The key text to format
|
||||||
|
* @returns Formatted key
|
||||||
|
*/
|
||||||
|
export function formatKey(keyText: string): string {
|
||||||
|
// If already quoted, check if we can unquote
|
||||||
|
if ((keyText.startsWith('"') && keyText.endsWith('"')) ||
|
||||||
|
(keyText.startsWith("'") && keyText.endsWith("'"))) {
|
||||||
|
if (canUnquote(keyText)) {
|
||||||
|
return keyText.slice(1, -1);
|
||||||
|
}
|
||||||
|
return keyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not quoted, check if we need to add quotes
|
||||||
|
if (keyNeedsQuotes(keyText)) {
|
||||||
|
return `"${keyText}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string contains escape sequences that need to be preserved
|
||||||
|
* @param str - The string to check
|
||||||
|
* @returns Whether the string contains escape sequences
|
||||||
|
*/
|
||||||
|
export function containsEscapeSequences(str: string): boolean {
|
||||||
|
// Check for common escape sequences
|
||||||
|
return /\\[btnfr"\\\/]|\\u[0-9a-fA-F]{4}|\\U[0-9a-fA-F]{8}/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string can use literal string syntax (single quotes)
|
||||||
|
* @param str - The string to check (without quotes)
|
||||||
|
* @returns Whether literal string syntax can be used
|
||||||
|
*/
|
||||||
|
export function canUseLiteralString(str: string): boolean {
|
||||||
|
// Literal strings cannot contain single quotes or control characters
|
||||||
|
// and don't need escape sequences
|
||||||
|
return !str.includes("'") &&
|
||||||
|
!/[\x00-\x08\x0A-\x1F\x7F]/.test(str) &&
|
||||||
|
!containsEscapeSequences(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string should use multiline syntax
|
||||||
|
* @param str - The string to check (without quotes)
|
||||||
|
* @returns Whether multiline syntax should be used
|
||||||
|
*/
|
||||||
|
export function shouldUseMultiline(str: string): boolean {
|
||||||
|
// Use multiline for strings that contain newlines
|
||||||
|
return str.includes('\n') || str.includes('\r');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a string value optimally
|
||||||
|
* @param value - The string value (potentially with quotes)
|
||||||
|
* @returns Optimally formatted string
|
||||||
|
*/
|
||||||
|
export function formatStringValue(value: string): string {
|
||||||
|
// If it's already a properly formatted string, keep it
|
||||||
|
if (!value.startsWith('"') && !value.startsWith("'")) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the actual string content
|
||||||
|
let content: string;
|
||||||
|
let isLiteral = false;
|
||||||
|
|
||||||
|
if (value.startsWith('"""') && value.endsWith('"""')) {
|
||||||
|
// Multiline basic string
|
||||||
|
content = value.slice(3, -3);
|
||||||
|
} else if (value.startsWith("'''") && value.endsWith("'''")) {
|
||||||
|
// Multiline literal string
|
||||||
|
content = value.slice(3, -3);
|
||||||
|
isLiteral = true;
|
||||||
|
} else if (value.startsWith('"') && value.endsWith('"')) {
|
||||||
|
// Basic string
|
||||||
|
content = value.slice(1, -1);
|
||||||
|
} else if (value.startsWith("'") && value.endsWith("'")) {
|
||||||
|
// Literal string
|
||||||
|
content = value.slice(1, -1);
|
||||||
|
isLiteral = true;
|
||||||
|
} else {
|
||||||
|
return value; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide on the best format
|
||||||
|
if (shouldUseMultiline(content)) {
|
||||||
|
if (isLiteral || !containsEscapeSequences(content)) {
|
||||||
|
// Use multiline literal string if no escapes needed
|
||||||
|
return `'''${content}'''`;
|
||||||
|
} else {
|
||||||
|
// Use multiline basic string
|
||||||
|
return `"""${content}"""`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (canUseLiteralString(content) && !containsEscapeSequences(content)) {
|
||||||
|
// Use literal string for simple cases
|
||||||
|
return `'${content}'`;
|
||||||
|
} else {
|
||||||
|
// Use basic string
|
||||||
|
return `"${content}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize value representation (for strings, numbers, etc.)
|
||||||
|
* @param value - The value to optimize
|
||||||
|
* @returns Optimized value representation
|
||||||
|
*/
|
||||||
|
export function optimizeValue(value: string): string {
|
||||||
|
// Handle string values
|
||||||
|
if (value.startsWith('"') || value.startsWith("'")) {
|
||||||
|
return formatStringValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-strings, return as-is
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect all comments from comment newline nodes
|
||||||
|
* @param commentsNL - Array of comment newline nodes
|
||||||
|
* @returns Array of comment tokens
|
||||||
|
*/
|
||||||
|
export function collectComments(commentsNL: TomlCstNode[] = []): TomlComment[] {
|
||||||
|
const comments: TomlComment[] = [];
|
||||||
|
|
||||||
|
commentsNL.forEach((commentNLNode) => {
|
||||||
|
if (commentNLNode.children?.Comment) {
|
||||||
|
const commentsTok = commentNLNode.children.Comment;
|
||||||
|
for (const comment of commentsTok) {
|
||||||
|
if (comment.image) {
|
||||||
|
comments.push(comment as TomlComment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single element from a context that should contain exactly one key-value pair
|
||||||
|
* @param ctx - The context to extract from
|
||||||
|
* @returns The single element
|
||||||
|
* @throws Error if the context doesn't contain exactly one element
|
||||||
|
*/
|
||||||
|
export function getSingle(ctx: TomlContext): TomlCstNode {
|
||||||
|
const ctxKeys = Object.keys(ctx);
|
||||||
|
if (ctxKeys.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expecting single key CST ctx but found: <${ctxKeys.length}> keys`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleElementKey = ctxKeys[0];
|
||||||
|
const singleElementValues = ctx[singleElementKey];
|
||||||
|
|
||||||
|
if (!Array.isArray(singleElementValues) || singleElementValues.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expecting single item in CST ctx key but found: <${singleElementValues?.length || 0}> items`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return singleElementValues[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the start offset of an array item (deprecated - use arrItemProp instead)
|
||||||
|
* @param item - The array item node
|
||||||
|
* @returns The start offset
|
||||||
|
*/
|
||||||
|
export function arrItemOffset(item: TomlCstNode): number {
|
||||||
|
return arrItemProp(item, 'startOffset') as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific property from an array item, handling wrapped values
|
||||||
|
* @param item - The array item node
|
||||||
|
* @param propName - The property name to retrieve
|
||||||
|
* @returns The property value
|
||||||
|
* @throws Error for non-exhaustive matches
|
||||||
|
*/
|
||||||
|
export function arrItemProp(item: TomlCstNode, propName: keyof TomlCstNode): any {
|
||||||
|
let currentItem = item;
|
||||||
|
|
||||||
|
// Unwrap 'val' nodes
|
||||||
|
if (currentItem.name === 'val' && currentItem.children) {
|
||||||
|
currentItem = getSingle(currentItem.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct property access
|
||||||
|
if (currentItem[propName] !== undefined) {
|
||||||
|
return currentItem[propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for LSquare (array start)
|
||||||
|
if (currentItem.children?.LSquare?.[0]?.[propName] !== undefined) {
|
||||||
|
return currentItem.children.LSquare[0][propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for LCurly (inline table start)
|
||||||
|
if (currentItem.children?.LCurly?.[0]?.[propName] !== undefined) {
|
||||||
|
return currentItem.children.LCurly[0][propName];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Non-exhaustive match for property ${propName}`);
|
||||||
|
}
|
||||||
413
frontend/src/common/prettier/plugins/toml/printer.ts
Normal file
413
frontend/src/common/prettier/plugins/toml/printer.ts
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
/**
|
||||||
|
* TOML Printer for Prettier
|
||||||
|
*
|
||||||
|
* This module provides a visitor-based printer for TOML CST nodes,
|
||||||
|
* converting them to Prettier's document format.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseTomlCstVisitor } from '@toml-tools/parser';
|
||||||
|
import { tokensDictionary as t } from '@toml-tools/lexer';
|
||||||
|
import { doc } from 'prettier';
|
||||||
|
import type { AstPath, Doc } from 'prettier';
|
||||||
|
import {
|
||||||
|
trimComment,
|
||||||
|
collectComments,
|
||||||
|
arrItemOffset,
|
||||||
|
arrItemProp,
|
||||||
|
getSingle,
|
||||||
|
formatKey,
|
||||||
|
optimizeValue,
|
||||||
|
} from './printer-utils';
|
||||||
|
import type {
|
||||||
|
TomlCstNode,
|
||||||
|
TomlDocument,
|
||||||
|
TomlExpression,
|
||||||
|
TomlKeyVal,
|
||||||
|
TomlComment,
|
||||||
|
TomlContext
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const { join, line, hardline, softline, ifBreak, indent, group } = doc.builders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOML Beautifier Visitor class that extends the base CST visitor
|
||||||
|
*/
|
||||||
|
class TomlBeautifierVisitor extends BaseTomlCstVisitor {
|
||||||
|
// Helper methods
|
||||||
|
public mapVisit: (elements: TomlCstNode[] | undefined) => (Doc | string)[];
|
||||||
|
public visitSingle: (ctx: TomlContext) => Doc | string;
|
||||||
|
public visit: (ctx: TomlCstNode, inParam?: any) => Doc | string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
// Try to call validateVisitor if it exists
|
||||||
|
if (typeof (this as any).validateVisitor === 'function') {
|
||||||
|
(this as any).validateVisitor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize helper methods
|
||||||
|
this.mapVisit = (elements: TomlCstNode[] | undefined): (Doc | string)[] => {
|
||||||
|
if (!elements) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return elements.map((element) => this.visit(element));
|
||||||
|
};
|
||||||
|
|
||||||
|
this.visitSingle = (ctx: TomlContext): Doc | string => {
|
||||||
|
const singleElement = getSingle(ctx);
|
||||||
|
return this.visit(singleElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store reference to inherited visit method and override it
|
||||||
|
const originalVisit = Object.getPrototypeOf(this).visit?.bind(this);
|
||||||
|
this.visit = (ctx: TomlCstNode, inParam?: any): Doc | string => {
|
||||||
|
if (!ctx) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to use the inherited visit method first
|
||||||
|
if (originalVisit) {
|
||||||
|
try {
|
||||||
|
return originalVisit(ctx, inParam);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Original visit method failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: manually dispatch based on node name/type
|
||||||
|
const methodName = ctx.name;
|
||||||
|
if (methodName && typeof (this as any)[methodName] === 'function') {
|
||||||
|
const visitMethod = (this as any)[methodName];
|
||||||
|
try {
|
||||||
|
if (ctx.children) {
|
||||||
|
return visitMethod.call(this, ctx.children);
|
||||||
|
} else {
|
||||||
|
return visitMethod.call(this, ctx);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Visit method ${methodName} failed:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback: return image if available
|
||||||
|
return ctx.image || '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit the root TOML document
|
||||||
|
*/
|
||||||
|
toml(ctx: TomlDocument): Doc {
|
||||||
|
// Handle empty toml document
|
||||||
|
if (!ctx.expression) {
|
||||||
|
return [line];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTable = (node: TomlExpression): boolean => {
|
||||||
|
return !!node.table;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOnlyComment = (node: TomlExpression): boolean => {
|
||||||
|
return !!node.Comment && Object.keys(node).length === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const expsCsts = ctx.expression;
|
||||||
|
const cstGroups: TomlExpression[][] = [];
|
||||||
|
let currCstGroup: TomlExpression[] = [];
|
||||||
|
|
||||||
|
// Split expressions into groups defined by tables
|
||||||
|
for (let i = expsCsts.length - 1; i >= 0; i--) {
|
||||||
|
const currCstNode = expsCsts[i];
|
||||||
|
currCstGroup.push(currCstNode);
|
||||||
|
|
||||||
|
if (isTable(currCstNode)) {
|
||||||
|
let j = i - 1;
|
||||||
|
let stillInComments = true;
|
||||||
|
|
||||||
|
// Add leading comments to current group
|
||||||
|
while (j >= 0 && stillInComments) {
|
||||||
|
const priorCstNode = expsCsts[j];
|
||||||
|
if (isOnlyComment(priorCstNode)) {
|
||||||
|
currCstGroup.push(priorCstNode);
|
||||||
|
j--;
|
||||||
|
i--;
|
||||||
|
} else {
|
||||||
|
stillInComments = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse since we scanned backwards
|
||||||
|
currCstGroup.reverse();
|
||||||
|
cstGroups.push(currCstGroup);
|
||||||
|
currCstGroup = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currCstGroup.length > 0) {
|
||||||
|
currCstGroup.reverse();
|
||||||
|
cstGroups.push(currCstGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for reverse scanning
|
||||||
|
cstGroups.reverse();
|
||||||
|
const docGroups = cstGroups.map((currGroup) => this.mapVisit(currGroup));
|
||||||
|
|
||||||
|
// Add newlines between group elements
|
||||||
|
const docGroupsInnerNewlines = docGroups.map((currGroup) =>
|
||||||
|
join(line, currGroup)
|
||||||
|
);
|
||||||
|
const docGroupsOuterNewlines = join([line, line], docGroupsInnerNewlines);
|
||||||
|
|
||||||
|
return [docGroupsOuterNewlines, line];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit an expression (keyval, table, or comment)
|
||||||
|
*/
|
||||||
|
expression(ctx: TomlExpression): Doc | string {
|
||||||
|
if (ctx.keyval) {
|
||||||
|
let keyValDoc = this.visit(ctx.keyval[0]);
|
||||||
|
if (ctx.Comment) {
|
||||||
|
const commentText = trimComment(ctx.Comment[0].image);
|
||||||
|
keyValDoc = [keyValDoc, ' ' + commentText];
|
||||||
|
}
|
||||||
|
return keyValDoc;
|
||||||
|
} else if (ctx.table) {
|
||||||
|
let tableDoc = this.visit(ctx.table[0]);
|
||||||
|
if (ctx.Comment) {
|
||||||
|
const commentText = trimComment(ctx.Comment[0].image);
|
||||||
|
tableDoc = [tableDoc, ' ' + commentText];
|
||||||
|
}
|
||||||
|
return tableDoc;
|
||||||
|
} else if (ctx.Comment) {
|
||||||
|
return trimComment(ctx.Comment[0].image);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a key-value pair
|
||||||
|
*/
|
||||||
|
keyval(ctx: TomlKeyVal): Doc {
|
||||||
|
const keyDoc = this.visit(ctx.key[0]);
|
||||||
|
const valueDoc = this.visit(ctx.val[0]);
|
||||||
|
return [keyDoc, ' = ', valueDoc];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a key
|
||||||
|
*/
|
||||||
|
key(ctx: any): Doc {
|
||||||
|
const keyTexts = ctx.IKey?.map((tok: any) => {
|
||||||
|
const keyText = tok.image;
|
||||||
|
// Apply key formatting (add/remove quotes as needed)
|
||||||
|
return formatKey(keyText);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return join('.', keyTexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a value
|
||||||
|
*/
|
||||||
|
val(ctx: any): Doc | string {
|
||||||
|
try {
|
||||||
|
const actualValueNode = getSingle(ctx);
|
||||||
|
if (actualValueNode.image !== undefined) {
|
||||||
|
// Terminal token - 优化值的表示
|
||||||
|
return optimizeValue(actualValueNode.image);
|
||||||
|
} else {
|
||||||
|
return this.visit(actualValueNode);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果getSingle失败,尝试直接处理children
|
||||||
|
if (ctx.children) {
|
||||||
|
// 处理不同类型的值
|
||||||
|
for (const [childKey, childNodes] of Object.entries(ctx.children)) {
|
||||||
|
if (Array.isArray(childNodes) && childNodes.length > 0) {
|
||||||
|
const firstChild = childNodes[0];
|
||||||
|
|
||||||
|
// 处理基本类型
|
||||||
|
if (firstChild.image !== undefined) {
|
||||||
|
// 优化值的表示(特别是字符串)
|
||||||
|
return optimizeValue(firstChild.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理复杂类型(如数组、内联表等)
|
||||||
|
if (firstChild.name) {
|
||||||
|
return this.visit(firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit an array
|
||||||
|
*/
|
||||||
|
array(ctx: any): Doc {
|
||||||
|
const arrayValuesDocs = ctx.arrayValues ? this.visit(ctx.arrayValues) : '';
|
||||||
|
const postComments = collectComments(ctx.commentNewline);
|
||||||
|
const commentsDocs = postComments.map((commentTok) => {
|
||||||
|
const trimmedCommentText = trimComment(commentTok.image);
|
||||||
|
return [hardline, trimmedCommentText];
|
||||||
|
});
|
||||||
|
|
||||||
|
return group(['[', indent([arrayValuesDocs, commentsDocs]), softline, ']']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit array values
|
||||||
|
*/
|
||||||
|
arrayValues(ctx: any): Doc {
|
||||||
|
const values = ctx.val || [];
|
||||||
|
const commas = ctx.Comma || [];
|
||||||
|
const comments = collectComments(ctx.commentNewline);
|
||||||
|
|
||||||
|
const itemsCst = [...values, ...commas, ...comments];
|
||||||
|
itemsCst.sort((a, b) => {
|
||||||
|
const aOffset = arrItemOffset(a);
|
||||||
|
const bOffset = arrItemOffset(b);
|
||||||
|
return aOffset - bOffset;
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemsDoc: Doc[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < itemsCst.length; i++) {
|
||||||
|
const cstItem = itemsCst[i];
|
||||||
|
|
||||||
|
if (cstItem.name === 'val') {
|
||||||
|
const valDoc = this.visit(cstItem);
|
||||||
|
const valEndLine = arrItemProp(cstItem, 'endLine');
|
||||||
|
let potentialComma = '';
|
||||||
|
|
||||||
|
// Handle next item (comma or comment)
|
||||||
|
if (itemsCst[i + 1]) {
|
||||||
|
let nextPossibleComment = itemsCst[i + 1];
|
||||||
|
|
||||||
|
// Skip commas
|
||||||
|
if (nextPossibleComment.tokenType === t.Comma) {
|
||||||
|
potentialComma = ',';
|
||||||
|
i++;
|
||||||
|
nextPossibleComment = itemsCst[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle same-line comments
|
||||||
|
if (
|
||||||
|
nextPossibleComment &&
|
||||||
|
nextPossibleComment.tokenType === t.Comment &&
|
||||||
|
nextPossibleComment.startLine === valEndLine
|
||||||
|
) {
|
||||||
|
i++;
|
||||||
|
const trimmedComment = trimComment(nextPossibleComment.image);
|
||||||
|
const comment = ' ' + trimmedComment;
|
||||||
|
itemsDoc.push([valDoc, potentialComma, comment, hardline]);
|
||||||
|
} else {
|
||||||
|
// No comment on same line
|
||||||
|
const isTrailingComma = i === itemsCst.length - 1;
|
||||||
|
const optionalCommaLineBreak = isTrailingComma
|
||||||
|
? ifBreak(',', '') // Only print trailing comma if multiline array
|
||||||
|
: [potentialComma, line];
|
||||||
|
itemsDoc.push([valDoc, optionalCommaLineBreak]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Last item without followup
|
||||||
|
itemsDoc.push([valDoc]);
|
||||||
|
}
|
||||||
|
} else if (cstItem.tokenType === t.Comment) {
|
||||||
|
// Separate line comment
|
||||||
|
const trimmedComment = trimComment(cstItem.image);
|
||||||
|
itemsDoc.push([trimmedComment, hardline]);
|
||||||
|
} else {
|
||||||
|
throw new Error('Non-exhaustive match in arrayValues');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [softline, itemsDoc];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit an inline table
|
||||||
|
*/
|
||||||
|
inlineTable(ctx: any): Doc {
|
||||||
|
const inlineTableKeyValsDocs = ctx.inlineTableKeyVals
|
||||||
|
? this.visit(ctx.inlineTableKeyVals)
|
||||||
|
: '';
|
||||||
|
return group(['{ ', inlineTableKeyValsDocs, ' }']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit inline table key-value pairs
|
||||||
|
*/
|
||||||
|
inlineTableKeyVals(ctx: any): Doc {
|
||||||
|
const keyValDocs = this.mapVisit(ctx.keyval);
|
||||||
|
return join(', ', keyValDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a table
|
||||||
|
*/
|
||||||
|
table(ctx: any): Doc | string {
|
||||||
|
return this.visitSingle(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit a standard table
|
||||||
|
*/
|
||||||
|
stdTable(ctx: any): Doc {
|
||||||
|
if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) {
|
||||||
|
const keyTexts = ctx.key[0].children.IKey.map((tok: any) => {
|
||||||
|
return formatKey(tok.image);
|
||||||
|
});
|
||||||
|
return ['[', join('.', keyTexts), ']'];
|
||||||
|
}
|
||||||
|
return '[]';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit an array table
|
||||||
|
*/
|
||||||
|
arrayTable(ctx: any): Doc {
|
||||||
|
if (ctx.key && ctx.key[0] && ctx.key[0].children && ctx.key[0].children.IKey) {
|
||||||
|
const keyTexts = ctx.key[0].children.IKey.map((tok: any) => {
|
||||||
|
return formatKey(tok.image);
|
||||||
|
});
|
||||||
|
return ['[[', join('.', keyTexts), ']]'];
|
||||||
|
}
|
||||||
|
return '[[]]';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit newline (should not be called)
|
||||||
|
*/
|
||||||
|
nl(ctx: any): never {
|
||||||
|
throw new Error('Should not get here!');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visit comment newline (no-op)
|
||||||
|
*/
|
||||||
|
commentNewline(ctx: any): void {
|
||||||
|
// No operation needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton visitor instance
|
||||||
|
const beautifierVisitor = new TomlBeautifierVisitor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main print function for Prettier
|
||||||
|
* @param path - AST path from Prettier
|
||||||
|
* @param options - Print options
|
||||||
|
* @param print - Print function (unused in this implementation)
|
||||||
|
* @returns Formatted document
|
||||||
|
*/
|
||||||
|
export function print(path: AstPath<TomlCstNode>, options?: any, print?: any): Doc {
|
||||||
|
const cst = path.node as TomlDocument;
|
||||||
|
return beautifierVisitor.visit(cst);
|
||||||
|
}
|
||||||
62
frontend/src/common/prettier/plugins/toml/types.ts
Normal file
62
frontend/src/common/prettier/plugins/toml/types.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* TypeScript type definitions for TOML Prettier plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TOML CST Node types based on @toml-tools/parser
|
||||||
|
export interface TomlCstNode {
|
||||||
|
name?: string;
|
||||||
|
image?: string;
|
||||||
|
children?: Record<string, TomlCstNode[]>;
|
||||||
|
startOffset?: number;
|
||||||
|
endOffset?: number;
|
||||||
|
startLine?: number;
|
||||||
|
endLine?: number;
|
||||||
|
tokenType?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlComment extends TomlCstNode {
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlContext {
|
||||||
|
[key: string]: TomlCstNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlValue extends TomlCstNode {
|
||||||
|
children: TomlContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlKeyVal extends TomlCstNode {
|
||||||
|
key: TomlCstNode[];
|
||||||
|
val: TomlCstNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlArray extends TomlCstNode {
|
||||||
|
arrayValues?: TomlCstNode;
|
||||||
|
commentNewline?: TomlCstNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlInlineTable extends TomlCstNode {
|
||||||
|
inlineTableKeyVals?: TomlCstNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlTable extends TomlCstNode {
|
||||||
|
table: TomlCstNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlExpression extends TomlCstNode {
|
||||||
|
keyval?: TomlKeyVal[];
|
||||||
|
table?: TomlTable[];
|
||||||
|
Comment?: TomlComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TomlDocument extends TomlCstNode {
|
||||||
|
expression?: TomlExpression[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print options for TOML formatting
|
||||||
|
export interface TomlPrintOptions {
|
||||||
|
printWidth?: number;
|
||||||
|
tabWidth?: number;
|
||||||
|
useTabs?: boolean;
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
const initializeThemeColors = async () => {
|
const initializeThemeColors = async () => {
|
||||||
try {
|
try {
|
||||||
const themes = await ThemeService.GetDefaultThemes();
|
const themes = await ThemeService.GetDefaultThemes();
|
||||||
|
if (!themes) {
|
||||||
|
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||||
|
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||||
|
}
|
||||||
if (themes[ThemeType.ThemeTypeDark]) {
|
if (themes[ThemeType.ThemeTypeDark]) {
|
||||||
Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors);
|
Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import javaPrettierPlugin from "@/common/prettier/plugins/java"
|
|||||||
import xmlPrettierPlugin from "@prettier/plugin-xml"
|
import xmlPrettierPlugin from "@prettier/plugin-xml"
|
||||||
import * as rustPrettierPlugin from "@/common/prettier/plugins/rust";
|
import * as rustPrettierPlugin from "@/common/prettier/plugins/rust";
|
||||||
import * as shellPrettierPlugin from "@/common/prettier/plugins/shell";
|
import * as shellPrettierPlugin from "@/common/prettier/plugins/shell";
|
||||||
import tomlPrettierPlugin from "prettier-plugin-toml";
|
import tomlPrettierPlugin from "@/common/prettier/plugins/toml";
|
||||||
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
|
import clojurePrettierPlugin from "@cospaia/prettier-plugin-clojure";
|
||||||
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
|
import groovyPrettierPlugin from "@/common/prettier/plugins/groovy";
|
||||||
import powershellPrettierPlugin from "@/common/prettier/plugins/powershell";
|
import powershellPrettierPlugin from "@/common/prettier/plugins/powershell";
|
||||||
|
|||||||
@@ -72,6 +72,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
|
<!-- Go代码格式化测试区域 -->
|
||||||
|
<SettingSection title="Go Code Formatter Test">
|
||||||
|
<SettingItem title="Go Code Input">
|
||||||
|
<textarea
|
||||||
|
v-model="goCode"
|
||||||
|
placeholder="Enter Go code to format..."
|
||||||
|
class="select-input code-textarea"
|
||||||
|
rows="8"
|
||||||
|
></textarea>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title="Actions">
|
||||||
|
<div class="button-group">
|
||||||
|
<button @click="testGoFormatter" class="test-button primary" :disabled="isFormatting">
|
||||||
|
{{ isFormatting ? 'Formatting...' : 'Format Go Code' }}
|
||||||
|
</button>
|
||||||
|
<button @click="resetGoCode" class="test-button">
|
||||||
|
Reset to Sample
|
||||||
|
</button>
|
||||||
|
<button @click="loadComplexSample" class="test-button">
|
||||||
|
Load Complex Sample
|
||||||
|
</button>
|
||||||
|
<button @click="loadBrokenSample" class="test-button">
|
||||||
|
Load Broken Sample
|
||||||
|
</button>
|
||||||
|
<button @click="checkWasmStatus" class="test-button">
|
||||||
|
Check WASM Status
|
||||||
|
</button>
|
||||||
|
<button @click="initializeGoWasm" class="test-button" :disabled="isInitializing">
|
||||||
|
{{ isInitializing ? 'Initializing...' : 'Initialize Go WASM' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<!-- 加载状态和进度 -->
|
||||||
|
<div v-if="formatStatus" class="test-status detailed-status">
|
||||||
|
<div class="status-header" :class="formatStatus.type">
|
||||||
|
<strong>{{ formatStatus.type.toUpperCase() }}:</strong> {{ formatStatus.message }}
|
||||||
|
</div>
|
||||||
|
<div v-if="formatStatus.details" class="status-details">
|
||||||
|
<div v-for="(detail, index) in formatStatus.details" :key="index" class="status-detail">
|
||||||
|
<span class="detail-time">[{{ detail.time }}]</span>
|
||||||
|
<span class="detail-message">{{ detail.message }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="formatStatus.duration" class="status-duration">
|
||||||
|
执行时间: {{ formatStatus.duration }}ms
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 格式化结果 -->
|
||||||
|
<SettingItem v-if="formattedCode" title="Formatted Result">
|
||||||
|
<textarea
|
||||||
|
v-model="formattedCode"
|
||||||
|
readonly
|
||||||
|
class="select-input code-textarea result-textarea"
|
||||||
|
rows="8"
|
||||||
|
></textarea>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
<!-- 清除所有测试状态 -->
|
<!-- 清除所有测试状态 -->
|
||||||
<SettingSection title="Cleanup">
|
<SettingSection title="Cleanup">
|
||||||
<SettingItem title="Clear All">
|
<SettingItem title="Clear All">
|
||||||
@@ -91,6 +151,8 @@ import { ref } from 'vue'
|
|||||||
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
|
import * as TestService from '@/../bindings/voidraft/internal/services/testservice'
|
||||||
import SettingSection from '../components/SettingSection.vue'
|
import SettingSection from '../components/SettingSection.vue'
|
||||||
import SettingItem from '../components/SettingItem.vue'
|
import SettingItem from '../components/SettingItem.vue'
|
||||||
|
import { format } from 'prettier'
|
||||||
|
import goPrettierPlugin from '@/common/prettier/plugins/go/go.mjs'
|
||||||
|
|
||||||
// Badge测试状态
|
// Badge测试状态
|
||||||
const badgeText = ref('')
|
const badgeText = ref('')
|
||||||
@@ -105,6 +167,33 @@ const notificationStatus = ref<{ type: string; message: string } | null>(null)
|
|||||||
// 清除状态
|
// 清除状态
|
||||||
const clearStatus = ref<{ type: string; message: string } | null>(null)
|
const clearStatus = ref<{ type: string; message: string } | null>(null)
|
||||||
|
|
||||||
|
// Go代码格式化测试状态
|
||||||
|
const goCode = ref(`package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
if len(os.Args)<2{
|
||||||
|
fmt.Println("Usage: program <name>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name:=os.Args[1]
|
||||||
|
fmt.Printf("Hello, %s!\\n",name)
|
||||||
|
}`)
|
||||||
|
|
||||||
|
const formattedCode = ref('')
|
||||||
|
const isFormatting = ref(false)
|
||||||
|
const isInitializing = ref(false)
|
||||||
|
const formatStatus = ref<{
|
||||||
|
type: 'success' | 'error' | 'info' | 'warning';
|
||||||
|
message: string;
|
||||||
|
details?: Array<{ time: string; message: string }>;
|
||||||
|
duration?: number;
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// 显示状态消息的辅助函数
|
// 显示状态消息的辅助函数
|
||||||
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
|
const showStatus = (statusRef: any, type: 'success' | 'error', message: string) => {
|
||||||
statusRef.value = { type, message }
|
statusRef.value = { type, message }
|
||||||
@@ -158,6 +247,382 @@ const testUpdateNotification = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go代码格式化相关函数
|
||||||
|
const addFormatDetail = (message: string) => {
|
||||||
|
const time = new Date().toLocaleTimeString()
|
||||||
|
if (!formatStatus.value) {
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'info',
|
||||||
|
message: '正在执行...',
|
||||||
|
details: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!formatStatus.value.details) {
|
||||||
|
formatStatus.value.details = []
|
||||||
|
}
|
||||||
|
formatStatus.value.details.push({ time, message })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WASM状态
|
||||||
|
const checkWasmStatus = async () => {
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'info',
|
||||||
|
message: '检查WASM状态...',
|
||||||
|
details: []
|
||||||
|
}
|
||||||
|
|
||||||
|
addFormatDetail('开始检查环境...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查浏览器环境
|
||||||
|
addFormatDetail('检查浏览器环境支持')
|
||||||
|
if (typeof WebAssembly === 'undefined') {
|
||||||
|
throw new Error('WebAssembly not supported in this browser')
|
||||||
|
}
|
||||||
|
addFormatDetail('✅ WebAssembly 支持正常')
|
||||||
|
|
||||||
|
// 检查Go运行时
|
||||||
|
addFormatDetail('检查Go运行时状态')
|
||||||
|
if (typeof globalThis.Go !== 'undefined') {
|
||||||
|
addFormatDetail('✅ Go运行时已加载')
|
||||||
|
} else {
|
||||||
|
addFormatDetail('❌ Go运行时未加载')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查formatGo函数
|
||||||
|
addFormatDetail('检查formatGo函数')
|
||||||
|
if (typeof globalThis.formatGo === 'function') {
|
||||||
|
addFormatDetail('✅ formatGo函数可用')
|
||||||
|
} else {
|
||||||
|
addFormatDetail('❌ formatGo函数不可用')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查WASM文件可访问性
|
||||||
|
addFormatDetail('检查WASM文件可访问性')
|
||||||
|
try {
|
||||||
|
const response = await fetch('/go.wasm', { method: 'HEAD' })
|
||||||
|
if (response.ok) {
|
||||||
|
addFormatDetail('✅ go.wasm文件可访问')
|
||||||
|
} else {
|
||||||
|
addFormatDetail(`❌ go.wasm文件不可访问: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addFormatDetail(`❌ go.wasm文件访问失败: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查wasm_exec.js
|
||||||
|
addFormatDetail('检查wasm_exec.js文件')
|
||||||
|
try {
|
||||||
|
const response = await fetch('/wasm_exec.js', { method: 'HEAD' })
|
||||||
|
if (response.ok) {
|
||||||
|
addFormatDetail('✅ wasm_exec.js文件可访问')
|
||||||
|
} else {
|
||||||
|
addFormatDetail(`❌ wasm_exec.js文件不可访问: ${response.status}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addFormatDetail(`❌ wasm_exec.js文件访问失败: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatStatus.value.type = 'success'
|
||||||
|
formatStatus.value.message = 'WASM状态检查完成'
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
addFormatDetail(`❌ 检查失败: ${error.message}`)
|
||||||
|
formatStatus.value.type = 'error'
|
||||||
|
formatStatus.value.message = `WASM状态检查失败: ${error.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动初始化 Go WASM
|
||||||
|
const initializeGoWasm = async () => {
|
||||||
|
if (isInitializing.value) return
|
||||||
|
|
||||||
|
isInitializing.value = true
|
||||||
|
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'info',
|
||||||
|
message: '正在初始化 Go WASM...',
|
||||||
|
details: []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
addFormatDetail('开始手动初始化 Go WASM')
|
||||||
|
|
||||||
|
// 直接调用插件的初始化函数
|
||||||
|
const { initialize } = await import('@/common/prettier/plugins/go/go.mjs')
|
||||||
|
|
||||||
|
addFormatDetail('调用插件初始化函数...')
|
||||||
|
await initialize()
|
||||||
|
|
||||||
|
addFormatDetail('检查 formatGo 函数是否可用...')
|
||||||
|
if (typeof globalThis.formatGo === 'function') {
|
||||||
|
addFormatDetail('✅ formatGo 函数初始化成功')
|
||||||
|
|
||||||
|
// 测试函数
|
||||||
|
addFormatDetail('测试 formatGo 函数...')
|
||||||
|
const testCode = 'package main\nfunc main(){}'
|
||||||
|
const result = globalThis.formatGo(testCode)
|
||||||
|
addFormatDetail(`✅ 测试成功,格式化后长度: ${result.length}`)
|
||||||
|
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Go WASM 初始化成功!',
|
||||||
|
details: formatStatus.value.details
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('formatGo 函数仍然不可用')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
addFormatDetail(`❌ 初始化失败: ${error.message}`)
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'error',
|
||||||
|
message: `Go WASM 初始化失败: ${error.message}`,
|
||||||
|
details: formatStatus.value.details
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isInitializing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试Go代码格式化
|
||||||
|
const testGoFormatter = async () => {
|
||||||
|
if (isFormatting.value) return
|
||||||
|
|
||||||
|
isFormatting.value = true
|
||||||
|
formattedCode.value = ''
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'info',
|
||||||
|
message: '正在格式化Go代码...',
|
||||||
|
details: []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
addFormatDetail('开始格式化流程')
|
||||||
|
addFormatDetail(`输入代码长度: ${goCode.value.length} 字符`)
|
||||||
|
|
||||||
|
// 设置超时检测
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
addFormatDetail('⚠️ 格式化超时 (10秒),可能存在阻塞')
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
addFormatDetail('调用prettier格式化...')
|
||||||
|
|
||||||
|
const result = await format(goCode.value, {
|
||||||
|
parser: 'go-format',
|
||||||
|
plugins: [goPrettierPlugin]
|
||||||
|
})
|
||||||
|
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime
|
||||||
|
|
||||||
|
addFormatDetail('✅ 格式化完成')
|
||||||
|
addFormatDetail(`输出代码长度: ${result.length} 字符`)
|
||||||
|
|
||||||
|
formattedCode.value = result
|
||||||
|
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'success',
|
||||||
|
message: '代码格式化成功!',
|
||||||
|
details: formatStatus.value.details,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
const duration = Date.now() - startTime
|
||||||
|
|
||||||
|
addFormatDetail(`❌ 格式化失败: ${error.message}`)
|
||||||
|
|
||||||
|
// 详细错误分析
|
||||||
|
if (error.message.includes('WASM')) {
|
||||||
|
addFormatDetail('可能原因: WASM模块加载或初始化问题')
|
||||||
|
} else if (error.message.includes('formatGo')) {
|
||||||
|
addFormatDetail('可能原因: Go函数未正确暴露到全局作用域')
|
||||||
|
} else if (error.message.includes('timeout')) {
|
||||||
|
addFormatDetail('可能原因: 代码执行超时或阻塞')
|
||||||
|
}
|
||||||
|
|
||||||
|
formatStatus.value = {
|
||||||
|
type: 'error',
|
||||||
|
message: `格式化失败: ${error.message}`,
|
||||||
|
details: formatStatus.value.details,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isFormatting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置Go代码为示例
|
||||||
|
const resetGoCode = () => {
|
||||||
|
goCode.value = `package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
if len(os.Args)<2{
|
||||||
|
fmt.Println("Usage: program <name>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name:=os.Args[1]
|
||||||
|
fmt.Printf("Hello, %s!\\n",name)
|
||||||
|
}`
|
||||||
|
formattedCode.value = ''
|
||||||
|
formatStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载复杂示例
|
||||||
|
const loadComplexSample = () => {
|
||||||
|
goCode.value = `package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct{
|
||||||
|
ID int \`json:"id"\`
|
||||||
|
Name string \`json:"name"\`
|
||||||
|
Email string \`json:"email"\`
|
||||||
|
CreatedAt time.Time \`json:"created_at"\`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserService struct{
|
||||||
|
users []User
|
||||||
|
nextID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService()*UserService{
|
||||||
|
return &UserService{
|
||||||
|
users:make([]User,0),
|
||||||
|
nextID:1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *UserService)CreateUser(name,email string)*User{
|
||||||
|
user:=User{
|
||||||
|
ID:s.nextID,
|
||||||
|
Name:name,
|
||||||
|
Email:email,
|
||||||
|
CreatedAt:time.Now(),
|
||||||
|
}
|
||||||
|
s.users=append(s.users,user)
|
||||||
|
s.nextID++
|
||||||
|
return &user
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *UserService)GetUser(id int)*User{
|
||||||
|
for i:=range s.users{
|
||||||
|
if s.users[i].ID==id{
|
||||||
|
return &s.users[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func(s *UserService)ListUsers()[]User{
|
||||||
|
return s.users
|
||||||
|
}
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
service:=NewUserService()
|
||||||
|
|
||||||
|
http.HandleFunc("/users",func(w http.ResponseWriter,r *http.Request){
|
||||||
|
switch r.Method{
|
||||||
|
case http.MethodGet:
|
||||||
|
users:=service.ListUsers()
|
||||||
|
w.Header().Set("Content-Type","application/json")
|
||||||
|
json.NewEncoder(w).Encode(users)
|
||||||
|
case http.MethodPost:
|
||||||
|
body,err:=ioutil.ReadAll(r.Body)
|
||||||
|
if err!=nil{
|
||||||
|
http.Error(w,"Bad request",http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req struct{
|
||||||
|
Name string \`json:"name"\`
|
||||||
|
Email string \`json:"email"\`
|
||||||
|
}
|
||||||
|
if err:=json.Unmarshal(body,&req);err!=nil{
|
||||||
|
http.Error(w,"Invalid JSON",http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user:=service.CreateUser(req.Name,req.Email)
|
||||||
|
w.Header().Set("Content-Type","application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(user)
|
||||||
|
default:
|
||||||
|
http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/users/",func(w http.ResponseWriter,r *http.Request){
|
||||||
|
if r.Method!=http.MethodGet{
|
||||||
|
http.Error(w,"Method not allowed",http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
idStr:=strings.TrimPrefix(r.URL.Path,"/users/")
|
||||||
|
id,err:=strconv.Atoi(idStr)
|
||||||
|
if err!=nil{
|
||||||
|
http.Error(w,"Invalid user ID",http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user:=service.GetUser(id)
|
||||||
|
if user==nil{
|
||||||
|
http.Error(w,"User not found",http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type","application/json")
|
||||||
|
json.NewEncoder(w).Encode(user)
|
||||||
|
})
|
||||||
|
|
||||||
|
port:=os.Getenv("PORT")
|
||||||
|
if port==""{
|
||||||
|
port="8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Server starting on port %s\\n",port)
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port,nil))
|
||||||
|
}`
|
||||||
|
formattedCode.value = ''
|
||||||
|
formatStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载有语法错误的示例
|
||||||
|
const loadBrokenSample = () => {
|
||||||
|
goCode.value = `package main
|
||||||
|
|
||||||
|
import(
|
||||||
|
"fmt"
|
||||||
|
"os
|
||||||
|
)
|
||||||
|
|
||||||
|
func main({
|
||||||
|
if len(os.Args<2{
|
||||||
|
fmt.Println("Usage: program <name>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name:=os.Args[1
|
||||||
|
fmt.Printf("Hello, %s!\\n",name)
|
||||||
|
`
|
||||||
|
formattedCode.value = ''
|
||||||
|
formatStatus.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// 清除所有测试状态
|
// 清除所有测试状态
|
||||||
const clearAll = async () => {
|
const clearAll = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -167,6 +632,10 @@ const clearAll = async () => {
|
|||||||
notificationTitle.value = ''
|
notificationTitle.value = ''
|
||||||
notificationSubtitle.value = ''
|
notificationSubtitle.value = ''
|
||||||
notificationBody.value = ''
|
notificationBody.value = ''
|
||||||
|
// 清空Go测试状态
|
||||||
|
formattedCode.value = ''
|
||||||
|
formatStatus.value = null
|
||||||
|
resetGoCode()
|
||||||
showStatus(clearStatus, 'success', 'All test states cleared successfully')
|
showStatus(clearStatus, 'success', 'All test states cleared successfully')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
|
showStatus(clearStatus, 'error', `Failed to clear test states: ${error.message || error}`)
|
||||||
@@ -207,6 +676,25 @@ const clearAll = async () => {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.code-textarea {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
min-height: 120px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
word-break: normal;
|
||||||
|
tab-size: 2;
|
||||||
|
|
||||||
|
&.result-textarea {
|
||||||
|
background-color: var(--settings-card-bg);
|
||||||
|
border-color: #22c55e;
|
||||||
|
color: var(--settings-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
@@ -271,4 +759,47 @@ const clearAll = async () => {
|
|||||||
border-color: rgba(239, 68, 68, 0.2);
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detailed-status {
|
||||||
|
.status-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 8px 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Consolas, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
.status-detail {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.detail-time {
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-message {
|
||||||
|
color: var(--settings-text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-duration {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--settings-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user