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;