From ad75e7053e5e96dbc5885ccc69e50e32415ee4a5 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Tue, 24 Dec 2024 14:28:25 +0800 Subject: [PATCH] refactor(language-core): extract `collectStyleScopedClassReferences` --- .../lib/codegen/template/element.ts | 190 +----------------- .../codegen/template/styleScopedClasses.ts | 178 +++++++++++++++- .../language-core/lib/codegen/utils/index.ts | 14 ++ 3 files changed, 193 insertions(+), 189 deletions(-) diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index cacbbd9572..da1ca30b8c 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -1,11 +1,9 @@ import * as CompilerDOM from '@vue/compiler-dom'; import { camelize, capitalize } from '@vue/shared'; -import type * as ts from 'typescript'; -import { getNodeText } from '../../parsers/scriptSetupRanges'; import type { Code, VueCodeInformation } from '../../types'; import { hyphenateTag } from '../../utils/shared'; import { createVBindShorthandInlayHintInfo } from '../inlayHints'; -import { collectVars, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../utils'; +import { collectVars, createTsAst, endOfLine, newLine, normalizeAttributeValue, variableNameRegex, wrapWith } from '../utils'; import { generateCamelized } from '../utils/camelized'; import type { TemplateCodegenContext } from './context'; import { generateElementChildren } from './elementChildren'; @@ -16,6 +14,7 @@ import type { TemplateCodegenOptions } from './index'; import { generateInterpolation } from './interpolation'; import { generateObjectProperty } from './objectProperty'; import { generatePropertyAccess } from './propertyAccess'; +import { collectStyleScopedClassReferences } from './styleScopedClasses'; import { generateTemplateChild } from './templateChild'; const colonReg = /:/g; @@ -420,7 +419,7 @@ function* generateVScope( yield* generateElementDirectives(options, ctx, node); const [refName, offset] = yield* generateReferencesForElements(options, ctx, node); // - yield* generateReferencesForScopedCssClasses(options, ctx, node); + collectStyleScopedClassReferences(options, ctx, node); if (inScope) { yield `}${newLine}`; @@ -618,142 +617,6 @@ function* generateReferencesForElements( return []; } -function* generateReferencesForScopedCssClasses( - options: TemplateCodegenOptions, - ctx: TemplateCodegenContext, - node: CompilerDOM.ElementNode -): Generator { - for (const prop of node.props) { - if ( - prop.type === CompilerDOM.NodeTypes.ATTRIBUTE - && prop.name === 'class' - && prop.value - ) { - if (options.template.lang === 'pug') { - const getClassOffset = Reflect.get(prop.value.loc.start, 'getClassOffset') as (offset: number) => number; - const content = prop.value.loc.source.slice(1, -1); - - let startOffset = 1; - for (const className of content.split(' ')) { - if (className) { - ctx.scopedClasses.push({ - source: 'template', - className, - offset: getClassOffset(startOffset), - }); - } - startOffset += className.length + 1; - } - } - else { - let isWrapped = false; - const [content, startOffset] = normalizeAttributeValue(prop.value); - if (content) { - const classes = collectClasses(content, startOffset + (isWrapped ? 1 : 0)); - ctx.scopedClasses.push(...classes); - } - else { - ctx.emptyClassOffsets.push(startOffset); - } - } - } - else if ( - prop.type === CompilerDOM.NodeTypes.DIRECTIVE - && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION - && prop.arg.content === 'class' - ) { - const content = '`${' + prop.exp.content + '}`'; - const startOffset = prop.exp.loc.start.offset - 3; - - const { ts } = options; - const ast = ts.createSourceFile('', content, 99 satisfies ts.ScriptTarget.Latest); - const literals: ts.StringLiteralLike[] = []; - - ts.forEachChild(ast, node => { - if ( - !ts.isExpressionStatement(node) || - !isTemplateExpression(node.expression) - ) { - return; - } - - const expression = node.expression.templateSpans[0].expression; - - if (ts.isStringLiteralLike(expression)) { - literals.push(expression); - } - - if (ts.isArrayLiteralExpression(expression)) { - walkArrayLiteral(expression); - } - - if (ts.isObjectLiteralExpression(expression)) { - walkObjectLiteral(expression); - } - }); - - for (const literal of literals) { - if (literal.text) { - const classes = collectClasses( - literal.text, - literal.end - literal.text.length - 1 + startOffset - ); - ctx.scopedClasses.push(...classes); - } - else { - ctx.emptyClassOffsets.push(literal.end - 1 + startOffset); - } - } - - function walkArrayLiteral(node: ts.ArrayLiteralExpression) { - const { elements } = node; - for (const element of elements) { - if (ts.isStringLiteralLike(element)) { - literals.push(element); - } - else if (ts.isObjectLiteralExpression(element)) { - walkObjectLiteral(element); - } - } - } - - function walkObjectLiteral(node: ts.ObjectLiteralExpression) { - const { properties } = node; - for (const property of properties) { - if (ts.isPropertyAssignment(property)) { - const { name } = property; - if (ts.isIdentifier(name)) { - walkIdentifier(name); - } - else if (ts.isStringLiteral(name)) { - literals.push(name); - } - else if (ts.isComputedPropertyName(name)) { - const { expression } = name; - if (ts.isStringLiteralLike(expression)) { - literals.push(expression); - } - } - } - else if (ts.isShorthandPropertyAssignment(property)) { - walkIdentifier(property.name); - } - } - } - - function walkIdentifier(node: ts.Identifier) { - const text = getNodeText(ts, node, ast); - ctx.scopedClasses.push({ - source: 'template', - className: text, - offset: node.end - text.length + startOffset - }); - } - } - } -} - function camelizeComponentName(newName: string) { return camelize('-' + newName); } @@ -761,50 +624,3 @@ function camelizeComponentName(newName: string) { function getTagRenameApply(oldName: string) { return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; } - -function normalizeAttributeValue(node: CompilerDOM.TextNode): [string, number] { - let offset = node.loc.start.offset; - let content = node.loc.source; - if ( - (content.startsWith(`'`) && content.endsWith(`'`)) - || (content.startsWith(`"`) && content.endsWith(`"`)) - ) { - offset++; - content = content.slice(1, -1); - } - return [content, offset]; -} - -function collectClasses(content: string, startOffset = 0) { - const classes: { - source: string; - className: string; - offset: number; - }[] = []; - - let currentClassName = ''; - let offset = 0; - for (const char of (content + ' ')) { - if (char.trim() === '') { - if (currentClassName !== '') { - classes.push({ - source: 'template', - className: currentClassName, - offset: offset + startOffset - }); - offset += currentClassName.length; - currentClassName = ''; - } - offset += char.length; - } - else { - currentClassName += char; - } - } - return classes; -} - -// isTemplateExpression is missing in tsc -function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { - return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression; -} \ No newline at end of file diff --git a/packages/language-core/lib/codegen/template/styleScopedClasses.ts b/packages/language-core/lib/codegen/template/styleScopedClasses.ts index d7f8cb5bd8..43f2a4ce9b 100644 --- a/packages/language-core/lib/codegen/template/styleScopedClasses.ts +++ b/packages/language-core/lib/codegen/template/styleScopedClasses.ts @@ -1,6 +1,10 @@ +import * as CompilerDOM from '@vue/compiler-dom'; +import type * as ts from 'typescript'; +import { getNodeText } from '../../parsers/scriptSetupRanges'; import type { Code } from '../../types'; -import { endOfLine } from '../utils'; +import { endOfLine, normalizeAttributeValue } from '../utils'; import type { TemplateCodegenContext } from './context'; +import type { TemplateCodegenOptions } from './index'; export function* generateStyleScopedClassReferences( ctx: TemplateCodegenContext, @@ -81,4 +85,174 @@ export function* generateStyleScopedClassReferences( } } } -} \ No newline at end of file +} + +export function collectStyleScopedClassReferences( + options: TemplateCodegenOptions, + ctx: TemplateCodegenContext, + node: CompilerDOM.ElementNode +) { + for (const prop of node.props) { + if ( + prop.type === CompilerDOM.NodeTypes.ATTRIBUTE + && prop.name === 'class' + && prop.value + ) { + if (options.template.lang === 'pug') { + const getClassOffset = Reflect.get(prop.value.loc.start, 'getClassOffset') as (offset: number) => number; + const content = prop.value.loc.source.slice(1, -1); + + let startOffset = 1; + for (const className of content.split(' ')) { + if (className) { + ctx.scopedClasses.push({ + source: 'template', + className, + offset: getClassOffset(startOffset), + }); + } + startOffset += className.length + 1; + } + } + else { + let isWrapped = false; + const [content, startOffset] = normalizeAttributeValue(prop.value); + if (content) { + const classes = collectClasses(content, startOffset + (isWrapped ? 1 : 0)); + ctx.scopedClasses.push(...classes); + } + else { + ctx.emptyClassOffsets.push(startOffset); + } + } + } + else if ( + prop.type === CompilerDOM.NodeTypes.DIRECTIVE + && prop.arg?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION + && prop.arg.content === 'class' + ) { + const content = '`${' + prop.exp.content + '}`'; + const startOffset = prop.exp.loc.start.offset - 3; + + const { ts } = options; + const ast = ts.createSourceFile('', content, 99 satisfies ts.ScriptTarget.Latest); + const literals: ts.StringLiteralLike[] = []; + + ts.forEachChild(ast, node => { + if ( + !ts.isExpressionStatement(node) || + !isTemplateExpression(node.expression) + ) { + return; + } + + const expression = node.expression.templateSpans[0].expression; + + if (ts.isStringLiteralLike(expression)) { + literals.push(expression); + } + + if (ts.isArrayLiteralExpression(expression)) { + walkArrayLiteral(expression); + } + + if (ts.isObjectLiteralExpression(expression)) { + walkObjectLiteral(expression); + } + }); + + for (const literal of literals) { + if (literal.text) { + const classes = collectClasses( + literal.text, + literal.end - literal.text.length - 1 + startOffset + ); + ctx.scopedClasses.push(...classes); + } + else { + ctx.emptyClassOffsets.push(literal.end - 1 + startOffset); + } + } + + function walkArrayLiteral(node: ts.ArrayLiteralExpression) { + const { elements } = node; + for (const element of elements) { + if (ts.isStringLiteralLike(element)) { + literals.push(element); + } + else if (ts.isObjectLiteralExpression(element)) { + walkObjectLiteral(element); + } + } + } + + function walkObjectLiteral(node: ts.ObjectLiteralExpression) { + const { properties } = node; + for (const property of properties) { + if (ts.isPropertyAssignment(property)) { + const { name } = property; + if (ts.isIdentifier(name)) { + walkIdentifier(name); + } + else if (ts.isStringLiteral(name)) { + literals.push(name); + } + else if (ts.isComputedPropertyName(name)) { + const { expression } = name; + if (ts.isStringLiteralLike(expression)) { + literals.push(expression); + } + } + } + else if (ts.isShorthandPropertyAssignment(property)) { + walkIdentifier(property.name); + } + } + } + + function walkIdentifier(node: ts.Identifier) { + const text = getNodeText(ts, node, ast); + ctx.scopedClasses.push({ + source: 'template', + className: text, + offset: node.end - text.length + startOffset + }); + } + } + } +} + +function collectClasses(content: string, startOffset = 0) { + const classes: { + source: string; + className: string; + offset: number; + }[] = []; + + let currentClassName = ''; + let offset = 0; + for (const char of (content + ' ')) { + if (char.trim() === '') { + if (currentClassName !== '') { + classes.push({ + source: 'template', + className: currentClassName, + offset: offset + startOffset + }); + offset += currentClassName.length; + currentClassName = ''; + } + offset += char.length; + } + else { + currentClassName += char; + } + } + return classes; +} + +// isTemplateExpression is missing in tsc +function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { + return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression; +} diff --git a/packages/language-core/lib/codegen/utils/index.ts b/packages/language-core/lib/codegen/utils/index.ts index e2f28894da..a39915e108 100644 --- a/packages/language-core/lib/codegen/utils/index.ts +++ b/packages/language-core/lib/codegen/utils/index.ts @@ -1,3 +1,4 @@ +import * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import { getNodeText } from '../../parsers/scriptSetupRanges'; import type { Code, SfcBlock, VueCodeInformation } from '../../types'; @@ -64,6 +65,19 @@ export function collectIdentifiers( return results; } +export function normalizeAttributeValue(node: CompilerDOM.TextNode): [string, number] { + let offset = node.loc.start.offset; + let content = node.loc.source; + if ( + (content.startsWith(`'`) && content.endsWith(`'`)) + || (content.startsWith(`"`) && content.endsWith(`"`)) + ) { + offset++; + content = content.slice(1, -1); + } + return [content, offset]; +} + export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) { if (astHolder.__volar_ast_text !== text) { astHolder.__volar_ast_text = text;