feat: parser style

This commit is contained in:
“chenhuachun”
2025-04-02 16:22:56 +08:00
parent f9b9125d32
commit 4e22bc8aa2
7 changed files with 317 additions and 33 deletions

View File

@@ -34,7 +34,8 @@
"@vtj/coder": "workspace:~",
"@vtj/core": "workspace:~",
"@vue/compiler-dom": "~3.5.13",
"@vue/compiler-sfc": "~3.5.13"
"@vue/compiler-sfc": "~3.5.13",
"postcss": "~8.5.0"
},
"devDependencies": {
"@vtj/cli": "workspace:~"

View File

@@ -1,22 +1,43 @@
import { type BlockSchema, type NodeSchema, BlockModel } from '@vtj/core';
import {
type BlockSchema,
type NodeSchema,
type Dependencie,
BlockModel
} from '@vtj/core';
import { tsFormatter } from '@vtj/coder';
import { parseSFC, isJSCode } from '../shared';
import { parseTemplate } from './template';
import { parseScripts } from './scripts';
import { parseScripts, type ImportStatement } from './scripts';
import { parseStyle } from './style';
import { patchCode } from './utils';
export interface ParseVueOptions {
id: string;
name: string;
source: string;
dependencies?: Dependencie[];
}
export async function parseVue(options: ParseVueOptions) {
const { id, name, source } = options;
const { id, name, source, dependencies = [] } = options;
const sfc = parseSFC(source);
const { state, watch, lifeCycles, computed, methods, props, emits, inject } =
parseScripts(sfc.script);
const { nodes, slots, context } = parseTemplate(id, name, sfc.template);
const { styles, css } = parseStyle(sfc.styles.join('\n'));
const {
state,
watch,
lifeCycles,
computed,
methods,
props,
emits,
inject,
handlers,
imports
} = parseScripts(sfc.script);
const { nodes, slots, context } = parseTemplate(id, name, sfc.template, {
handlers,
styles
});
const dsl: BlockSchema = {
id,
name,
@@ -29,16 +50,19 @@ export async function parseVue(options: ParseVueOptions) {
methods,
slots,
emits,
nodes
nodes,
css
};
const computedKeys = Object.keys(computed || {});
const { libs } = parseDeps(imports, dependencies);
await walkDsl(dsl, async (node: NodeSchema) => {
await walkNode(node, async (content: any) => {
if (isJSCode(content)) {
const options = {
context,
computed: [],
libs: {}
computed: computedKeys,
libs
};
const code = await tsFormatter(content.value);
content.value = patchCode(code, node.id as string, options);
@@ -88,3 +112,28 @@ async function walkNode(node: NodeSchema, callback: (n: any) => Promise<void>) {
};
await walking(node);
}
function parseDeps(
imports: ImportStatement[] = [],
dependencies: Dependencie[] = []
) {
const libs: Record<string, string> = {};
const depsMap = dependencies.reduce(
(prev, current) => {
prev[current.package] = current.library;
return prev;
},
{} as Record<string, string>
);
for (const { from, imports: names } of imports) {
names.forEach((name) => {
const library = depsMap[from];
if (library) {
libs[name] = library;
}
});
}
return {
libs
};
}

View File

@@ -0,0 +1,33 @@
import postcss from 'postcss';
export type CSSRules = Record<string, Record<string, string>>;
export interface ParseStyleResult {
styles: CSSRules;
css: string;
}
export function parseStyle(content: string) {
const styles: CSSRules = {};
const css: string[] = [];
const root = postcss.parse(content);
const classRegex = /^.[\w]+_[\w]{5,}/;
for (const rule of root.nodes) {
if (rule.type === 'rule') {
const style: Record<string, string> = {};
if (classRegex.test(rule.selector)) {
rule.nodes.forEach((decl) => {
if (decl.type === 'decl') {
style[decl.prop] = decl.value;
}
});
styles[rule.selector] = style;
} else {
css.push(rule.toString());
}
}
}
return {
styles,
css: css.join('\n')
};
}

View File

@@ -4,7 +4,8 @@ import {
type NodeEvents,
type JSExpression,
type NodeDirective,
type BlockSlot
type BlockSlot,
type JSFunction
} from '@vtj/core';
import { compileTemplate } from '@vue/compiler-sfc';
import {
@@ -20,13 +21,29 @@ import {
import { uid } from '@vtj/base';
import { isJSExpression, isNodeSchema } from '../shared';
import { getJSExpression, getJSFunction } from './utils';
import type { CSSRules } from './style';
let __slots: BlockSlot[] = [];
let __context: Record<string, Set<string>> = {};
let __handlers: Record<string, JSFunction> = {};
let __styles: CSSRules = {};
export function parseTemplate(id: string, name: string, content: string = '') {
export interface ParseTemplateOptions {
handlers?: Record<string, JSFunction>;
styles?: CSSRules;
}
export function parseTemplate(
id: string,
name: string,
content: string = '',
options?: ParseTemplateOptions
) {
__slots = [];
__context = {};
__handlers = options?.handlers || {};
__styles = options?.styles || {};
const result = compileTemplate({
id,
filename: name,
@@ -64,7 +81,21 @@ function getProps(nodes: Array<AttributeNode | DirectiveNode>) {
for (const item of nodes) {
// 普通属性
if (item.type === NodeTypes.ATTRIBUTE) {
props[item.name] = item.value?.content || '';
if (item.name === 'class') {
const classValue = item.value?.content || '';
const classRegex = /[\w]+_[\w]{5,}/;
const selector = classValue.match(classRegex)?.[0] || '';
const classes = classValue.split(' ').filter((n) => n !== selector);
const style = __styles[`.${selector}`];
if (style) {
props.style = style;
}
if (classes.length) {
props.class = classes.join(' ');
}
} else {
props[item.name] = item.value?.content || '';
}
}
// 动态绑定的属性
@@ -81,7 +112,10 @@ function getProps(nodes: Array<AttributeNode | DirectiveNode>) {
return props;
}
function getEvents(nodes: Array<AttributeNode | DirectiveNode>) {
function getEvents(
nodes: Array<AttributeNode | DirectiveNode>,
handlers: Record<string, JSFunction> = {}
) {
const events: NodeEvents = {};
for (const item of nodes) {
// 动态绑定的属性
@@ -97,11 +131,23 @@ function getEvents(nodes: Array<AttributeNode | DirectiveNode>) {
},
{} as Record<string, boolean>
);
events[item.arg.content] = {
name: item.arg.content,
handler: getJSFunction(`(${item.exp?.loc.source})`),
modifiers
};
const code = item.exp?.loc.source || '';
const regex = new RegExp(`${item.arg.content}_\[\\w\]\{5\,\}`);
const name = code.match(regex)?.[0] || '';
const handler = handlers[name];
if (name && handler) {
events[item.arg.content] = {
name: item.arg.content,
handler,
modifiers
};
} else {
events[item.arg.content] = {
name: item.arg.content,
handler: getJSFunction(`(${code})`),
modifiers
};
}
}
}
}
@@ -232,7 +278,7 @@ function createNodeSchema(
const dsl: NodeSchema = {
name: node.tag,
props: getProps(node.props),
events: getEvents(node.props),
events: getEvents(node.props, __handlers),
directives: getDirectives(scope || node)
};
dsl.id = getNodeId(dsl);

View File

@@ -2,11 +2,7 @@ import { expect, test } from 'vitest';
import { tsFormatter } from '@vtj/coder';
import { parseUniApp, parseVue } from '../src';
import { App } from './UniApp';
import { template1 } from './template';
const dependencies = {
'element-plus': ['ElInput', 'ElButton']
};
import { template1, dependencies } from './template';
// test('index', async () => {
// const result = parseUniApp(App);
@@ -19,7 +15,8 @@ test('template1', async () => {
const result = await parseVue({
id: '235w0t1w',
name: 'Bbb',
source: template1
source: template1,
dependencies: dependencies as any
});
console.log(JSON.stringify(result, null, 2));

View File

@@ -1,42 +1,197 @@
export const dependencies = [
{
package: 'vue',
version: 'latest',
library: 'Vue',
urls: ['@vtj/materials/deps/vue/vue.global.prod.js'],
assetsLibrary: 'VueMaterial',
required: true,
official: true,
enabled: true,
platform: ['web', 'h5']
},
{
package: 'vue-router',
version: 'latest',
library: 'VueRouter',
urls: ['@vtj/materials/deps/vue-router/vue-router.global.prod.js'],
assetsLibrary: 'VueRouterMaterial',
required: true,
official: true,
enabled: true
},
{
package: '@vtj/utils',
version: 'latest',
library: 'VtjUtils',
urls: ['@vtj/materials/deps/@vtj/utils/index.umd.js'],
required: true,
official: true,
enabled: true
},
{
package: '@vtj/icons',
version: 'latest',
library: 'VtjIcons',
urls: [
'@vtj/materials/deps/@vtj/icons/style.css',
'@vtj/materials/deps/@vtj/icons/index.umd.js'
],
required: true,
official: true,
enabled: true,
platform: ['web', 'h5']
},
{
package: '@vueuse/core',
version: 'latest',
library: 'VueUse',
urls: [
'@vtj/materials/deps/@vueuse/shared/index.iife.min.js',
'@vtj/materials/deps/@vueuse/core/index.iife.min.js'
],
required: false,
official: true,
enabled: true,
platform: ['web', 'h5']
},
{
package: 'element-plus',
version: 'latest',
library: 'ElementPlus',
localeLibrary: 'ElementPlusLocaleZhCn',
urls: [
'@vtj/materials/deps/element-plus/dark/css-vars.css',
'@vtj/materials/deps/element-plus/index.css',
'@vtj/materials/deps/element-plus/zh-cn.js',
'@vtj/materials/deps/element-plus/index.full.min.js'
],
assetsUrl: '@vtj/materials/assets/element/index.umd.js',
assetsLibrary: 'ElementPlusMaterial',
required: false,
official: true,
enabled: true,
platform: 'web'
},
{
package: '@vtj/ui',
version: 'latest',
library: 'VtjUI',
urls: [
'@vtj/materials/deps/vxe-table/style.min.css',
'@vtj/materials/deps/@vtj/ui/style.css',
'@vtj/materials/deps/xe-utils/xe-utils.umd.min.js',
'@vtj/materials/deps/vxe-table/index.umd.min.js',
'@vtj/materials/deps/@vtj/ui/index.umd.js'
],
assetsUrl: '@vtj/materials/assets/ui/index.umd.js',
assetsLibrary: 'VtjUIMaterial',
required: false,
official: true,
enabled: true,
platform: 'web'
},
{
package: 'ant-design-vue',
version: 'latest',
library: 'antd',
urls: [
'@vtj/materials/deps/ant-design-vue/reset.css',
'@vtj/materials/deps/ant-design-vue/dayjs/dayjs.min.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/customParseFormat.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekday.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/localeData.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekOfYear.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/weekYear.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/advancedFormat.js',
'@vtj/materials/deps/ant-design-vue/dayjs/plugin/quarterOfYear.js',
'@vtj/materials/deps/ant-design-vue/antd.min.js'
],
assetsUrl: '@vtj/materials/assets/antdv/index.umd.js',
assetsLibrary: 'AntdvMaterial',
required: false,
official: true,
enabled: false,
platform: ['web']
},
{
package: '@vtj/charts',
version: 'latest',
library: 'VtjCharts',
urls: [
'@vtj/materials/deps/echarts/echarts.min.js',
'@vtj/materials/deps/@vtj/charts/index.umd.js'
],
assetsUrl: '@vtj/materials/assets/charts/index.umd.js',
assetsLibrary: 'VtjChartsMaterial',
required: false,
official: true,
enabled: true,
platform: ['web', 'h5']
},
{
package: 'mockjs',
version: 'latest',
library: 'Mock',
urls: ['@vtj/materials/deps/mockjs/mock-min.js'],
required: false,
official: true,
enabled: true
}
];
export const template1 = `
<template>
<XPanel
v-for="(item, index) in 3"
header="标题"
@click.stop="(...args: any[]) => click_13mntm28({ item, index }, args)">
@click.stop="(...args: any[]) => click_13mxuu2q({ item, index }, args)">
<div class="my-div div_193l8saav">
<span> {{ item }}</span>
</div></XPanel
>
<ElButton type="primary" @click="click_13mph5o7"> 按钮</ElButton>
<ElButton type="primary" @click="click_33mxuu2q"> 按钮</ElButton>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent, reactive } from 'vue';
import { XPanel } from '@vtj/ui';
import { ElButton } from 'element-plus';
import { dateFormat } from '@vtj/utils';
import { useProvider } from '@vtj/renderer';
export default defineComponent({
name: 'Bbb',
components: { XPanel, ElButton },
setup(props) {
const provider = useProvider({ id: '13dbje0g', version: '1743564663400' });
const provider = useProvider({ id: '13dbje0g', version: '1743578537999' });
const state = reactive({});
return { state, props, provider };
return { state, props, provider, dateFormat };
},
methods: {
click_13mntm28({ item, index }, args) {
click_13mxuu2q({ item, index }, args) {
return (() => {
console.log('click panel!', item);
}).apply(this, args);
},
click_13mph5o7(e) {
click_33mxuu2q(e) {
console.log('click button!', e);
console.log(dateFormat(new Date(), 'YYYY-MM-DD'));
}
}
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.my-div {
color: red;
}
.div_193l8saav {
padding-top: 20px;
padding-bottom: 20px;
padding-left: 20px;
padding-right: 20px;
}
</style>
`;