Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ declare global {
| 'angular'
| 'html'
| 'vue'
| 'css'
| 'scss'
| 'less'
| 'oxc'
| 'oxc-ts'
| 'astro'
Expand Down
185 changes: 185 additions & 0 deletions src/core-parts/finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,62 @@ function handleAstroElement(ctx: CaseHandlerContext) {
}
}

function handleCssCssAtrule(ctx: CaseHandlerContext) {
ctx.nonCommentNodes.push(ctx.currentASTNode);

if (
isTypeof(
ctx.node,
z.object({
name: z.literal('apply'),
nodes: z.undefined(),
raws: z.object({
afterName: z.string(),
params: z.string(),
}),
source: z.object({
start: z.object({
line: z.number(),
}),
}),
}),
)
) {
// Note: In fact, the `@apply` rule is not a `keywordStartingNode`, but it is considered a kind of safe list to maintain the `classNameNode`s obtained from the code inside the rule.
ctx.keywordStartingNodes.push(ctx.currentASTNode);

const offset = '@apply'.length;

const classNameNodeRangeStart = ctx.currentASTNode.start + offset;
const classNameNodeRangeEnd = ctx.currentASTNode.end;

const nodeStartLineIndex = ctx.node.source.start.line - 1;

// Note: In fact, since CSS code does not have a delimiter, it might be better to create a new node type. However, if we consider the characters on the left and right of the class name as a delimiter, the formatting method is the same as AttributeNode, so I have processed it as an AttributeNode type for now.
ctx.classNameNodes.push({
type: 'attribute',
isTheFirstLineOnTheSameLineAsTheOpeningTag: true,
elementName: '',
range: [classNameNodeRangeStart, classNameNodeRangeEnd],
startLineIndex: nodeStartLineIndex,
});
}
}

function handleCssCssComment(ctx: CaseHandlerContext) {
if (
isTypeof(
ctx.node,
z.object({
text: z.string(),
}),
) &&
ctx.node.text.trim() === 'prettier-ignore'
) {
ctx.prettierIgnoreNodes.push(ctx.currentASTNode);
}
}

