Skip to content

Commit

Permalink
feat: make animation and keyframes scoped (#37)
Browse files Browse the repository at this point in the history
* refactor: declarations -> children in style rule

* refactor: normalize style ast node children as `children` property

* feat: add scope id for keyframes

* refactor: update declarations to children (missed in previoues commit)

* fix: avoid mutating ast node when adding scope id
  • Loading branch information
ktsn committed Jun 8, 2018
1 parent 8026a15 commit e81cd6b
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 62 deletions.
4 changes: 2 additions & 2 deletions src/parser/style/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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}}`
}
Expand Down
111 changes: 89 additions & 22 deletions src/parser/style/manipulate.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as t from './types'
import assert from 'assert'
import { clone, unquote } from '../../utils'
import { AssetResolver } from '../../asset-resolver'

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-'
Expand All @@ -25,29 +27,50 @@ 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)
}
}

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<t.ChildNode[]>((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(
Expand All @@ -73,31 +96,75 @@ 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<string, string>()

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

export function getNode(
styles: t.Style[],
path: number[]
): t.ChildNode | undefined {
return path.reduce<any | undefined>(
return path.reduce<any>(
(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 }
Expand Down
6 changes: 3 additions & 3 deletions src/parser/style/modify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]
})

Expand Down
18 changes: 10 additions & 8 deletions src/parser/style/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 8 additions & 7 deletions src/parser/style/types.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Range } from '../modifier'

export interface Style extends Range {
export interface HasChildren<T extends ChildNode> {
children: T[]
}

export interface Style extends Range, HasChildren<AtRule | Rule> {
path: [number]
body: (AtRule | Rule)[]
}

export interface Rule extends Range {
export interface Rule extends Range, HasChildren<Declaration> {
type: 'Rule'
before: string
after: string
path: number[]
selectors: Selector[]
declarations: Declaration[]
}

export interface Declaration extends Range {
Expand All @@ -24,14 +26,13 @@ export interface Declaration extends Range {
important: boolean
}

export interface AtRule extends Range {
export interface AtRule extends Range, HasChildren<ChildNode> {
type: 'AtRule'
before: string
after: string
path: number[]
name: string
params: string
children: ChildNode[]
}

export type ChildNode = AtRule | Rule | Declaration
Expand Down Expand Up @@ -82,7 +83,7 @@ export interface Combinator {
export interface RuleForPrint {
path: number[]
selectors: string[]
declarations: DeclarationForPrint[]
children: DeclarationForPrint[]
}

export interface DeclarationForPrint {
Expand Down
4 changes: 2 additions & 2 deletions src/view/components/StyleInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</p>

<ul class="declaration-list" @click.stop>
<li class="declaration" v-for="d in rule.declarations" :key="d.path.join('.')">
<li class="declaration" v-for="d in rule.children" :key="d.path.join('.')">
<StyleDeclaration
:prop="d.prop"
:value="d.value"
Expand Down Expand Up @@ -79,7 +79,7 @@ export default Vue.extend({
if (this.endingInput) return
this.$emit('add-declaration', {
path: rule.path.concat(rule.declarations.length)
path: rule.path.concat(rule.children.length)
})
this.autoFocusOnNextRender = true
},
Expand Down
15 changes: 7 additions & 8 deletions test/helpers/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import {
ChildNode
} from '@/parser/style/types'

export function createStyle(body: (AtRule | Rule)[]): Style {
modifyPath(body)
export function createStyle(children: (AtRule | Rule)[]): Style {
modifyPath(children)
return {
path: [0],
body,
children,
range: [-1, -1]
}
}
Expand All @@ -26,10 +26,9 @@ function modifyPath(nodes: (AtRule | Rule | Declaration)[]): void {
nodes.forEach((node, i) => {
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)
}
})
}
Expand All @@ -55,15 +54,15 @@ export function atRule(

export function rule(
selectors: Selector[],
declarations: Declaration[] = []
children: Declaration[] = []
): Rule {
return {
type: 'Rule',
path: [],
before: '',
after: '',
selectors,
declarations,
children,
range: [-1, -1]
}
}
Expand Down
4 changes: 2 additions & 2 deletions test/parser/style/asset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') + '")'
Expand All @@ -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')
Expand Down

0 comments on commit e81cd6b

Please sign in to comment.