🚧 Modify toml,powershell prettier plugin(beta)
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user