function handleAstroFrontmatter(ctx: CaseHandlerContext) {
ctx.nonCommentNodes.push(ctx.currentASTNode);

Expand Down Expand Up @@ -1747,6 +1803,11 @@ const typescriptCaseHandlers: CaseHandlers = {
Line: handleTypeScriptBlock,
};

const cssCaseHandlers: CaseHandlers = {
'css-atrule': handleCssCssAtrule,
'css-comment': handleCssCssComment,
};

const parserCaseHandlers: ParserCaseHandlers = {
babel: {
...babelCaseHandlers,
Expand Down Expand Up @@ -1800,6 +1861,15 @@ const parserCaseHandlers: ParserCaseHandlers = {
element: handleAngularElement,
comment: handleHtmlComment,
},
css: {
...cssCaseHandlers,
},
scss: {
...cssCaseHandlers,
},
less: {
...cssCaseHandlers,
},
astro: {
frontmatter: handleAstroFrontmatter,
attribute: handleAstroAttribute,
Expand Down Expand Up @@ -2190,6 +2260,121 @@ export function findTargetClassNameNodesBasedOnHtml(
);
}

export function findTargetClassNameNodesBasedOnCss(
formattedText: string,
ast: AST,
options: ResolvedOptions,
): ClassNameNode[] {
const supportedAttributes: string[] = ['class', 'className', ...options.customAttributes];
const supportedFunctions: string[] = ['classNames', ...options.customFunctions];
/**
* Most nodes
*/
const nonCommentNodes: ASTNode[] = [];
/**
* Nodes with a valid 'prettier-ignore' syntax
*/
const prettierIgnoreNodes: ASTNode[] = [];
/**
* Nodes starting with supported attribute names or supported function names
*/
const keywordStartingNodes: ASTNode[] = [];
/**
* Class names enclosed in delimiters
*/
const classNameNodes: ClassNameNode[] = [];

function recursion(node: unknown, parentNode?: { type: string }): void {
if (!isTypeof(node, z.object({ type: z.string() }))) {
return;
}

let recursiveProps: string[] = [];

switch (node.type) {
case 'css-atrule':
case 'css-root':
case 'css-rule': {
recursiveProps = ['nodes'];
break;
}
default: {
break;
}
}

Object.entries(node).forEach(([key, value]) => {
if (!recursiveProps.includes(key)) {
return;
}

if (Array.isArray(value)) {
value.forEach((childNode: unknown) => {
recursion(childNode, node);
});
return;
}

recursion(value, node);
});

if (
!isTypeof(
node,
z.object({
source: z.object({
startOffset: z.number(),
endOffset: z.number(),
}),
}),
)
) {
return;
}

const nodeType = node.type;
const currentNodeRangeStart = node.source.startOffset;
const currentNodeRangeEnd = node.source.endOffset;

const currentASTNode: ASTNode = {
type: nodeType,
start: currentNodeRangeStart,
end: currentNodeRangeEnd,
};

const handler = parserCaseHandlers[String(options.parser)]?.[nodeType];

if (handler) {
const context: CaseHandlerContext = {
formattedText,
options,
supportedAttributes,
supportedFunctions,
nonCommentNodes,
prettierIgnoreNodes,
keywordStartingNodes,
classNameNodes,
node,
parentNode,
currentASTNode,
};

handler(context);
} else {
nonCommentNodes.push(currentASTNode);
}
}

recursion(ast);

return filterAndSortClassNameNodes(
nonCommentNodes,
prettierIgnoreNodes,
keywordStartingNodes,
classNameNodes,
);
}

export function findTargetClassNameNodesBasedOnAstro(
formattedText: string,
ast: AST,
Expand Down
7 changes: 7 additions & 0 deletions src/core-parts/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { AST } from 'prettier';
import {
findTargetClassNameNodesBasedOnJavaScript,
findTargetClassNameNodesBasedOnHtml,
findTargetClassNameNodesBasedOnCss,
findTargetClassNameNodesBasedOnAstro,
} from './finder';
import {
Expand Down Expand Up @@ -645,6 +646,12 @@ export async function parseLineByLineAndReplaceAsync({
targetClassNameNodes = findTargetClassNameNodesBasedOnHtml(formattedText, ast, options);
break;
}
case 'css':
case 'scss':
case 'less': {
targetClassNameNodes = findTargetClassNameNodesBasedOnCss(formattedText, ast, options);
break;
}
case 'astro': {
targetClassNameNodes = findTargetClassNameNodesBasedOnAstro(formattedText, ast, options);
break;
Expand Down
11 changes: 11 additions & 0 deletions src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Parser, Plugin } from 'prettier';
import { format } from 'prettier';
import { parsers as babelParsers } from 'prettier/plugins/babel';
import { parsers as htmlParsers } from 'prettier/plugins/html';
import { parsers as postcssParsers } from 'prettier/plugins/postcss';
import { parsers as typescriptParsers } from 'prettier/plugins/typescript';

import { advancedParse } from './core-parts/parser';
Expand Down Expand Up @@ -34,6 +35,7 @@ function transformParser(
...(refinedParser ?? {}),
// @ts-expect-error
parse: async (text: string, options: ResolvedOptions): Promise<FormattedTextAST> => {
// NOTE: This statement is deprecated and will be removed in version 0.12.0. There are still no plans to support the `markdown` and `mdx` parsers. I just thought it would be better to guide users to override Prettier's configuration rather than branching inside this plugin.
if (options.parentParser === 'markdown' || options.parentParser === 'mdx') {
let codeblockStart = '```';
const codeblockEnd = '```';
Expand Down Expand Up @@ -187,6 +189,15 @@ export const parsers: { [parserName: string]: Parser } = {
vue: transformParser('vue', {
defaultParser: htmlParsers.vue,
}),
css: transformParser('css', {
defaultParser: postcssParsers.css,
}),
scss: transformParser('scss', {
defaultParser: postcssParsers.scss,
}),
less: transformParser('less', {
defaultParser: postcssParsers.less,
}),
oxc: transformParser('oxc', {
defaultParser: null,
externalPluginName: '@prettier/plugin-oxc',
Expand Down
Loading
Loading