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')