diff --git a/src/parser/style/codegen.ts b/src/parser/style/codegen.ts index 3eebbc7..276c508 100644 --- a/src/parser/style/codegen.ts +++ b/src/parser/style/codegen.ts @@ -2,7 +2,7 @@ import assert from 'assert' import * as t from './types' export function genStyle(ast: t.Style): string { - return ast.body + return ast.children .map(node => { switch (node.type) { case 'AtRule': @@ -51,7 +51,7 @@ function genAtRule(atRule: t.AtRule): string { export function genRule(rule: t.Rule): string { const selectors = rule.selectors.map(genSelector).join(', ') - const declarations = rule.declarations.map(genDeclaration).join(' ') + const declarations = rule.children.map(genDeclaration).join(' ') return `${selectors} {${declarations}}` } diff --git a/src/parser/style/manipulate.ts b/src/parser/style/manipulate.ts index 81ba43f..b753c17 100644 --- a/src/parser/style/manipulate.ts +++ b/src/parser/style/manipulate.ts @@ -1,4 +1,5 @@ import * as t from './types' +import assert from 'assert' import { clone, unquote } from '../../utils' import { AssetResolver } from '../../asset-resolver' @@ -6,6 +7,7 @@ interface StyleVisitor { atRule?(atRule: t.AtRule): t.AtRule | void rule?(rule: t.Rule): t.Rule | void declaration?(decl: t.Declaration): t.Declaration | void + lastSelector?(selector: t.Selector, rule: t.Rule): t.Selector | void } export const scopePrefix = 'data-scope-' @@ -25,7 +27,12 @@ function visitStyle(style: t.Style, visitor: StyleVisitor): t.Style { }) case 'Rule': return clone(apply(node, visitor.rule), { - declarations: node.declarations.map(loop) + selectors: node.selectors.map(selector => { + return visitor.lastSelector && isInterestSelector(node, style) + ? visitor.lastSelector(selector, node) || selector + : selector + }), + children: node.children.map(loop) }) case 'Declaration': return apply(node, visitor.declaration) @@ -33,21 +40,37 @@ function visitStyle(style: t.Style, visitor: StyleVisitor): t.Style { } return clone(style, { - body: style.body.map(loop) + children: style.children.map(loop) }) } export function visitLastSelectors( - node: t.Style, + root: t.Style, fn: (selector: t.Selector, rule: t.Rule) => t.Selector | void ): t.Style { - return visitStyle(node, { - rule: rule => { - return clone(rule, { - selectors: rule.selectors.map(s => fn(s, rule) || s) - }) - } - }) + return visitStyle(root, { lastSelector: fn }) +} + +/** + * Excludes selectors in @keyframes + */ +function isInterestSelector(rule: t.Rule, root: t.Style): boolean { + const atRules = rule.path + .slice(1) + .reduce((nodes, index) => { + const prev = (nodes[nodes.length - 1] || root) as t.HasChildren< + t.ChildNode + > + assert( + 'children' in prev, + '[style manipulate] the rule probably has an invalid path.' + ) + + return nodes.concat(prev.children[index]) + }, []) + .filter((n): n is t.AtRule => n.type === 'AtRule') + + return atRules.every(r => !/-?keyframes$/.test(r.name)) } export function resolveAsset( @@ -73,14 +96,62 @@ export function resolveAsset( } export function addScope(node: t.Style, scope: string): t.Style { - return visitLastSelectors(node, selector => { - return clone(selector, { - attributes: selector.attributes.concat({ - type: 'Attribute', - name: scopePrefix + scope, - insensitive: false + const keyframes = new Map() + + const keyframesReplaced = visitStyle(node, { + atRule(atRule) { + if (/-?keyframes$/.test(atRule.name)) { + const replaced = atRule.params + '-' + scope + keyframes.set(atRule.params, replaced) + + return clone(atRule, { + params: replaced + }) + } + } + }) + + return visitStyle(keyframesReplaced, { + declaration: decl => { + // individual animation-name declaration + if (/^(-\w+-)?animation-name$/.test(decl.prop)) { + return clone(decl, { + value: decl.value + .split(',') + .map(v => keyframes.get(v.trim()) || v.trim()) + .join(',') + }) + } + + // shorthand + if (/^(-\w+-)?animation$/.test(decl.prop)) { + return clone(decl, { + value: decl.value + .split(',') + .map(v => { + const vals = v.trim().split(/\s+/) + const i = vals.findIndex(val => keyframes.has(val)) + if (i !== -1) { + vals.splice(i, 1, keyframes.get(vals[i])!) + return vals.join(' ') + } else { + return v + } + }) + .join(',') + }) + } + }, + + lastSelector: selector => { + return clone(selector, { + attributes: selector.attributes.concat({ + type: 'Attribute', + name: scopePrefix + scope, + insensitive: false + }) }) - }) + } }) } @@ -88,16 +159,12 @@ export function getNode( styles: t.Style[], path: number[] ): t.ChildNode | undefined { - return path.reduce( + return path.reduce( (acc, i) => { if (!acc) return if (acc.children) { return acc.children[i] - } else if (acc.body) { - return acc.body[i] - } else if (acc.declarations) { - return acc.declarations[i] } }, { children: styles } diff --git a/src/parser/style/modify.ts b/src/parser/style/modify.ts index 931a99f..82da9a4 100644 --- a/src/parser/style/modify.ts +++ b/src/parser/style/modify.ts @@ -51,10 +51,10 @@ export function insertDeclaration( } const inserted = clone(rule, { - declarations: [ - ...rule.declarations.slice(last), + children: [ + ...rule.children.slice(last), d, - ...rule.declarations.slice(last + 1) + ...rule.children.slice(last + 1) ] }) diff --git a/src/parser/style/transform.ts b/src/parser/style/transform.ts index 10bd30f..94a1a68 100644 --- a/src/parser/style/transform.ts +++ b/src/parser/style/transform.ts @@ -13,11 +13,11 @@ export function transformStyle( if (!root.nodes) { return { path: [index], - body: [], + children: [], range: [-1, -1] } } - const body = root.nodes + const children = root.nodes .map((node, i) => { switch (node.type) { case 'atrule': @@ -28,13 +28,15 @@ export function transformStyle( return undefined } }) - .filter((node): node is t.AtRule | t.Rule => { - return node !== undefined - }) + .filter( + (node): node is t.AtRule | t.Rule => { + return node !== undefined + } + ) return { path: [index], - body, + children, range: toRange(root.source, code) } } @@ -84,7 +86,7 @@ function transformRule( const selectors = (n as selectorParser.Selector).nodes return transformSelector(selectors) }), - declarations: decls.map((decl, i) => { + children: decls.map((decl, i) => { return transformDeclaration(decl, path.concat(i), code) }), range: toRange(rule.source, code) @@ -261,7 +263,7 @@ export function transformRuleForPrint(rule: t.Rule): t.RuleForPrint { return { path: rule.path, selectors: rule.selectors.map(genSelector), - declarations: rule.declarations.map(decl => ({ + children: rule.children.map(decl => ({ path: decl.path, prop: decl.prop, value: decl.value, diff --git a/src/parser/style/types.ts b/src/parser/style/types.ts index cdf89f4..925d17c 100644 --- a/src/parser/style/types.ts +++ b/src/parser/style/types.ts @@ -1,17 +1,19 @@ import { Range } from '../modifier' -export interface Style extends Range { +export interface HasChildren { + children: T[] +} + +export interface Style extends Range, HasChildren { path: [number] - body: (AtRule | Rule)[] } -export interface Rule extends Range { +export interface Rule extends Range, HasChildren { type: 'Rule' before: string after: string path: number[] selectors: Selector[] - declarations: Declaration[] } export interface Declaration extends Range { @@ -24,14 +26,13 @@ export interface Declaration extends Range { important: boolean } -export interface AtRule extends Range { +export interface AtRule extends Range, HasChildren { type: 'AtRule' before: string after: string path: number[] name: string params: string - children: ChildNode[] } export type ChildNode = AtRule | Rule | Declaration @@ -82,7 +83,7 @@ export interface Combinator { export interface RuleForPrint { path: number[] selectors: string[] - declarations: DeclarationForPrint[] + children: DeclarationForPrint[] } export interface DeclarationForPrint { diff --git a/src/view/components/StyleInformation.vue b/src/view/components/StyleInformation.vue index 17acfc0..0e0d9c2 100644 --- a/src/view/components/StyleInformation.vue +++ b/src/view/components/StyleInformation.vue @@ -11,7 +11,7 @@

    -
  • +
  • { const nextPath = path.concat(i) node.path = nextPath - if (node.type === 'AtRule') { + + if (node.type !== 'Declaration') { loop(node.children, nextPath) - } else if (node.type === 'Rule') { - loop(node.declarations, nextPath) } }) } @@ -55,7 +54,7 @@ export function atRule( export function rule( selectors: Selector[], - declarations: Declaration[] = [] + children: Declaration[] = [] ): Rule { return { type: 'Rule', @@ -63,7 +62,7 @@ export function rule( before: '', after: '', selectors, - declarations, + children, range: [-1, -1] } } diff --git a/test/parser/style/asset.spec.ts b/test/parser/style/asset.spec.ts index 230f927..054bda7 100644 --- a/test/parser/style/asset.spec.ts +++ b/test/parser/style/asset.spec.ts @@ -16,7 +16,7 @@ describe('Style asset resolution', () => { ]) const resolved = resolveAsset(style, basePath, asset) - const decl = (resolved.body[0] as Rule).declarations[0] + const decl = (resolved.children[0] as Rule).children[0] expect(decl.prop).toBe('background') expect(decl.value).toBe( 'url("/assets?path=' + encodeURIComponent('/path/to/assets/bg.png') + '")' @@ -38,7 +38,7 @@ describe('Style asset resolution', () => { ]) const resolved = resolveAsset(style, basePath, asset) - const decls = (resolved.body[0] as Rule).declarations + const decls = (resolved.children[0] as Rule).children expect(decls.length).toBe(2) expect(decls[0].prop).toBe('font-size') expect(decls[0].value).toBe('18px') diff --git a/test/parser/style/scoped.spec.ts b/test/parser/style/scoped.spec.ts index 96d6882..fbb74cb 100644 --- a/test/parser/style/scoped.spec.ts +++ b/test/parser/style/scoped.spec.ts @@ -8,7 +8,7 @@ function getAst(code: string) { return transformStyle(root, code, 0) } -describe('Scoped selector', () => { +describe('Scoped style', () => { it('should add scope attribute on the last selector', () => { const scope = 'abcdef' const code = 'h1 > .foo .bar {}' @@ -17,7 +17,7 @@ describe('Scoped selector', () => { const result = addScope(ast, scope) const expected: any = ast - expected.body[0].selectors[0].attributes.push( + expected.children[0].selectors[0].attributes.push( attribute('data-scope-' + scope) ) @@ -32,7 +32,57 @@ describe('Scoped selector', () => { const result = addScope(ast, scope) const expected: any = ast - expected.body[0].children[0].selectors[0].attributes.push( + expected.children[0].children[0].selectors[0].attributes.push( + attribute('data-scope-' + scope) + ) + + assertStyleNode(result, expected) + }) + + it('adds scope id to keyframes and animation name', () => { + const scope = 'abcdef' + const code = ` + .foo { + animate: test 2s; + } + + @keyframes test { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + ` + + const ast = getAst(code) + const result = addScope(ast, scope) + + const expected: any = ast + expected.children[0].selectors[0].attributes.push( + attribute('data-scope-' + scope) + ) + expected.children[0].children[0].value = 'test-' + scope + ' 2s' + expected.children[1].params = 'test-' + scope + + assertStyleNode(result, expected) + }) + + it('does not add scope id to no-matched animation name', () => { + const scope = 'abcdef' + const code = ` + .foo { + animate: test 2s; + } + ` + + const ast = getAst(code) + const result = addScope(ast, scope) + + const expected: any = ast + expected.children[0].selectors[0].attributes.push( attribute('data-scope-' + scope) ) diff --git a/test/parser/style/transform.spec.ts b/test/parser/style/transform.spec.ts index 66b2a85..2a52e37 100644 --- a/test/parser/style/transform.spec.ts +++ b/test/parser/style/transform.spec.ts @@ -168,10 +168,10 @@ describe('Style AST transformer', () => { it('should transform node position', () => { const ast = getAst('.foo {\n color: red;\n}\n.bar {}') - const rule = ast.body[0] as Rule + const rule = ast.children[0] as Rule expect(ast.range).toEqual([0, 30]) expect(rule.range).toEqual([0, 22]) - expect(rule.declarations[0].range).toEqual([9, 20]) + expect(rule.children[0].range).toEqual([9, 20]) }) }) diff --git a/test/view/store/project.spec.ts b/test/view/store/project.spec.ts index be85382..fca26d1 100644 --- a/test/view/store/project.spec.ts +++ b/test/view/store/project.spec.ts @@ -5,6 +5,7 @@ import { createStyle, rule, selector } from '../../helpers/style' import { addScope as addScopeToTemplate } from '@/parser/template/manipulate' import { ClientConnection } from '@/view/communication' import { StyleMatcher } from '@/view/store/style-matcher' +import { RuleForPrint } from '@/parser/style/types' describe('Store project getters', () => { let store: Store, state: ProjectState @@ -268,10 +269,10 @@ describe('Store project actions', () => { }) describe('matchSelectedNodeWithStyles', () => { - const emptyRule = { + const emptyRule: RuleForPrint = { path: [0, 0], selectors: ['div'], - declarations: [] + children: [] } it('extract rules of node', () => { @@ -279,7 +280,7 @@ describe('Store project actions', () => { state.currentUri = 'file:///Foo.vue' state.selectedPath = [0] - const matched = [docs['file:///Foo.vue'].styles[0].body[0]] + const matched = [docs['file:///Foo.vue'].styles[0].children[0]] ;(mockStyleMatcher.match as any).mockReturnValue(matched) store.dispatch('project/matchSelectedNodeWithStyles')