diff --git a/packages/uni-nvue-styler/__tests__/normalize.spec.ts b/packages/uni-nvue-styler/__tests__/normalize.spec.ts index 44939af1f514f411bd9c4e77c7145f8d108039a4..e9b07d03e725bcfc1018a1b100b0f32ccf79852d 100644 --- a/packages/uni-nvue-styler/__tests__/normalize.spec.ts +++ b/packages/uni-nvue-styler/__tests__/normalize.spec.ts @@ -264,6 +264,17 @@ zIndex: 4; } `) expect(json).toEqual({ + '@TRANSITION': { + bar: { + property: 'height', + }, + foo: { + property: 'marginTop', + }, + foobar: { + property: 'marginTop,height', + }, + }, foo: { transitionProperty: 'marginTop', }, @@ -293,6 +304,15 @@ zIndex: 4; } `) expect(json).toEqual({ + '@TRANSITION': { + bar: { + duration: 200, + }, + foo: { + delay: 500, + duration: 200, + }, + }, foo: { transitionDuration: 200, transitionDelay: 500, @@ -330,6 +350,14 @@ zIndex: 4; } `) expect(json).toEqual({ + '@TRANSITION': { + bar: { + timingFunction: 'cubic-bezier(0.88,1,-0.67,1.37)', + }, + foo: { + timingFunction: 'ease-in-out', + }, + }, foo: { transitionTimingFunction: 'ease-in-out', }, diff --git a/packages/uni-nvue-styler/__tests__/objectifier.spec.ts b/packages/uni-nvue-styler/__tests__/objectifier.spec.ts index 8753695af4ac2bd265567047db0fe05bd3fc03d2..31f3c5c6f60a1dd1cbd425c3b85a7929ee03235f 100644 --- a/packages/uni-nvue-styler/__tests__/objectifier.spec.ts +++ b/packages/uni-nvue-styler/__tests__/objectifier.spec.ts @@ -93,6 +93,14 @@ describe('nvue-styler: parse', () => { '.foo {transition-property: margin-top; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in;}' const { json, messages } = await objectifierRoot(code) expect(json).toEqual({ + '@TRANSITION': { + foo: { + delay: 200, + duration: 300, + property: 'marginTop', + timingFunction: 'ease-in', + }, + }, foo: { transitionDelay: 200, transitionDuration: 300, @@ -113,4 +121,233 @@ describe('nvue-styler: parse', () => { }) ) }) + test('transition transform', async () => { + const code = + '.foo {transition-property: transform; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in-out;}' + const { json, messages } = await objectifierRoot(code) + expect(json).toEqual({ + '@TRANSITION': { + foo: { + property: 'transform', + duration: 300, + delay: 200, + timingFunction: 'ease-in-out', + }, + }, + foo: { + transitionDelay: 200, + transitionDuration: 300, + transitionProperty: 'transform', + transitionTimingFunction: 'ease-in-out', + }, + }) + expect(messages[0]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `300ms` is autofixed to `300`', + }) + ) + expect(messages[1]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `0.2s` is autofixed to `200`', + }) + ) + }) + test('multi transition properties', async () => { + const code = + '.foo {transition-property: margin-top, height; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in-out;}' + const { json, messages } = await objectifierRoot(code) + expect(json).toEqual({ + '@TRANSITION': { + foo: { + property: 'marginTop,height', + duration: 300, + delay: 200, + timingFunction: 'ease-in-out', + }, + }, + foo: { + transitionDelay: 200, + transitionDuration: 300, + transitionProperty: 'marginTop,height', + transitionTimingFunction: 'ease-in-out', + }, + }) + expect(messages[0]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `300ms` is autofixed to `300`', + }) + ) + expect(messages[1]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `0.2s` is autofixed to `200`', + }) + ) + }) + test('complex transition', async () => { + const code = + '.foo {font-size: 20; color: #000000}\n\n .foo, .bar {color: #ff5000; height: 30; transition-property: margin-top; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in;}' + const { json, messages } = await objectifierRoot(code) + expect(json).toEqual({ + '@TRANSITION': { + foo: { + property: 'marginTop', + duration: 300, + delay: 200, + timingFunction: 'ease-in', + }, + bar: { + property: 'marginTop', + duration: 300, + delay: 200, + timingFunction: 'ease-in', + }, + }, + foo: { + fontSize: 20, + color: '#ff5000', + height: 30, + transitionDelay: 200, + transitionDuration: 300, + transitionProperty: 'marginTop', + transitionTimingFunction: 'ease-in', + }, + bar: { + color: '#ff5000', + height: 30, + transitionDelay: 200, + transitionDuration: 300, + transitionProperty: 'marginTop', + transitionTimingFunction: 'ease-in', + }, + }) + expect(messages[0]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `300ms` is autofixed to `300`', + }) + ) + expect(messages[1]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `0.2s` is autofixed to `200`', + }) + ) + }) + test('transition shorthand', async () => { + const code = + '.foo {font-size: 20; transition: margin-top 500ms ease-in-out 1s}' + const { json, messages } = await objectifierRoot(code) + expect(json).toEqual({ + '@TRANSITION': { + foo: { + property: 'marginTop', + duration: 500, + delay: 1000, + timingFunction: 'ease-in-out', + }, + }, + foo: { + fontSize: 20, + transitionDelay: 1000, + transitionDuration: 500, + transitionProperty: 'marginTop', + transitionTimingFunction: 'ease-in-out', + }, + }) + expect(messages[0]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `500ms` is autofixed to `500`', + }) + ) + expect(messages[1]).toEqual( + expect.objectContaining({ + type: 'warning', + text: 'NOTE: property value `1s` is autofixed to `1000`', + }) + ) + }) + test('padding & margin shorthand', async () => { + const code = + '.foo { padding: 20px; margin: 30px 40; } .bar { margin: 10px 20 30; padding: 10 20px 30px 40;}' + const { json } = await objectifierRoot(code) + expect(json).toEqual({ + foo: { + paddingTop: '20px', + paddingRight: '20px', + paddingBottom: '20px', + paddingLeft: '20px', + marginTop: '30px', + marginRight: 40, + marginBottom: '30px', + marginLeft: 40, + }, + bar: { + paddingTop: 10, + paddingRight: '20px', + paddingBottom: '30px', + paddingLeft: 40, + marginTop: '10px', + marginRight: 20, + marginBottom: 30, + marginLeft: 20, + }, + }) + }) + test('override padding & margin shorthand', async () => { + const code = + '.foo { padding: 20px; padding-left: 30px; } .bar { margin: 10px 20; margin-bottom: 30px;}' + const { json } = await objectifierRoot(code) + expect(json).toEqual({ + foo: { + paddingTop: '20px', + paddingRight: '20px', + paddingBottom: '20px', + paddingLeft: '30px', + }, + bar: { + marginTop: '10px', + marginRight: 20, + marginBottom: '30px', + marginLeft: 20, + }, + }) + }) + test('pseudo class', async () => { + const code = + '.class-a {color: #0000ff;} .class-a:last-child:focus {color: #ff0000;}' + const { json } = await objectifierRoot(code) + expect(json).toEqual({ + 'class-a': { + color: '#0000ff', + 'color:last-child:focus': '#ff0000', + }, + }) + }) + test('iconfont', async () => { + const code = + '@font-face {font-family: "font-family-name-1"; src: url("font file url 1-1") format("truetype");} @font-face {font-family: "font-family-name-2"; src: url("font file url 2-1") format("truetype"), url("font file url 2-2") format("woff");}' + const { json } = await objectifierRoot(code) + expect(json).toEqual({ + '@FONT-FACE': [ + { + fontFamily: 'font-family-name-1', + src: 'url("font file url 1-1") format("truetype")', + }, + { + fontFamily: 'font-family-name-2', + src: 'url("font file url 2-1") format("truetype"), url("font file url 2-2") format("woff")', + }, + ], + }) + }) + test('syntax error', async () => { + const code = 'asdf' + const { messages } = await objectifierRoot(code) + expect(messages[0].text).toContain('Unknown word') + }) }) diff --git a/packages/uni-nvue-styler/__tests__/utils.ts b/packages/uni-nvue-styler/__tests__/utils.ts index 41bdd701f9bca09b99955a35ef6b00361ff21292..5f265c353f0e9b3eea058a3df9c3800760eb5175 100644 --- a/packages/uni-nvue-styler/__tests__/utils.ts +++ b/packages/uni-nvue-styler/__tests__/utils.ts @@ -1,10 +1,19 @@ import postcss from 'postcss' import { expand, normalize } from '../src' export function parseCss(input: string, filename: string = 'foo.css') { - return postcss([ - expand, - normalize({ descendant: false, logLevel: 'NOTE' }), - ]).process(input, { - from: filename, - }) + return postcss([expand, normalize({ descendant: false, logLevel: 'NOTE' })]) + .process(input, { + from: filename, + }) + .catch((err: any) => { + return { + root: null, + messages: [ + { + type: 'warning', + text: err.message, + }, + ], + } + }) } diff --git a/packages/uni-nvue-styler/src/objectifier.ts b/packages/uni-nvue-styler/src/objectifier.ts index 55a03dcbf4e51789ba58119b972125b11a0ec2a0..b592dc9cdfef2d2852eea00dba18038784f38253 100644 --- a/packages/uni-nvue-styler/src/objectifier.ts +++ b/packages/uni-nvue-styler/src/objectifier.ts @@ -1,57 +1,78 @@ -import { AtRule, Container, Root, Document } from 'postcss' -import { extend, hasOwn, isArray } from './utils' +import { Container, Root, Document } from 'postcss' +import { extend } from './utils' interface ObjectifierContext { - TRANSITION: Record> + 'FONT-FACE': Record[] + TRANSITION: Record> } -export function objectifier( +export function objectifier(node: Root | Document | Container | null) { + if (!node) { + return {} + } + const context: ObjectifierContext = { 'FONT-FACE': [], TRANSITION: {} } + const result = transform(node, context) + if (context['FONT-FACE'].length) { + result['@FONT-FACE'] = context['FONT-FACE'] + } + if (Object.keys(context.TRANSITION).length) { + result['@TRANSITION'] = context.TRANSITION + } + return result +} + +function transform( node: Root | Document | Container, - context: ObjectifierContext = { TRANSITION: {} } + context: ObjectifierContext ) { - let name: string const result: Record | unknown> = {} node.each((child) => { if (child.type === 'atrule') { - name = '@' + child.name - if (child.params) name += ' ' + child.params - if (!hasOwn(result, name)) { - result[name] = atRule(child) - } else if (isArray(result[name])) { - ;(result[name] as unknown[]).push(atRule(child)) - } else { - result[name] = [result[name], atRule(child)] + const body = transform(child, context) + const fontFamily = body.fontFamily as string + if (fontFamily && '"\''.indexOf(fontFamily[0]) > -1) { + body.fontFamily = fontFamily.slice(1, fontFamily.length - 1) } + context['FONT-FACE'].push(body) } else if (child.type === 'rule') { - const body = objectifier(child, context) + const body = transform(child, context) child.selectors.forEach((selector) => { - const className = selector.slice(1) + let className = selector.slice(1) + const pseudoIndex = className.indexOf(':') + if (pseudoIndex > -1) { + const pseudoClass = className.slice(pseudoIndex) + className = className.slice(0, pseudoIndex) + Object.keys(body).forEach(function (name) { + body[name + pseudoClass] = body[name] + delete body[name] + }) + } + transition(className, body, context) if (result[className]) { // clone result[className] = extend({}, result[className], body) } else { result[className] = body } - transition(body, context) }) } else if (child.type === 'decl') { - name = child.prop - const value = child.value - if (!hasOwn(result, name)) { - result[name] = value - } else if (isArray(result[name])) { - ;(result[name] as unknown[]).push(value) - } else { - result[name] = [result[name], value] - } + result[child.prop] = child.value } }) return result } function transition( + className: string, body: Record, - context: ObjectifierContext -) {} - -function atRule(node: AtRule) {} + { TRANSITION }: ObjectifierContext +) { + Object.keys(body).forEach((prop) => { + if (prop.indexOf('transition') === 0 && prop !== 'transition') { + const realProp = prop.replace('transition', '') + TRANSITION[className] = TRANSITION[className] || {} + TRANSITION[className][realProp[0].toLowerCase() + realProp.slice(1)] = + body[prop] + } + }) +}