Skip to content

Commit 09b6eb2

Browse files
authored
Merge pull request #137 from ony3000/support-css
Support css, scss, and less
2 parents 7564db0 + 27ea51a commit 09b6eb2

159 files changed

Lines changed: 10591 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

global.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ declare global {
4242
| 'angular'
4343
| 'html'
4444
| 'vue'
45+
| 'css'
46+
| 'scss'
47+
| 'less'
4548
| 'oxc'
4649
| 'oxc-ts'
4750
| 'astro'

src/core-parts/finder.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,62 @@ function handleAstroElement(ctx: CaseHandlerContext) {
16841684
}
16851685
}
16861686

1687+
function handleCssCssAtrule(ctx: CaseHandlerContext) {
1688+
ctx.nonCommentNodes.push(ctx.currentASTNode);
1689+
1690+
if (
1691+
isTypeof(
1692+
ctx.node,
1693+
z.object({
1694+
name: z.literal('apply'),
1695+
nodes: z.undefined(),
1696+
raws: z.object({
1697+
afterName: z.string(),
1698+
params: z.string(),
1699+
}),
1700+
source: z.object({
1701+
start: z.object({
1702+
line: z.number(),
1703+
}),
1704+
}),
1705+
}),
1706+
)
1707+
) {
1708+
// 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.
1709+
ctx.keywordStartingNodes.push(ctx.currentASTNode);
1710+
1711+
const offset = '@apply'.length;
1712+
1713+
const classNameNodeRangeStart = ctx.currentASTNode.start + offset;
1714+
const classNameNodeRangeEnd = ctx.currentASTNode.end;
1715+
1716+
const nodeStartLineIndex = ctx.node.source.start.line - 1;
1717+
1718+
// 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.
1719+
ctx.classNameNodes.push({
1720+
type: 'attribute',
1721+
isTheFirstLineOnTheSameLineAsTheOpeningTag: true,
1722+
elementName: '',
1723+
range: [classNameNodeRangeStart, classNameNodeRangeEnd],
1724+
startLineIndex: nodeStartLineIndex,
1725+
});
1726+
}
1727+
}
1728+
1729+
function handleCssCssComment(ctx: CaseHandlerContext) {
1730+
if (
1731+
isTypeof(
1732+
ctx.node,
1733+
z.object({
1734+
text: z.string(),
1735+
}),
1736+
) &&
1737+
ctx.node.text.trim() === 'prettier-ignore'
1738+
) {
1739+
ctx.prettierIgnoreNodes.push(ctx.currentASTNode);
1740+
}
1741+
}
1742+
16871743
function handleAstroFrontmatter(ctx: CaseHandlerContext) {
16881744
ctx.nonCommentNodes.push(ctx.currentASTNode);
16891745

@@ -1747,6 +1803,11 @@ const typescriptCaseHandlers: CaseHandlers = {
17471803
Line: handleTypeScriptBlock,
17481804
};
17491805

1806+
const cssCaseHandlers: CaseHandlers = {
1807+
'css-atrule': handleCssCssAtrule,
1808+
'css-comment': handleCssCssComment,
1809+
};
1810+
17501811
const parserCaseHandlers: ParserCaseHandlers = {
17511812
babel: {
17521813
...babelCaseHandlers,
@@ -1800,6 +1861,15 @@ const parserCaseHandlers: ParserCaseHandlers = {
18001861
element: handleAngularElement,
18011862
comment: handleHtmlComment,
18021863
},
1864+
css: {
1865+
...cssCaseHandlers,
1866+
},
1867+
scss: {
1868+
...cssCaseHandlers,
1869+
},
1870+
less: {
1871+
...cssCaseHandlers,
1872+
},
18031873
astro: {
18041874
frontmatter: handleAstroFrontmatter,
18051875
attribute: handleAstroAttribute,
@@ -2190,6 +2260,121 @@ export function findTargetClassNameNodesBasedOnHtml(
21902260
);
21912261
}
21922262

2263+
export function findTargetClassNameNodesBasedOnCss(
2264+
formattedText: string,
2265+
ast: AST,
2266+
options: ResolvedOptions,
2267+
): ClassNameNode[] {
2268+
const supportedAttributes: string[] = ['class', 'className', ...options.customAttributes];
2269+
const supportedFunctions: string[] = ['classNames', ...options.customFunctions];
2270+
/**
2271+
* Most nodes
2272+
*/
2273+
const nonCommentNodes: ASTNode[] = [];
2274+
/**
2275+
* Nodes with a valid 'prettier-ignore' syntax
2276+
*/
2277+
const prettierIgnoreNodes: ASTNode[] = [];
2278+
/**
2279+
* Nodes starting with supported attribute names or supported function names
2280+
*/
2281+
const keywordStartingNodes: ASTNode[] = [];
2282+
/**
2283+
* Class names enclosed in delimiters
2284+
*/
2285+
const classNameNodes: ClassNameNode[] = [];
2286+
2287+
function recursion(node: unknown, parentNode?: { type: string }): void {
2288+
if (!isTypeof(node, z.object({ type: z.string() }))) {
2289+
return;
2290+
}
2291+
2292+
let recursiveProps: string[] = [];
2293+
2294+
switch (node.type) {
2295+
case 'css-atrule':
2296+
case 'css-root':
2297+
case 'css-rule': {
2298+
recursiveProps = ['nodes'];
2299+
break;
2300+
}
2301+
default: {
2302+
break;
2303+
}
2304+
}
2305+
2306+
Object.entries(node).forEach(([key, value]) => {
2307+
if (!recursiveProps.includes(key)) {
2308+
return;
2309+
}
2310+
2311+
if (Array.isArray(value)) {
2312+
value.forEach((childNode: unknown) => {
2313+
recursion(childNode, node);
2314+
});
2315+
return;
2316+
}
2317+
2318+
recursion(value, node);
2319+
});
2320+
2321+
if (
2322+
!isTypeof(
2323+
node,
2324+
z.object({
2325+
source: z.object({
2326+
startOffset: z.number(),
2327+
endOffset: z.number(),
2328+
}),
2329+
}),
2330+
)
2331+
) {
2332+
return;
2333+
}
2334+
2335+
const nodeType = node.type;
2336+
const currentNodeRangeStart = node.source.startOffset;
2337+
const currentNodeRangeEnd = node.source.endOffset;
2338+
2339+
const currentASTNode: ASTNode = {
2340+
type: nodeType,
2341+
start: currentNodeRangeStart,
2342+
end: currentNodeRangeEnd,
2343+
};
2344+
2345+
const handler = parserCaseHandlers[String(options.parser)]?.[nodeType];
2346+
2347+
if (handler) {
2348+
const context: CaseHandlerContext = {
2349+
formattedText,
2350+
options,
2351+
supportedAttributes,
2352+
supportedFunctions,
2353+
nonCommentNodes,
2354+
prettierIgnoreNodes,
2355+
keywordStartingNodes,
2356+
classNameNodes,
2357+
node,
2358+
parentNode,
2359+
currentASTNode,
2360+
};
2361+
2362+
handler(context);
2363+
} else {
2364+
nonCommentNodes.push(currentASTNode);
2365+
}
2366+
}
2367+
2368+
recursion(ast);
2369+
2370+
return filterAndSortClassNameNodes(
2371+
nonCommentNodes,
2372+
prettierIgnoreNodes,
2373+
keywordStartingNodes,
2374+
classNameNodes,
2375+
);
2376+
}
2377+
21932378
export function findTargetClassNameNodesBasedOnAstro(
21942379
formattedText: string,
21952380
ast: AST,

src/core-parts/processor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AST } from 'prettier';
44
import {
55
findTargetClassNameNodesBasedOnJavaScript,
66
findTargetClassNameNodesBasedOnHtml,
7+
findTargetClassNameNodesBasedOnCss,
78
findTargetClassNameNodesBasedOnAstro,
89
} from './finder';
910
import {
@@ -645,6 +646,12 @@ export async function parseLineByLineAndReplaceAsync({
645646
targetClassNameNodes = findTargetClassNameNodesBasedOnHtml(formattedText, ast, options);
646647
break;
647648
}
649+
case 'css':
650+
case 'scss':
651+
case 'less': {
652+
targetClassNameNodes = findTargetClassNameNodesBasedOnCss(formattedText, ast, options);
653+
break;
654+
}
648655
case 'astro': {
649656
targetClassNameNodes = findTargetClassNameNodesBasedOnAstro(formattedText, ast, options);
650657
break;

src/parsers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Parser, Plugin } from 'prettier';
22
import { format } from 'prettier';
33
import { parsers as babelParsers } from 'prettier/plugins/babel';
44
import { parsers as htmlParsers } from 'prettier/plugins/html';
5+
import { parsers as postcssParsers } from 'prettier/plugins/postcss';
56
import { parsers as typescriptParsers } from 'prettier/plugins/typescript';
67

78
import { advancedParse } from './core-parts/parser';
@@ -34,6 +35,7 @@ function transformParser(
3435
...(refinedParser ?? {}),
3536
// @ts-expect-error
3637
parse: async (text: string, options: ResolvedOptions): Promise<FormattedTextAST> => {
38+
// 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.
3739
if (options.parentParser === 'markdown' || options.parentParser === 'mdx') {
3840
let codeblockStart = '```';
3941
const codeblockEnd = '```';
@@ -187,6 +189,15 @@ export const parsers: { [parserName: string]: Parser } = {
187189
vue: transformParser('vue', {
188190
defaultParser: htmlParsers.vue,
189191
}),
192+
css: transformParser('css', {
193+
defaultParser: postcssParsers.css,
194+
}),
195+
scss: transformParser('scss', {
196+
defaultParser: postcssParsers.scss,
197+
}),
198+
less: transformParser('less', {
199+
defaultParser: postcssParsers.less,
200+
}),
190201
oxc: transformParser('oxc', {
191202
defaultParser: null,
192203
externalPluginName: '@prettier/plugin-oxc',

0 commit comments

Comments
 (0)