├── ChangeLog.md ├── .babelrc ├── .gitignore ├── ScreenShot.png ├── .travis.yml ├── test ├── fixtures │ ├── simple.css │ ├── exports.css │ ├── font.css │ ├── clazz-pseudo.css │ ├── real-import.css │ ├── import.css │ ├── welcome.css │ ├── simple-component.css │ ├── media.css │ ├── animation.css │ ├── clazz.css │ ├── clazz-media.css │ ├── component.css │ └── clazz.css.json ├── animation-test.js ├── color-test.js ├── transform-test.js ├── font-test.js ├── selector-test.js ├── Sample.js ├── listen-test.js ├── parseMedia-test.js ├── match-test.js ├── decls-test.js ├── features-test.js ├── border-test.js ├── support │ └── index.js └── postcss-react-native-test.js ├── .npmignore ├── src ├── util │ ├── unescape.js │ ├── isObjectLike.js │ └── camel.js ├── words.js ├── declarations │ ├── transform.js │ ├── index.js │ ├── animation.js │ ├── font.js │ ├── border.js │ └── transition.js ├── flatten.js ├── fill.js ├── animation │ ├── locals-load.js │ ├── loader.js │ ├── css-locals-transition.js │ ├── css-locals-dimension.js │ ├── utils.js │ └── animation.js ├── listen.js ├── selector.js ├── mediaSelector.js ├── unit.js ├── match.js ├── features.js ├── DimensionComponent.js ├── componentHelpers.js ├── index.js ├── decls.js ├── source.js └── AnimatedCSS.js ├── LICENSE ├── package.json ├── bin └── prn.js └── README.md /ChangeLog.md: -------------------------------------------------------------------------------- 1 | 1.0.0 - 2016-06-10 2 | 3 | * First Release 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-native" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | lib 4 | dist 5 | 6 | out 7 | -------------------------------------------------------------------------------- /ScreenShot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jspears/postcss-react-native/HEAD/ScreenShot.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - iojs 5 | - "0.12" 6 | - "0.10" 7 | -------------------------------------------------------------------------------- /test/fixtures/simple.css: -------------------------------------------------------------------------------- 1 | .opacity { 2 | opacity: .5; 3 | } 4 | .opacity { 5 | opacity: .216708; 6 | } -------------------------------------------------------------------------------- /test/fixtures/exports.css: -------------------------------------------------------------------------------- 1 | :export { 2 | color: #FF0000; 3 | } 4 | 5 | .other { 6 | opacity: 0.5; 7 | } -------------------------------------------------------------------------------- /test/fixtures/font.css: -------------------------------------------------------------------------------- 1 | .font1 { 2 | font: 2em "Open Sans", sans-serif; 3 | } 4 | .font2 { 5 | font-size: 2in; 6 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules/ 3 | out 4 | test/ 5 | .travis.yml 6 | gulpfile.js 7 | .idea 8 | ChangeLog.md 9 | .babelrc 10 | -------------------------------------------------------------------------------- /test/fixtures/clazz-pseudo.css: -------------------------------------------------------------------------------- 1 | @namespace Text "react-native.Text"; 2 | 3 | Text|StyledText { 4 | color:red; 5 | } 6 | Text|StyledText:checked { 7 | color:blue; 8 | } -------------------------------------------------------------------------------- /test/fixtures/real-import.css: -------------------------------------------------------------------------------- 1 | @import './welcome.css'; 2 | @import './component.css'; 3 | 4 | .real { 5 | border: 1px solid red; 6 | } 7 | 8 | .stuff { 9 | color: chartreuse; 10 | } -------------------------------------------------------------------------------- /src/util/unescape.js: -------------------------------------------------------------------------------- 1 | export default function (str) { 2 | if (!str) return str; 3 | str = str.trim(); 4 | const e = str.length - 1; 5 | const f = str[0]; 6 | if (f === '"' || f === '\'' && str[e] === f) 7 | return str.substring(1, e); 8 | return str; 9 | } -------------------------------------------------------------------------------- /test/fixtures/import.css: -------------------------------------------------------------------------------- 1 | @import url("fineprint.css") print; 2 | @import url("bluish.css") projection, tv; 3 | @import 'custom.css'; 4 | @import url("chrome://communicator/skin/"); 5 | @import "common.css" screen, projection; 6 | @import url('landscape.css') screen and (orientation:landscape); 7 | -------------------------------------------------------------------------------- /src/words.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import valueparser from 'postcss-value-parser'; 4 | 5 | export default function (str) { 6 | const rest = []; 7 | if (!str) return rest; 8 | 9 | valueparser(str).walk((node, pos, nodes)=> { 10 | if (node.type === 'word') { 11 | rest.push(node.value); 12 | } 13 | }); 14 | 15 | return rest; 16 | }; 17 | -------------------------------------------------------------------------------- /src/util/isObjectLike.js: -------------------------------------------------------------------------------- 1 | export default function isObjectLike(value) { 2 | if (!value) return false; 3 | const tofv = typeof value; 4 | switch (tofv) { 5 | case 'string': 6 | case 'boolean': 7 | case 'number': 8 | return false; 9 | } 10 | if (value instanceof Date || value instanceof RegExp)return false; 11 | return true; 12 | }; 13 | -------------------------------------------------------------------------------- /test/fixtures/welcome.css: -------------------------------------------------------------------------------- 1 | .container { 2 | flex: 1; 3 | justify-content: center; 4 | align-items: center; 5 | background-color: #c0c0c0; 6 | } 7 | 8 | .welcome { 9 | font: 20px "Thonburi"; 10 | text-align: center; 11 | margin: 0px; 12 | 13 | } 14 | 15 | .instructions { 16 | text-align: center; 17 | color: #333333; 18 | margin-bottom: 5px; 19 | border: 1px solid green; 20 | } 21 | -------------------------------------------------------------------------------- /src/declarations/transform.js: -------------------------------------------------------------------------------- 1 | import {list} from 'postcss'; 2 | 3 | const funcRe = /^([^(]*)\(([^)]*)\)$/; 4 | const parseFunc = (val)=> { 5 | const [match, func, unit] = funcRe.exec(val); 6 | return {[func]: unit}; 7 | }; 8 | export default function transform(postfix, value, str, container, config){ 9 | if (postfix == null) { 10 | const parts = list.space(value).map(parseFunc); 11 | return parts; 12 | } 13 | return {[postfix]: value}; 14 | } -------------------------------------------------------------------------------- /test/fixtures/simple-component.css: -------------------------------------------------------------------------------- 1 | @namespace Text "react-native.Text"; 2 | 3 | Text|StyledText { 4 | color: red; 5 | background-color: teal; 6 | margin: 10px; 7 | padding: 5px; 8 | } 9 | 10 | Text|StyledText.green { 11 | color: green; 12 | } 13 | 14 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) { 15 | Text|StyledText { 16 | margin: 20px; 17 | } 18 | Text|StyledText.blue { 19 | color:blue; 20 | } 21 | } -------------------------------------------------------------------------------- /test/animation-test.js: -------------------------------------------------------------------------------- 1 | import {testString, read, testTransform, test, json} from './support'; 2 | import {expect} from 'chai'; 3 | import source from '../src/source'; 4 | describe('animation', function () { 5 | it('should parse animation', function () { 6 | return testString(read('test/fixtures/animation.css'), { 7 | toJSON(f, css){ 8 | 9 | expect(f).to.exist; 10 | return f; 11 | } 12 | }); 13 | 14 | }); 15 | 16 | }); -------------------------------------------------------------------------------- /src/util/camel.js: -------------------------------------------------------------------------------- 1 | const uc = (v = '')=> { 2 | return v ? v[0].toUpperCase() + v.substring(1) : v; 3 | }; 4 | 5 | const ucc = (v = '')=> { 6 | return v.split('-').map(uc).join(''); 7 | }; 8 | 9 | export default function (arg, ...args) { 10 | const [a, ...rest] = arg.split('-'); 11 | const r = [a]; 12 | if (rest.length) { 13 | r.push(...rest.filter(Boolean).map(ucc)); 14 | } 15 | if (args.length) { 16 | r.push(...args.map(ucc)); 17 | } 18 | return r.join(''); 19 | }; 20 | -------------------------------------------------------------------------------- /test/color-test.js: -------------------------------------------------------------------------------- 1 | import color from 'parse-color'; 2 | import {expect} from 'chai'; 3 | import {testString} from './support'; 4 | 5 | describe('color', function(){ 6 | 7 | it('should parse purple', ()=>{ 8 | 9 | const r = color('purple'); 10 | }); 11 | it('should parse color decl', ()=>{ 12 | return testString('.c { color:#fff }', { 13 | toJSON(o){ 14 | expect(o.rules[0].css['c'][0].values).to.eql('rgba(255, 255, 255, 1)'); 15 | } 16 | }) 17 | }); 18 | }); -------------------------------------------------------------------------------- /test/fixtures/media.css: -------------------------------------------------------------------------------- 1 | .stuff { 2 | margin: 10px; 3 | border-width: 5px; 4 | border-top: 2px solid green; 5 | color: red; 6 | } 7 | 8 | 9 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) { 10 | .stuff { 11 | margin: 20px 10px 5px 2px; 12 | border-top-width: 3px; 13 | border-bottom-width: 5pt; 14 | color: blue; 15 | -android-color: purble; 16 | -ios-color: yellow; 17 | 18 | } 19 | 20 | .other { 21 | opacity: .5; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/transform-test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {testString} from './support'; 3 | 4 | describe('transform', function () { 5 | 6 | it('should parse', ()=> { 7 | return testString( 8 | `.t1 { 9 | transform: translate(100px) rotate(20deg); 10 | transform-origin: 0 -250px; 11 | } 12 | `, { 13 | toJSON(o){ 14 | expect(o).to.exist; 15 | return o; 16 | }, 17 | toStyleSheet(...args){ 18 | } 19 | }) 20 | }); 21 | }); -------------------------------------------------------------------------------- /src/flatten.js: -------------------------------------------------------------------------------- 1 | import {StyleSheet} from 'react-native'; 2 | 3 | export default function _flatten(flatten, css) { 4 | if (!flatten || flatten.length == 0) return css; 5 | 6 | const merged = flatten.concat(css).reduce((ret, style)=> { 7 | for (const key of Object.keys(style)) { 8 | (ret[key] || (ret[key] = [])).push(style[key]); 9 | } 10 | return ret; 11 | }, {}); 12 | 13 | return Object.keys(merged).reduce((ret, key)=> { 14 | ret[key] = StyleSheet.flatten(merged[key]); 15 | return ret; 16 | }, {}); 17 | 18 | } -------------------------------------------------------------------------------- /src/declarations/index.js: -------------------------------------------------------------------------------- 1 | import {border as _border} from './border'; 2 | import _transition from './transition'; 3 | import _font from './font'; 4 | import _animation from './animation'; 5 | import _transform from './transform'; 6 | export const font = _font; 7 | export const border = _border; 8 | export const transition = _transition; 9 | export const animation = _animation; 10 | export const transform = _transform; 11 | export default { 12 | border: _border, 13 | transition: _transition, 14 | font: _font, 15 | animation: _animation, 16 | transform: _transform 17 | }; -------------------------------------------------------------------------------- /test/fixtures/animation.css: -------------------------------------------------------------------------------- 1 | @namespace Text "react-native.View"; 2 | 3 | .stuff { 4 | color:red; 5 | } 6 | View|SlideIn { 7 | animation-duration: 3s; 8 | animation-name: slidein; 9 | animation-iteration-count: 1; 10 | animation-direction: alternate; 11 | } 12 | 13 | .stuff { 14 | color:blue; 15 | animation: 4s linear 0s infinite alternate slidein; 16 | } 17 | @keyframes slidein { 18 | from { 19 | margin-left: 300px; 20 | width: 300px; 21 | } 22 | 50%,60% { 23 | margin-left: 200px; 24 | width: 200px; 25 | 26 | } 27 | to { 28 | margin-left: 0; 29 | width: 100px; 30 | } 31 | } -------------------------------------------------------------------------------- /src/declarations/animation.js: -------------------------------------------------------------------------------- 1 | import Animation from '../animation/animation'; 2 | import {toMillis} from '../animation/utils'; 3 | 4 | const TIMING = { 5 | 'duration': toMillis, 6 | 'delay': toMillis 7 | }; 8 | //(postfix, str, vendor, tag, config) 9 | export default function (postfix, value, vendor, tag, config) { 10 | config.prefix == 'animations'; 11 | if (postfix) { 12 | return { 13 | [postfix]: TIMING[postfix] ? TIMING[postfix](value) : value 14 | } 15 | } else if (value) { 16 | const anim = new Animation(); 17 | anim.animation(value); 18 | const ret = anim.toJSON(); 19 | return ret[0]; 20 | } 21 | } -------------------------------------------------------------------------------- /test/font-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {font} from '../src/declarations'; 4 | import {expect} from 'chai'; 5 | 6 | describe('font', function () { 7 | 8 | const testFont = (decl, eql, fn = it)=>it(`should ${decl}`, ()=>expect(font(...decl), `:"${decl}"`).to.eql(eql)); 9 | 10 | testFont(['', '2em "Open Sans", sans-serif'], { 11 | "family": [ 12 | "Open Sans", 13 | "sans-serif" 14 | ], 15 | "size": "2em" 16 | }); 17 | 18 | testFont(['size', '2em'], { 19 | "size": "2em" 20 | }); 21 | testFont(['family', '"Open Sans", sans-serif'], { 22 | "family": "\"Open Sans\", sans-serif" 23 | }); 24 | }); -------------------------------------------------------------------------------- /src/fill.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | export const TRBL = ['top', 'right', 'bottom', 'left']; 3 | //trbl 4 | //0123 5 | export default function prefill(values, pos) { 6 | values = values ? Array.isArray(values) ? values : [values] : []; 7 | const ln = values.length; 8 | if (pos < ln || ln === 0) { 9 | return values[pos]; 10 | } 11 | if (pos == 1) { 12 | return values[0]; 13 | } 14 | if (pos == 2) { 15 | return prefill(values, 0); 16 | } 17 | if (pos == 3) { 18 | return prefill(values, 1); 19 | } 20 | } 21 | 22 | export const table = (values, side, table)=> prefill(values, table.indexOf(side)); 23 | 24 | export const trbl = (values, side)=>table(values, side, TRBL); -------------------------------------------------------------------------------- /test/fixtures/clazz.css: -------------------------------------------------------------------------------- 1 | @namespace View "react-native.View"; 2 | 3 | View|MyView .combine { 4 | height: 20px; 5 | } 6 | 7 | View|MyView { 8 | flex: 1; 9 | border: 1px solid red; 10 | transition: height 1s ease-in-out; 11 | } 12 | .whatever { 13 | margin: 10px; 14 | } 15 | 16 | View|MyView.whatever { 17 | margin: 15px; 18 | flex-direction: column; 19 | } 20 | 21 | View|MyView.appear, View|MyView.enter { 22 | height: 0px; 23 | transition: height .5s ease-in; 24 | } 25 | 26 | View|MyView.appear-active, View|MyView.enter-active { 27 | height: 100px; 28 | } 29 | 30 | View|MyView.leave { 31 | height: 100px; 32 | transition: height .5s ease-in; 33 | } 34 | 35 | View|MyView.leave-active { 36 | height: 0px; 37 | } 38 | -------------------------------------------------------------------------------- /test/selector-test.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import selector from '../src/selector'; 3 | const compare = (str, result)=> { 4 | expect(selector(str)).to.eql(result); 5 | }; 6 | const tester = (...result)=> { 7 | return function () { 8 | compare( this.test.title.replace(/.*\"(.*)\"/, '$1'), result); 9 | } 10 | }; 11 | 12 | describe('selector', function () { 13 | 14 | 15 | it('should parse ".select, hello.more"', tester({'class': 'select'}, {'tag': 'hello', 'class': 'more'})); 16 | it('should parse ".select, .more"', tester({'class': 'select'}, {'class': 'more'})); 17 | it('should parse ".select"', tester({'class': 'select'})); 18 | it('should parse ".select::before"', tester({'class': 'select', 'pseudo':'::before'})); 19 | }); -------------------------------------------------------------------------------- /test/fixtures/clazz-media.css: -------------------------------------------------------------------------------- 1 | @namespace View "react-native.View"; 2 | 3 | @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) { 4 | 5 | View|MyView .combine { 6 | height: 20px; 7 | } 8 | 9 | View|MyView { 10 | flex: 1; 11 | border: 1px solid red; 12 | transition: height 1s ease-in-out; 13 | } 14 | 15 | .whatever { 16 | margin: 10px; 17 | } 18 | 19 | View|MyView.whatever { 20 | margin: 15px; 21 | flex-direction: column; 22 | } 23 | 24 | View|MyView.appear, View|MyView.enter { 25 | height: 0px; 26 | transition: height .5s ease-in; 27 | } 28 | 29 | View|MyView.appear-active, View|MyView.enter-active { 30 | height: 100px; 31 | } 32 | 33 | View|MyView.leave { 34 | height: 100px; 35 | transition: height .5s ease-in; 36 | } 37 | 38 | View|MyView.leave-active { 39 | height: 0px; 40 | } 41 | } -------------------------------------------------------------------------------- /src/animation/locals-load.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function loadPlugin(plugin, locals, opts) { 4 | if (typeof plugin === 'string') { 5 | if (/^css-locals-/.test(plugin)) { 6 | return require('./' + plugin)(locals, opts); 7 | } 8 | return loadPlugin(require(plugin), locals, opts); 9 | } 10 | return plugin(locals, opts); 11 | } 12 | 13 | function localsLoad(plugin, locals, opts) { 14 | if (!plugin) { 15 | return []; 16 | } 17 | if (typeof plugin === 'string') { 18 | 19 | return localsLoad(plugin.split(/,\s*/), locals, opts); 20 | } 21 | if (!Array.isArray(plugin)) { 22 | if (typeof plugin === 'object') { 23 | return Object.keys(plugin).map(function (plug) { 24 | return loadPlugin(plug, locals, plugin[plug]); 25 | }); 26 | } else { 27 | plugin = [plugin]; 28 | } 29 | } 30 | 31 | return plugin.map(function (plug) { 32 | return loadPlugin(plug, locals, opts); 33 | }); 34 | 35 | } 36 | 37 | module.exports = localsLoad; 38 | -------------------------------------------------------------------------------- /src/listen.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | 3 | const listeners = new Set(); 4 | 5 | const listen = (...args) => { 6 | for (const listen of listeners) { 7 | listen(...args); 8 | } 9 | }; 10 | //add subscriptions 11 | const subscribe = listen.subscribe = (...cbs) => { 12 | listeners.add(...cbs); 13 | //unsubscribes 14 | return () => { 15 | for (const cb of cbs) { 16 | listeners.delete(cb); 17 | } 18 | } 19 | }; 20 | //listen to a property. update it when the listen is fired. 21 | listen.property = (obj, key, cb) => subscribe((...args) => obj[key] = cb(...args)); 22 | 23 | //clear all listeners. 24 | listen.clear = (_listeners = listeners) => listeners.clear(); 25 | 26 | //After the value is triggered once, it stops listening. 27 | listen.once = (cb)=> { 28 | const _unlisten = listeners.subscribe((...args)=> { 29 | const ret = cb(...args); 30 | _listen(); 31 | return ret; 32 | }); 33 | return _unlisten; 34 | }; 35 | return listen; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 Justin Spears 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/declarations/font.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import pcf from 'parse-css-font'; 4 | 5 | /** 6 | * as each of the properties of the shorthand: 7 | font-style: normal 8 | font-variant: normal 9 | font-weight: normal 10 | font-stretch: normal 11 | font-size: medium 12 | line-height: normal 13 | font-family: depends on user agent 14 | * @param postfix 15 | * @param values 16 | */ 17 | export default function font(postfix, values) { 18 | 19 | if (postfix) { 20 | switch (postfix) { 21 | case 'family': 22 | case 'size': 23 | case 'style': 24 | case 'weight': 25 | case 'line-height': 26 | case 'color': 27 | return { 28 | [postfix]: values 29 | }; 30 | } 31 | console.warn('should not get here', postfix, values); 32 | } 33 | const ret = pcf(values); 34 | return Object.keys(ret).reduce((r, k)=> { 35 | const v = ret[k]; 36 | if (v == 'normal') return r; 37 | 38 | if (k == 'lineHeight') { 39 | k = 'line-height'; 40 | } 41 | r[k] = v; 42 | return r; 43 | }, {}); 44 | } -------------------------------------------------------------------------------- /src/selector.js: -------------------------------------------------------------------------------- 1 | import selectorParser from 'postcss-selector-parser'; 2 | 3 | const parser = selectorParser(); 4 | 5 | export default function selector(selectorString) { 6 | const selector = parser.process(selectorString).res; 7 | let tag, sel, all = []; 8 | selector.walk(function ({type, value, namespace}) { 9 | switch (type) { 10 | case 'tag': 11 | case 'pseudo': 12 | case 'class': 13 | { 14 | if (!sel) { 15 | sel = {}; 16 | 17 | all.push(sel); 18 | } 19 | if (namespace) { 20 | sel.namespace = namespace; 21 | } 22 | sel[type] = value; 23 | break; 24 | } 25 | case 'selector': 26 | if (sel) { 27 | sel = {}; 28 | if (namespace) { 29 | sel.namespace = namespace; 30 | } 31 | all.push(sel); 32 | } 33 | break; 34 | default: 35 | console.warn(`selectors of type ${type} are not supported yet in selector ${selectorString}`); 36 | } 37 | }); 38 | return all; 39 | } 40 | -------------------------------------------------------------------------------- /test/Sample.js: -------------------------------------------------------------------------------- 1 | const css = {}, 2 | px = 1, 3 | vendor = config.vendor, 4 | inch = 96, 5 | vh = config.height / 100, 6 | vw = config.width / 100, 7 | 8 | units = { 9 | px: px, 10 | vh: vh, 11 | vw: vw, 12 | 'in': inch, 13 | pt: (inch / 72), 14 | em: 1, 15 | pc: 12 * (inch / 72), 16 | vmin: Math.min(vw, vh), 17 | vmax: Math.max(vw, vh) 18 | }; 19 | 20 | 21 | (function (exports, root) { 22 | 23 | let _style = function (FEATURES, config) { 24 | const css = {}; 25 | return css; 26 | }; 27 | let _sstyle = StyleSheet.create(_style); 28 | exports["View"] = React.createClass({ 29 | displayName: "View", 30 | componentWillMount(){ 31 | this._listen = listen.add(()=> { 32 | _sstyle = StyleSheet.create(_style); 33 | this.forceUpdate(); 34 | }); 35 | }, 36 | componentWillUnmount(){ 37 | this._listen && this._listen(); 38 | }, 39 | render(){ 40 | var props = Object.assign({}, this.props); 41 | delete props.children; 42 | props.style = _sstyle; 43 | return React.createElement(pkgs.Native, props, children); 44 | } 45 | 46 | }); 47 | })(module.exports, root); 48 | 49 | -------------------------------------------------------------------------------- /test/fixtures/component.css: -------------------------------------------------------------------------------- 1 | @import './welcome.css'; 2 | @namespace Native "react-native.View"; 3 | @namespace Text "react-native.Text"; 4 | 5 | Text|StyledText { 6 | color: red; 7 | background-color: teal; 8 | margin:10px; 9 | padding:5px; 10 | } 11 | 12 | Text|StyledText:checked { 13 | color: blue; 14 | background-color: white; 15 | } 16 | 17 | Native|View { 18 | border: 2px solid red; 19 | height: 100px; 20 | width: 200px; 21 | } 22 | 23 | Native|View.green { 24 | border-top-color: green; 25 | } 26 | 27 | .green { 28 | margin: 10px; 29 | } 30 | 31 | .text { 32 | color: purple; 33 | } 34 | 35 | View|SlideIn { 36 | animation-duration: 3s; 37 | animation-name: slidein; 38 | animation-iteration-count: 1; 39 | } 40 | 41 | @keyframes slidein { 42 | from { 43 | margin-left: 300px; 44 | width: 300px; 45 | } 46 | 50%,60% { 47 | margin-left: 200px; 48 | width: 200px; 49 | 50 | } 51 | to { 52 | margin-left: 0; 53 | width: 100px; 54 | } 55 | } 56 | @media only screen and (orientation: landscape) { 57 | .text { 58 | color: red; 59 | 60 | } 61 | 62 | Native|View { 63 | border-color: purple; 64 | width: 500px; 65 | } 66 | Text|StyledText { 67 | color: yellow; 68 | background-color: purple; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-react-native", 3 | "version": "1.0.1", 4 | "description": "PostCSS plugin to create react stylesheets", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "react-native", 9 | "postcss-plugin" 10 | ], 11 | "author": "Justin Spears", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jspears/postcss-react-native.git" 16 | }, 17 | "scripts": { 18 | "dist": "babel -s true src -d dist", 19 | "prepublish": "npm run dist", 20 | "test": "mocha --compilers js:babel-register test/*test.js" 21 | }, 22 | "bin": { 23 | "prn": "./bin/prn.js" 24 | }, 25 | "publishConfig": { 26 | "registry": "https://registry.npmjs.org" 27 | }, 28 | "dependencies": { 29 | "css-border-property": "^1.1.0", 30 | "css-mediaquery": "^0.1.2", 31 | "parse-color": "^1.0.0", 32 | "parse-css-font": "^2.0.2", 33 | "postcss": "^5.0.4", 34 | "postcss-selector-parser": "^2.0.0", 35 | "postcss-value-parser": "^3.3.0", 36 | "react-native-animatable": "^0.6.1" 37 | }, 38 | "devDependencies": { 39 | "babel": "^6.5.2", 40 | "babel-cli": "^6.9.0", 41 | "babel-preset-es2015": "^6.9.0", 42 | "babel-preset-react-native": "^1.9.1", 43 | "babel-register": "^6.9.0", 44 | "chai": "^3.2.0", 45 | "fb-watchman": "^1.9.0", 46 | "lodash": "^3.10.1", 47 | "mocha": "^2.2.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/mediaSelector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import FEATURES from './features'; 3 | import mq from 'css-mediaquery'; 4 | const mergeCss$reduce = (ret, {css={}})=> { 5 | return Object.keys(css).reduce((obj, key)=> { 6 | if (obj[key]) { 7 | obj[key] = Object.assign({}, obj[key], css[key]); 8 | } else { 9 | obj[key] = css[key]; 10 | } 11 | 12 | return obj; 13 | }, ret); 14 | }; 15 | 16 | export const mergeCss = (rules)=>rules.reduce(mergeCss$reduce, {}); 17 | 18 | 19 | export function match({css, rules = []}, config = {}) { 20 | 21 | return rules.filter(rule=>matchExpression(rule, config)); 22 | 23 | } 24 | 25 | export function matchExpression({expressions=[], invert=false}, config) { 26 | 27 | for (const expr of expressions) { 28 | if (FEATURES[expr.modifier ? `${expr.modifier}-${expr.feature}` : expr.feature](expr.value, config) !== true && !invert) { 29 | return false; 30 | } 31 | } 32 | return true; 33 | 34 | } 35 | 36 | export const parseMedia = mq.parse.bind(mq); 37 | 38 | /* 39 | [`(min-width: 700px), handheld and (orientation: landscape)`, [{ 40 | "inverse": false, 41 | "type": "all", 42 | "expressions": [{"modifier": "min", "feature": "width", "value": "700px"}] 43 | }, { 44 | "inverse": false, 45 | "type": "handheld", 46 | "expressions": [{"feature": "orientation", "modifier": void(0), "value": "landscape"}] 47 | }]], 48 | */ 49 | export default function (rule, style, config) { 50 | return mergeCss(match(rules, config)); 51 | } 52 | -------------------------------------------------------------------------------- /src/animation/loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var postcss = require('postcss'); 4 | var localsLoad = require('./locals-load'); 5 | var loaderUtils = require("loader-utils"); 6 | 7 | module.exports = function (source, map) { 8 | if (this.cacheable) { 9 | this.cacheable(); 10 | } 11 | 12 | var query = typeof this.query === 'string' ? loaderUtils.parseQuery(this.query) : this.query; 13 | 14 | var cssLocalPlugins = this.options && this.options.cssLocals 15 | ? this.options.cssLocals : [require('./css-locals-transition')]; 16 | 17 | 18 | var exports = { 19 | push(id){ 20 | this.css = id[1]; 21 | }, 22 | locals: {} 23 | }; 24 | 25 | var callback = this.async(); 26 | 27 | var fakeRequire = function (_mod) { 28 | exports.mod = _mod; 29 | return function () { 30 | return exports; 31 | } 32 | }; 33 | 34 | var _module = {}; 35 | 36 | //yeah, I know should use child compiler, but damn that is a lot of work. 37 | (new Function(['exports', 'module', 'require'], source))(null, _module, fakeRequire); 38 | 39 | postcss(localsLoad(cssLocalPlugins, exports.locals, query)).process(exports.css).then(function (result) { 40 | 41 | var str = 'exports = module.exports = require(' + JSON.stringify(exports.mod) + ')();\n\n'; 42 | str += '// imports\n\n'; 43 | str += 'exports.push([module.id, ' + JSON.stringify(result + '') + ', ""]);\n\n'; 44 | str += '// exports\nexports.locals = ' + (exports.locals ? JSON.stringify(exports.locals) : null) + ';'; 45 | callback(null, str); 46 | }).catch(callback); 47 | }; -------------------------------------------------------------------------------- /src/unit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import valueParser from 'postcss-value-parser'; 4 | 5 | 6 | const re = /^unset|inherit|auto|revert$|^(\d*(?:\.\d+?)?)(em|ex|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pc|%)?$/i; 7 | //`${re.source}{1,4}$|${re.source}(?:\s+?(\w+?))(?:\s+?(\w+?))$/i; 8 | 9 | /* 10 | /^(((\d*(?:\.\d+?)?)(em|ex|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pc)\s*){1,4})$|^((\d*(?:\.\d+?)?)(em|ex|ch|rem|vh|vw|vmin|vmax|px|mm|cm|in|pc)(?:\s+?(solid|dashed|hidden|double|dotted|groove|ridge|inset|outset))?)(\s+?\w+?)?$/. 11 | */ 12 | export function isUnit(val) { 13 | if (re.test(val)) { 14 | return val; 15 | } 16 | return false; 17 | } 18 | export function isBorderUnit(val) { 19 | if (isUnit(val)) return val; 20 | return /^medium|thin|thick$/.test(val) ? val : false; 21 | } 22 | export const allUnit = (vals, _isUnit = isUnit)=> { 23 | const ret = []; 24 | for (const val of vals) { 25 | const u = _isUnit(val); 26 | if (u === false) { 27 | return false; 28 | } 29 | ret.push(u); 30 | } 31 | return ret.length === 0 ? false : ret; 32 | }; 33 | 34 | export function isNumeric(n) { 35 | return !isNaN(parseFloat(n)) && isFinite(n); 36 | } 37 | 38 | export default function (value) { 39 | 40 | if (value === 'auto') { 41 | return null; 42 | } 43 | 44 | let {number, unit} =valueParser.unit(value); 45 | 46 | if (unit) { 47 | number = parseFloat(number); 48 | if (unit == 'px') { 49 | return {value: number}; 50 | } 51 | return { 52 | unit, 53 | value: number 54 | } 55 | } 56 | return isNumeric(value) ? {value: parseFloat(value)} : {value}; 57 | 58 | } -------------------------------------------------------------------------------- /src/animation/css-locals-transition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var utils = require('./utils'); 3 | var DEF_SELECTORS = ['enter', 'leave', 'appear']; 4 | var transition = require('./transition'); 5 | var animation = require('./animation'); 6 | 7 | 8 | module.exports = function cssLocalsTransition(locals, opts) { 9 | 10 | opts = opts || utils.EMPTY_OBJ; 11 | 12 | var localOpts = opts['transition'] || utils.EMPTY_OBJ; 13 | var updateLocal = localOpts.updateLocal || opts.localUpdate || utils.localUpdate; 14 | var selectors = localOpts.selectors || opts.selectors || utils.selectors; 15 | 16 | var selectorsMap = Object.keys(locals).filter(function (key) { 17 | //only selectors we care about. 18 | return selectors.indexOf(key) > -1 19 | }).reduce(function (ret, key) { 20 | ret[locals[key]] = key; 21 | return ret; 22 | }, {}); 23 | 24 | // Work with options here 25 | var re = utils.classToRegexp(Object.keys(selectorsMap)); 26 | 27 | return function cssLocalsTransition$postCssPlugin(css, result) { 28 | css.walkRules(re, function (s) { 29 | var prop = selectorsMap[s.selector.replace(/^\./, '')]; 30 | var trans = transition(), anim = animation(); 31 | 32 | s.walkDecls(/^transition.*/, function (node) { 33 | trans[node.prop](node.value); 34 | }); 35 | 36 | s.walkDecls(/^animation.*/, function (node) { 37 | anim[node.prop](node.value) 38 | }); 39 | 40 | var max = Math.max(anim.timeout(), trans.timeout()); 41 | if (max > 0) { 42 | updateLocal(locals, prop, 'timeout', max); 43 | } 44 | }); 45 | 46 | }; 47 | }; -------------------------------------------------------------------------------- /test/listen-test.js: -------------------------------------------------------------------------------- 1 | import listen from '../src/listen'; 2 | import {expect} from 'chai'; 3 | 4 | describe("listen", function () { 5 | 6 | it('should update property', function () { 7 | const fn1 = i => i + 1; 8 | const f = listen(); 9 | const obj = {}; 10 | const s1 = f.property(obj, 'stuff', fn1); 11 | f(2); 12 | expect(obj).to.eql({stuff:3}); 13 | f(3); 14 | expect(obj).to.eql({stuff:4}); 15 | s1(); 16 | f(2); 17 | expect(obj).to.eql({stuff:4}); 18 | 19 | }); 20 | 21 | it('should listen', ()=> { 22 | let count = 0; 23 | const fn1 = (i)=>count = (count + 1), fn2 = (i)=>count = (count + (i * 2)); 24 | const f = listen(); 25 | const s1 = f.subscribe(fn1), s2 = f.subscribe(fn2); 26 | expect(count).to.eql(0); 27 | f(1); 28 | expect(count).to.eql(3); 29 | f(2); 30 | expect(count).to.eql(8); 31 | s1(); 32 | s2(); 33 | f(1); 34 | expect(count).to.eql(8); 35 | const s3 = f.subscribe(fn1); 36 | f(1); 37 | f(2); 38 | expect(count).to.eql(10); 39 | s3(); 40 | f(1); 41 | expect(count).to.eql(10); 42 | 43 | }); 44 | 45 | it('should listen not share state', ()=> { 46 | let c1 = 0, c2 = 0; 47 | const fn1 = (i)=>c1 = i, fn2 = (i)=>c2 = i; 48 | 49 | const f1 = listen(), f2 = listen(); 50 | const s1 = f1.subscribe(fn1); 51 | const s2 = f2.subscribe(fn2); 52 | 53 | expect((f1(1), c1)).to.eql(1); 54 | expect((f2(1), c2)).to.eql(1); 55 | expect((s2(), f2(2), c1)).to.eql(1); 56 | expect((s1(), f1(2), c2)).to.eql(1); 57 | 58 | 59 | }); 60 | }); -------------------------------------------------------------------------------- /src/match.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TRBL = ['top', 'right', 'bottom', 'left']; 4 | const has = Function.call.bind(Object.prototype.hasOwnProperty); 5 | 6 | 7 | function handler(depth, parts, src, dest, obj) { 8 | if (!(src && dest)) return obj; 9 | const first = parts.length != 0 ? [parts.shift()] : depth[0]; 10 | const second = parts.length != 0 ? [parts.shift()] : depth[1]; 11 | for (const f of first) { 12 | let csrc = src[f], cdest = dest[f]; 13 | for (const s of second) { 14 | if (csrc && cdest && has(csrc, s) && has(cdest, s) && csrc[s] != cdest[s]) { 15 | (obj[f] || (obj[f] = {}))[s] = [csrc[s], cdest[s]]; 16 | } 17 | } 18 | } 19 | return obj; 20 | } 21 | function handlerSingle(depth, parts, src, dest, obj) { 22 | if (!(src && dest)) return obj; 23 | const second = parts.length != 0 ? [parts.shift()] : depth[0]; 24 | let csrc = src, cdest = dest; 25 | for (const s of second) { 26 | if (csrc && cdest && has(csrc, s) && has(cdest, s) && csrc[s] != cdest[s]) { 27 | obj[s] = [csrc[s], cdest[s]]; 28 | } 29 | } 30 | return obj; 31 | } 32 | function normalize(depth, parts) { 33 | if (!parts || parts.length === 0) return depth.concat(); 34 | const ret = []; 35 | for (let i = 0, l = depth.length; i < l; i++) { 36 | if (parts.length) { 37 | ret.push([parts.shift()]); 38 | } else { 39 | ret.push(depth[i]) 40 | } 41 | } 42 | return ret; 43 | } 44 | const handle = (parts, src, dest, obj)=> { 45 | return [src, dest]; 46 | }; 47 | 48 | const DECLS = ['height', 'width', 'opacity'].reduce((ret, k)=> { 49 | ret[k] = handle; 50 | return ret; 51 | }, { 52 | border: handler.bind(null, [TRBL, ['width', 'color', 'style']]), 53 | margin: handlerSingle.bind(null, [TRBL]), 54 | }); 55 | 56 | export default function match(transition, src, dest) { 57 | const parts = transition.split('-'); 58 | const p1 = parts.shift(); 59 | return {[p1]: DECLS[p1](parts, src, dest, {})}; 60 | 61 | 62 | } -------------------------------------------------------------------------------- /test/parseMedia-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import {parseMedia} from '../src/mediaSelector'; 3 | import {expect} from 'chai'; 4 | 5 | describe('parseMedia', function () { 6 | 7 | for (let [rule, valid] of [ 8 | [`(min-width: 700px)`, [{ 9 | "expressions": [ 10 | { 11 | "feature": "width", 12 | "modifier": "min", 13 | "value": "700px" 14 | } 15 | ], 16 | "inverse": false, 17 | "type": "all" 18 | }]], 19 | [`(min-width: 700px) and (orientation: landscape)`, [{ 20 | "inverse": false, 21 | "type": "all", 22 | "expressions": [{"modifier": "min", "feature": "width", "value": "700px"}, { 23 | "feature": "orientation", 24 | "modifier": void(0), 25 | "value": "landscape" 26 | }] 27 | }]], 28 | [`(min-width: 700px), handheld and (orientation: landscape)`, [{ 29 | "inverse": false, 30 | "type": "all", 31 | "expressions": [{"modifier": "min", "feature": "width", "value": "700px"}] 32 | }, { 33 | "inverse": false, 34 | "type": "handheld", 35 | "expressions": [{"feature": "orientation", "modifier": void(0), "value": "landscape"}] 36 | }]], 37 | [`screen and (min-aspect-ratio: 1/1) `, [{ 38 | "inverse": false, 39 | "type": "screen", 40 | "expressions": [{"modifier": "min", "feature": "aspect-ratio", "value": "1/1"}] 41 | }]], 42 | [`screen and (device-aspect-ratio: 16/9), screen and (device-aspect-ratio: 16/10)`, [{ 43 | "inverse": false, 44 | "type": "screen", 45 | "expressions": [{"feature": "device-aspect-ratio", "modifier": void(0), "value": "16/9"}] 46 | }, { 47 | "inverse": false, 48 | "type": "screen", 49 | "expressions": [{"feature": "device-aspect-ratio", "modifier": void(0), "value": "16/10"}] 50 | }]] 51 | ]) { 52 | 53 | it(`should parse rule ${rule}`, function () { 54 | expect(parseMedia(rule)).to.eql(valid); 55 | }) 56 | } 57 | 58 | }); -------------------------------------------------------------------------------- /src/features.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const toInt = (v)=> { 3 | return parseInt(v, 10); 4 | }; 5 | /** 6 | * This represents the ratio of horizontal pixels (first term) to vertical pixels (second term). 7 | * @param v 8 | * @param height 9 | * @param width 10 | * @returns {number} 11 | */ 12 | const ratio = (v, height, width) => { 13 | const [hor,ver=1] = v.split('/', 2).map(toInt); 14 | const vr = hor / ver; 15 | const cr = width / height; 16 | const ret = cr - vr; 17 | return ret; 18 | }; 19 | /** 20 | * 21 | * (ruleValue, currentValue); 22 | */ 23 | 24 | const DEVICE_FEATURES = { 25 | 'width': (rule, {width})=>width == rule, 26 | 'min-width': (rule, {width})=> width >= rule, 27 | 'max-width': (rule, {width})=> width <= rule, 28 | 'height': (rule, {height})=> rule == height, 29 | 'min-height': (rule, {height})=>height >= rule, 30 | 'max-height': (rule, {height})=> height <= rule, 31 | /* 32 | @media screen and (min-aspect-ratio: 1/1) { ... } 33 | This selects the style when the aspect ratio is either 1:1 or greater. In other words, these styles will only be applied when the viewing area is square or landscape. 34 | */ 35 | 'aspect-ratio': (v, {height, width}) => ratio(v, height, width) == 0, 36 | 'min-aspect-ratio': (v, {height, width}) => ratio(v, height, width) >= 0, 37 | 'max-aspect-ratio': (v, {height, width}) => ratio(v, height, width) <= 0 38 | }; 39 | 40 | const FEATURES = Object.assign({ 41 | 'color': (v, {scale}) => v == scale, 42 | 'min-color': (rule, {scale}) => scale >= rule, 43 | 'max-color': (rule, {scale}) => scale <= rule, 44 | 'orientation': (v, {height, width})=> { 45 | if (v === 'landscape') { 46 | return width > height; 47 | } else if (v === 'portrait') { 48 | return width < height; 49 | } 50 | } 51 | }, DEVICE_FEATURES, Object.keys(DEVICE_FEATURES).reduce((obj, key)=> { 52 | obj[key.replace(/^(min-|max-)?(.*)?/, '$1device-$2')] = DEVICE_FEATURES[key]; 53 | return obj; 54 | }, {}), 55 | 56 | //unsupported valid, media features 57 | ['color-index', 'min-color-index', 'max-color-index', 58 | 'monochrome', 'min-monochrome', 'max-monochrome', 59 | 'resolution', 'min-resolution', 'max-resolution', 60 | 'scan', 61 | 'grid'] 62 | .reduce((obj, feature)=> { 63 | obj[feature] = ()=> { 64 | console.log('unsupported media feature', feature); 65 | return false 66 | }; 67 | return obj; 68 | }, {})); 69 | 70 | export default FEATURES; -------------------------------------------------------------------------------- /src/DimensionComponent.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {View, Dimensions, PixelRatio, Platform} from 'react-native'; 3 | import {calculate, asArray, splitClass, splitComma, toggle, WINDOW, window} from './componentHelpers'; 4 | import listen from './listen'; 5 | import {createAnimatableComponent} from './AnimatedCSS'; 6 | 7 | export default function create(Wrapped, dynaStyles, keyframes, name) { 8 | 9 | const _styles = dynaStyles(); 10 | 11 | let Animated; 12 | 13 | return class DimensionComponent extends Component { 14 | static displayName = `${name}DimensionComponent`; 15 | 16 | static propTypes = { 17 | onResize: PropTypes.func, 18 | onAnimationBegin: PropTypes.func, 19 | onAnimationEnd: PropTypes.func, 20 | className: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]) 21 | }; 22 | 23 | static defaultProps = { 24 | onResize(e){ 25 | console.log('onResize', e); 26 | } 27 | }; 28 | state = {config: WINDOW}; 29 | 30 | constructor(props, ...args) { 31 | super(props, ...args); 32 | this.handleProps(props); 33 | } 34 | 35 | handleProps(props) { 36 | this._className = splitClass(props.className); 37 | this._styles = asArray(props.style); 38 | } 39 | 40 | componentWillReceiveProps(props) { 41 | this.handleProps(props); 42 | } 43 | 44 | componentWillMount() { 45 | this._resize = listen(); 46 | this._listenWindow || (this._listenWindow = window.subscribe(this.recalc)); 47 | // this._listenChildResize = this._resize.subscribe(this.recalc, this.props.onResize); 48 | this.recalc(this.state.config); 49 | } 50 | 51 | componentWillUnmount() { 52 | this._listenChildResize && this._listenChildResize(); 53 | this._listenWindow && this._listenWindow(); 54 | } 55 | 56 | recalc = (config) => { 57 | config = config || this.state.config; 58 | this.setState(calculate(config, _styles, this._className, this._styles, splitComma(this.state.pseudos), this.state)); 59 | }; 60 | 61 | handlePress = (e)=> { 62 | this.setState(calculate(this.state.config, _styles, this._className, this._styles, toggle(this.state.pseudos, 'checked'), this.state)); 63 | }; 64 | 65 | render() { 66 | 67 | const {children, onResize, className, ...props} = this.props; 68 | 69 | const {animation, transition, config, pseudos, ...state} = this.state; 70 | let DynaC = Wrapped; 71 | if (animation || transition) { 72 | DynaC = (Animated || (Animated = createAnimatableComponent(Wrapped, keyframes))); 73 | props.animation = animation; 74 | props.transition = transition; 75 | } 76 | return 77 | {children} 78 | 79 | } 80 | 81 | } 82 | } -------------------------------------------------------------------------------- /src/animation/css-locals-dimension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var transition = require('./transition'); 3 | var utils = require('./utils'); 4 | function isDimension(prop) { 5 | return /(?:(max|min)-)?(height|width)/.test(prop); 6 | } 7 | 8 | module.exports = function cssLocalsDimension(locals, opts) { 9 | opts = opts || utils.EMPTY_OBJ; 10 | var localOpts = opts['dimension'] || utils.EMPTY_OBJ; 11 | 12 | var updateLocal = localOpts.updateLocal || opts.localUpdate || utils.localUpdate; 13 | 14 | var selectors = localOpts.selectors || opts.selectors || utils.selectors; 15 | 16 | var classMap = selectors.reduce(function (ret, key) { 17 | if (key in locals) { 18 | ret[locals[key]] = key; 19 | } 20 | return ret; 21 | }, {}); 22 | 23 | var re = utils.classToRegexp(Object.keys(classMap)); 24 | 25 | /** 26 | * Two cases 27 | * 28 | * Transition X to auto. 29 | * .enter { 30 | * transition:height 1s ease; 31 | * height:0<-- fine. 32 | * } 33 | * .enterActive { 34 | * height:auto; <-- this triggers height auto 35 | * } 36 | * adds 37 | * \@enterActiveHeight = 'height 1s ease'; 38 | * 39 | * Transition from auto to X. 40 | * .enter { 41 | * transition:height 1s ease; 42 | * height:auto;<-- this triggers height auto 43 | * } 44 | * .enterActive { 45 | * height:0; 46 | * } 47 | * \@enterHeight = 'height 1s ease'; 48 | */ 49 | return function extractDimensions$return(css, result) { 50 | css.walkRules(re, function (s) { 51 | var selectorName = s.selector.replace(/^\./, ''); 52 | var prop = classMap[selectorName]; 53 | var trans = transition(); 54 | s.walkDecls(/^transition.*/, function (node) { 55 | trans[node.prop](node.value); 56 | }); 57 | trans.property().forEach(function (propname) { 58 | if (!isDimension(propname)) { 59 | return; 60 | } 61 | var found = false; 62 | 63 | s.walkDecls(propname, function (decl) { 64 | if (decl.value === 'auto') { 65 | //from auto. 66 | found = true; 67 | // decl.value === '100%'; 68 | // decl.cloneBefore({ prop: 'max-'+propname, value: '100%' }); 69 | decl.cloneBefore({ prop:decl.prop, value:'100%'}) 70 | decl.remove(); 71 | // const max = new Declaration({ prop: 'color', value: 'black' }); 72 | updateLocal(locals, prop, propname, trans.description().join(',')); 73 | } 74 | }); 75 | 76 | var activeProp = locals[prop + 'Active']; 77 | //so its a to auto. 78 | if (!found && activeProp) { 79 | css.walkRules(utils.classToRegexp([activeProp]), function (activeRule) { 80 | activeRule.walkDecls(propname, function (d) { 81 | if (d.value === 'auto') { 82 | //to auto 83 | updateLocal(locals, prop + '-active', propname, trans.description().join(',')); 84 | } 85 | }); 86 | }); 87 | } 88 | }); 89 | 90 | }); 91 | }; 92 | }; -------------------------------------------------------------------------------- /test/match-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {expect} from 'chai'; 4 | import {parse as _parse} from '../src/decls'; 5 | import match from '../src/match'; 6 | const parse = (value)=> { 7 | const {values} = _parse(...value.split(':', 2)); 8 | return values; 9 | }; 10 | describe('match-transition', function () { 11 | const matcher = (prop, src, dest, eql) => () => { 12 | return expect(match(prop, parse(src), parse(dest))).to.eql(eql); 13 | }; 14 | 15 | it('should match height', matcher('height', 'height:10px', 'height:5px', { 16 | height: ['10px', '5px'] 17 | })); 18 | it('should match width', matcher('width', 'width:10px', 'width:5px', { 19 | width: ['10px', '5px'] 20 | })); 21 | it('should match opacity', matcher('opacity', 'opacity:1', 'opacity:0', { 22 | opacity: ['1', '0'] 23 | })); 24 | 25 | it('should match margin', matcher('margin', 'margin:10px', 'margin:5px', { 26 | "margin": { 27 | "bottom": [ 28 | "10px", 29 | "5px" 30 | ], 31 | "left": [ 32 | "10px", 33 | "5px" 34 | ], 35 | "right": [ 36 | "10px", 37 | "5px" 38 | ], 39 | "top": [ 40 | "10px", 41 | "5px" 42 | ] 43 | } 44 | })); 45 | 46 | it('should match margin-right', matcher('margin-right', 'margin:10px', 'margin-right:5px', { 47 | "margin": { 48 | "right": [ 49 | "10px", 50 | "5px" 51 | ] 52 | } 53 | })); 54 | 55 | it('should match border-top', matcher('border-top', 'border-top:10px solid red', 'border:5px solid green', { 56 | "border": { 57 | "top": { 58 | "width": [ 59 | "10px", 60 | "5px" 61 | ], 62 | "color": [ 63 | "red", 64 | "green" 65 | ] 66 | } 67 | } 68 | })); 69 | it('should match border', matcher('border', 'border:10px solid red', 'border:5px solid green', { 70 | "border": { 71 | "top": { 72 | "width": [ 73 | "10px", 74 | "5px" 75 | ], 76 | "color": [ 77 | "red", 78 | "green" 79 | ] 80 | }, 81 | "right": { 82 | "width": [ 83 | "10px", 84 | "5px" 85 | ], 86 | "color": [ 87 | "red", 88 | "green" 89 | ] 90 | }, 91 | "bottom": { 92 | "width": [ 93 | "10px", 94 | "5px" 95 | ], 96 | "color": [ 97 | "red", 98 | "green" 99 | ] 100 | }, 101 | "left": { 102 | "width": [ 103 | "10px", 104 | "5px" 105 | ], 106 | "color": [ 107 | "red", 108 | "green" 109 | ] 110 | } 111 | } 112 | })) 113 | }); 114 | -------------------------------------------------------------------------------- /src/declarations/border.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import {allUnit, isBorderUnit} from '../unit'; 3 | import prefill, {trbl, TRBL} from '../fill'; 4 | import words from '../words'; 5 | 6 | const PROPS = ['width', 'style', 'color']; 7 | 8 | const _set = (obj, prop) => { 9 | if (Object.hasOwnProperty(prop)) { 10 | return obj[prop]; 11 | } 12 | return (obj[prop] = {}); 13 | }; 14 | const _border = (side, prop, values, obj)=> { 15 | 16 | if (side && prop) { 17 | _set(obj, side)[prop] = trbl(values, side); 18 | } else if (prop) { 19 | TRBL.forEach((s)=> { 20 | _border(s, prop, values, obj); 21 | }) 22 | } else if (side) { 23 | for (let i = 0, l = Math.min(values.length, PROPS.length); i < l; i++) { 24 | (obj[side] || (obj[side] = {}))[PROPS[i]] = values[i]; 25 | } 26 | } else { 27 | const units = allUnit(values, isBorderUnit); 28 | if (units) { 29 | TRBL.forEach(s => _set(obj, s).width = trbl(units, s)); 30 | } else { 31 | TRBL.forEach(s => PROPS.forEach((p, i)=> (i < values.length) && ((obj[s] || (obj[s] = {}))[p] = values[i]))); 32 | } 33 | } 34 | return obj; 35 | }; 36 | 37 | /* 38 | border-top-left-radius: 0 39 | border-top-right-radius: 0 40 | border-bottom-right-radius: 0 41 | border-bottom-left-radius: 0 42 | */ 43 | 44 | const radius = (side, corner, values, obj = {})=> { 45 | 46 | if (side && corner) { 47 | obj[`${side}-${corner}-radius`] = prefill(values, 0); 48 | } else if (side) { 49 | switch (side) { 50 | case 'left': 51 | case 'right': 52 | radius('top', side, prefill(values, 0), obj); 53 | radius('bottom', side, prefill(values, 1), obj); 54 | break; 55 | case 'top': 56 | case 'bottom': 57 | radius(side, 'left', prefill(values, 0), obj); 58 | radius(side, 'right', prefill(values, 1), obj); 59 | break; 60 | } 61 | } else if (corner) { 62 | radius('top', corner, prefill(values, 0), obj); 63 | radius('bottom', corner, prefill(values, 1), obj); 64 | } else { 65 | let i = 0; 66 | for (let [s,c] of [['top', 'left'], ['top', 'right'], ['bottom', 'right'], ['bottom', 'left']]) { 67 | radius(s, c, prefill(values, i++), obj); 68 | } 69 | } 70 | return obj; 71 | }; 72 | 73 | const handle = (type, values, obj = {})=> { 74 | const result = /^(?:border)?(?:-(top|right|bottom|left))?(?:-(right|left))?(?:-(width|style|color|radius))?$/.exec(type); 75 | if (result == null) { 76 | console.warn('do not understand ', type, values); 77 | return null; 78 | } 79 | const [ , side, corner, prop] = result; 80 | if (prop === 'radius') { 81 | return radius(side, corner, values); 82 | } else { 83 | return _border(side, prop, values, obj); 84 | } 85 | }; 86 | 87 | const parse = (str) => { 88 | const [decl, stuff] = str.split(/\s*:\s*/, 2); 89 | const value = stuff.split(/\s+?/); 90 | 91 | 92 | return handle(decl, value); 93 | }; 94 | 95 | export const border = (prefix, value)=> { 96 | return handle(prefix ? `-${prefix}` : '', words(value)); 97 | }; 98 | 99 | 100 | export default function toStyle(values) { 101 | 102 | return parse(Array.isArray(values) ? values.join(' ') : values); 103 | } -------------------------------------------------------------------------------- /test/decls-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {parse} from '../src/decls'; 4 | import {expect} from 'chai'; 5 | 6 | 7 | describe('decls', function () { 8 | 9 | describe('variations of parse', function () { 10 | for (const [test, result] of [ 11 | [[1], { 12 | "top": "1", 13 | "right": "1", 14 | "bottom": "1", 15 | "left": "1" 16 | }], 17 | [[1, 2], { 18 | "top": "1", 19 | "right": "2", 20 | "bottom": "1", 21 | "left": "2" 22 | }], 23 | [[1, 2, 3], { 24 | "top": "1", 25 | "right": "2", 26 | "bottom": "3", 27 | "left": "2" 28 | }], 29 | [[1, 2, 3, 4], { 30 | "top": "1", 31 | "right": "2", 32 | "bottom": "3", 33 | "left": "4" 34 | }] 35 | ]) { 36 | it(`should parse ${test} `, function () { 37 | const t = parse('margin', test.join(' ')); 38 | //.values.map(v=>v.value); 39 | expect(t.values).to.eql(result); 40 | 41 | }) 42 | } 43 | }); 44 | 45 | it('should return decls', function () { 46 | expect(parse('margin', '10px 3% 1em')).to.eql({ 47 | "type": "margin", 48 | "values": { 49 | "bottom": "1em", 50 | "left": "3%", 51 | "right": "3%", 52 | "top": "10px" 53 | }, 54 | "vendor": false 55 | }) 56 | }); 57 | it('should parse specific', function () { 58 | const {type, vendor, values} = parse('-ios-margin-left', '5px'); 59 | expect(type).to.eql('margin'); 60 | expect(values).to.eql({ 61 | left: '5px' 62 | }); 63 | }); 64 | 65 | it('should parse border radius', function () { 66 | const ret = parse('border-radius', '5px'); 67 | expect(ret).to.eql({ 68 | type: 'border', 69 | vendor: false, 70 | values: { 71 | "bottom-left-radius": "5px", 72 | "bottom-right-radius": "5px", 73 | "top-left-radius": "5px", 74 | "top-right-radius": "5px" 75 | } 76 | }) 77 | }); 78 | 79 | it('should parse border radius-top-radius', function () { 80 | const ret = parse('border-top-left-radius', '5px'); 81 | expect(ret).to.eql({ 82 | type: 'border', 83 | vendor: false, 84 | values: { 85 | "top-left-radius": "5px" 86 | } 87 | }) 88 | }); 89 | it('should parse border-top-left-radius', function () { 90 | const ret = parse('border-top-left-radius', '5px'); 91 | expect(ret).to.eql({ 92 | type: 'border', 93 | vendor: false, 94 | values: { 95 | "top-left-radius": "5px" 96 | } 97 | }) 98 | }); 99 | it('should parse border-bottom-right-radius', function () { 100 | expect(parse('border-bottom-right-radius', '5px')).to.eql({ 101 | "type": "border", 102 | "values": { 103 | "bottom-right-radius": "5px" 104 | }, 105 | "vendor": false 106 | }); 107 | 108 | }); 109 | 110 | }); -------------------------------------------------------------------------------- /src/componentHelpers.js: -------------------------------------------------------------------------------- 1 | import {Platform, Dimensions} from 'react-native'; 2 | import RCTDeviceEventEmitter from 'RCTDeviceEventEmitter'; 3 | import listen from './listen'; 4 | 5 | const whiteRe = /\s+?/; 6 | const commaRe = /,\s*/; 7 | 8 | export const classNamesWithPsuedos = (classNames = [], psuedos)=> { 9 | 10 | const all = ['__current', ...classNames]; 11 | if (!psuedos || psuedos.length === 0) { 12 | return all 13 | } 14 | 15 | return all.concat(all.reduce((ret, name)=> { 16 | for (const pseudo of psuedos) { 17 | ret.push(`${name}:${pseudo}`); 18 | } 19 | return ret; 20 | }, [])); 21 | }; 22 | 23 | export const isObjectEmpty = (value) => { 24 | if (value == null) return true; 25 | //noinspection LoopStatementThatDoesntLoopJS,JSUnusedLocalSymbols 26 | 27 | for (var key in value) { 28 | return false; 29 | } 30 | return true; 31 | }; 32 | 33 | export const toggle = (all, str)=> { 34 | const parts = all ? all.split(',') : []; 35 | const idx = parts.indexOf(str); 36 | if (idx === -1) { 37 | parts.push(str); 38 | } else { 39 | parts.splice(idx, 1); 40 | } 41 | return parts; 42 | }; 43 | 44 | const lastStyle = (styles, key)=> { 45 | if (!styles) return; 46 | for (let i = styles.length - 1; i != -1; i--) { 47 | if (styles[i][key]) 48 | return styles[i][key]; 49 | } 50 | }; 51 | 52 | export const calculate = (c, dyna = {}, classNames = [], styles = [], psuedos = [], prevState) => { 53 | //noinspection JSUnusedLocalSymbols 54 | const {x, y, width, height} = (c && c.layout) || {}; 55 | 56 | 57 | const config = Object.assign({}, WINDOW, {clientHeight: height, clientWidth: width}); 58 | const ret = {config, pseudos: psuedos.join(',')}; 59 | let transition; 60 | for (const className of classNamesWithPsuedos(classNames, psuedos)) { 61 | 62 | if (className in dyna) { 63 | const {__animation, __transition, __style} = dyna[className](config); 64 | 65 | if (!isObjectEmpty(__style)) { 66 | (ret.style || (ret.style = [])).push(__style); 67 | } 68 | if (!isObjectEmpty(__animation)) { 69 | (ret.animation || (ret.animation = [])).push(__animation); 70 | } 71 | if (!isObjectEmpty(__transition)) { 72 | //noinspection JSUnusedAssignment 73 | transition = Object.assign(transition || {}, __transition); 74 | } 75 | } 76 | } 77 | if (transition) { 78 | Object.keys(transition).forEach((key)=> { 79 | const from = lastStyle(prevState.style, key), to = lastStyle(ret.style, key); 80 | if (from == null || to == null) { 81 | return; 82 | } 83 | const t = transition[key]; 84 | const r = ret.transition || (ret.transition = {}); 85 | const re = Object.assign(r[key] || (r[key] = {}), t); 86 | re.from = from; 87 | re.to = to; 88 | }); 89 | } 90 | 91 | return ret; 92 | 93 | }; 94 | export const asArray = (val)=> { 95 | if (val == null) return; 96 | if (!Array.isArray(val)) return [val]; 97 | return val; 98 | }; 99 | export const splitClass = (str)=> { 100 | if (!str) return; 101 | if (typeof str === 'string') { 102 | return str.split(whiteRe); 103 | } 104 | return str; 105 | }; 106 | 107 | 108 | export const splitComma = (str)=> { 109 | return str ? str.split(commaRe) : []; 110 | }; 111 | export const WINDOW = { 112 | vendor: Platform.OS 113 | }; 114 | 115 | export const window = listen(); 116 | 117 | RCTDeviceEventEmitter.addListener('didUpdateDimensions', () => { 118 | Object.assign(WINDOW, Dimensions.get('window')); 119 | window(); 120 | }); 121 | -------------------------------------------------------------------------------- /test/features-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import FEATURES from '../src/features'; 3 | import {expect} from 'chai'; 4 | 5 | function feature(type, value, {width=0, height=0, scale=0}, answer) { 6 | return expect(FEATURES[type](value, { 7 | width, 8 | height, 9 | scale 10 | }), `${type} ${value} ${width}/${height}@${scale} is ${answer}`).to.be[answer+'']; 11 | } 12 | 13 | 14 | describe('features', function () { 15 | 16 | it('should match orientation', function () { 17 | feature('orientation', 'landscape', {height: 300, width: 400}, true); 18 | feature('orientation', 'landscape', {height: 600, width: 400}, false); 19 | feature('orientation', 'portrait', {height: 300, width: 400}, false); 20 | feature('orientation', 'portrait', {height: 600, width: 400}, true); 21 | }); 22 | 23 | it('should match color', function () { 24 | feature('color', 2, {scale: 1}, false); 25 | feature('color', 1, {scale: 1}, true); 26 | 27 | feature('min-color', 1, {scale: 1}, true); 28 | feature('min-color', 2, {scale: 4}, true); 29 | feature('min-color', 5, {scale: 4}, false); 30 | 31 | feature('max-color', 2, {scale: 1}, true); 32 | feature('max-color', 5, {scale: 4}, true); 33 | feature('max-color', 2, {scale: 4}, false); 34 | }); 35 | 36 | for (let prefix of ['', 'device-']) { 37 | it(`should match max-${prefix}height`, function () { 38 | feature(`max-${prefix}height`, 200, {height: 300}, false); 39 | feature(`max-${prefix}height`, 200, {height: 100}, true); 40 | }); 41 | it(`should match min-${prefix}height`, function () { 42 | feature(`min-${prefix}height`, 200, {height: 300}, true); 43 | feature(`min-${prefix}height`, 200, {height: 100}, false); 44 | }); 45 | it(`should match max-${prefix}width`, function () { 46 | feature(`max-${prefix}width`, 200, {width: 300}, false); 47 | feature(`max-${prefix}width`, 200, {width: 100}, true); 48 | }); 49 | it(`should match min-${prefix}width`, function () { 50 | feature(`min-${prefix}width`, 200, {width: 300}, true); 51 | feature(`min-${prefix}width`, 200, {width: 100}, false); 52 | }); 53 | it(`should match ${prefix}aspect-ratio`, function () { 54 | feature(`aspect-ratio`, `1/1`, {width: 300, height: 200}, false); 55 | feature(`aspect-ratio`, `1/1`, {width: 200, height: 200}, true); 56 | }); 57 | it(`should match min-${prefix}aspect-ratio`, function () { 58 | 59 | feature(`min-${prefix}aspect-ratio`, `1/1`, { 60 | width: 300, 61 | height: 200 62 | }, true); 63 | 64 | feature(`min-${prefix}aspect-ratio`, `1/1`, { 65 | width: 200, 66 | height: 200 67 | }, true); 68 | 69 | feature(`min-${prefix}aspect-ratio`, `2/1`, { 70 | width: 400, 71 | height: 300 72 | }, false); 73 | 74 | feature(`min-${prefix}aspect-ratio`, `2/1`, { 75 | width: 400, 76 | height: 100 77 | }, true); 78 | 79 | }); 80 | it(`should match max-${prefix}aspect-ratio`, function () { 81 | 82 | feature(`max-${prefix}aspect-ratio`, `1/1`, { 83 | width: 300, 84 | height: 400 85 | }, true); 86 | 87 | feature(`max-${prefix}aspect-ratio`, `1/1`, { 88 | width: 200, 89 | height: 200 90 | }, true); 91 | 92 | feature(`max-${prefix}aspect-ratio`, `2/1`, { 93 | width: 400, 94 | height: 200 95 | }, true); 96 | 97 | feature(`max-${prefix}aspect-ratio`, `2/1`, { 98 | width: 100, 99 | height: 400 100 | }, true); 101 | }); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /test/border-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import nb from '../src/declarations/border'; 3 | import {expect} from 'chai'; 4 | 5 | /** 6 | border-width: as each of the properties of the shorthand: 7 | border-top-width: medium 8 | border-right-width: medium 9 | border-bottom-width: medium 10 | border-left-width: medium 11 | border-style: as each of the properties of the shorthand: 12 | border-top-style: none 13 | border-right-style: none 14 | border-bottom-style: none 15 | border-left-style: none 16 | border-color: as each of the properties of the shorthand: 17 | border-top-color: currentColor 18 | border-right-color: currentColor 19 | border-bottom-color: currentColor 20 | border-left-color: currentColor 21 | **/ 22 | 23 | describe('border', function () { 24 | const borderTest = (test, eql, fn = it)=>fn(`should normalize ${test}`, ()=>expect(nb(test), test).to.eql(eql)); 25 | 26 | borderTest('border: 1px solid red', { 27 | top: {width: '1px', style: 'solid', color: 'red'}, 28 | right: {width: '1px', style: 'solid', color: 'red'}, 29 | bottom: {width: '1px', style: 'solid', color: 'red'}, 30 | left: {width: '1px', style: 'solid', color: 'red'} 31 | 32 | }); 33 | borderTest('border: medium', { 34 | top: {width: 'medium'}, 35 | right: {width: 'medium'}, 36 | bottom: {width: 'medium'}, 37 | left: {width: 'medium'} 38 | 39 | }); 40 | borderTest('border: medium thick', { 41 | top: {width: 'medium'}, 42 | right: {width: 'thick'}, 43 | bottom: {width: 'medium'}, 44 | left: {width: 'thick'} 45 | 46 | }); 47 | borderTest('border: 1px', { 48 | top: {width: '1px'}, 49 | right: {width: '1px'}, 50 | bottom: {width: '1px'}, 51 | left: {width: '1px'} 52 | }); 53 | 54 | borderTest('border: 1px 2px', { 55 | top: {width: '1px'}, 56 | right: {width: '2px'}, 57 | bottom: {width: '1px'}, 58 | left: {width: '2px'} 59 | }); 60 | borderTest('border: 1px 2px 3px', { 61 | top: {width: '1px'}, 62 | right: {width: '2px'}, 63 | bottom: {width: '3px'}, 64 | left: {width: '2px'} 65 | }); 66 | borderTest('border: 1px 2px 3px 4px', { 67 | top: {width: '1px'}, 68 | right: {width: '2px'}, 69 | bottom: {width: '3px'}, 70 | left: {width: '4px'} 71 | }); 72 | borderTest('border-left-radius: 2px 1px', { 73 | "top-left-radius": "2px", 74 | "bottom-left-radius": "1px" 75 | }); 76 | borderTest('border-top-radius: 2px', { 77 | "top-left-radius": "2px", 78 | "top-right-radius": "2px" 79 | }); 80 | borderTest('border-top-left-radius: 2px', { 81 | "top-left-radius": "2px" 82 | }); 83 | borderTest('border-radius: 2px 3px 4px', { 84 | "top-left-radius": "2px", 85 | "top-right-radius": "3px", 86 | "bottom-right-radius": "4px", 87 | "bottom-left-radius": "3px" 88 | }); 89 | borderTest('border-radius: 2px 3px', { 90 | "top-left-radius": "2px", 91 | "top-right-radius": "3px", 92 | "bottom-right-radius": "2px", 93 | "bottom-left-radius": "3px" 94 | }); 95 | borderTest('border-radius: 2px', { 96 | "top-left-radius": "2px", 97 | "top-right-radius": "2px", 98 | "bottom-right-radius": "2px", 99 | "bottom-left-radius": "2px" 100 | }); 101 | borderTest('border-top-width: 2px', { 102 | top: { 103 | width: '2px' 104 | } 105 | }); 106 | borderTest('border-top: 2px solid red', { 107 | top: { 108 | width: '2px', 109 | color: 'red', 110 | style: 'solid' 111 | } 112 | }); 113 | 114 | borderTest('border-top: 3px solid red', { 115 | top: { 116 | width: '3px', 117 | color: 'red', 118 | style: 'solid' 119 | } 120 | }); 121 | borderTest('border-top: medium solid green', { 122 | top: { 123 | width: 'medium', 124 | color: 'green', 125 | style: 'solid' 126 | } 127 | }); 128 | 129 | 130 | }); 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import postcss from 'postcss'; 3 | import {expect} from 'chai'; 4 | import FEATURES from '../../src/features'; 5 | import plugin from '../../src/index'; 6 | import source from '../../src/source' 7 | import listen from '../../src/listen' 8 | 9 | export const mockReactNative = (dimensions = {height: 500, width: 500, scale: 1}, OS = 'ios') => { 10 | return { 11 | StyleSheet: { 12 | create(o){ 13 | return Object.keys(o).reduce((ret, key, idx)=> { 14 | ret[key] = o[key]; 15 | return ret; 16 | }, {}); 17 | } 18 | }, 19 | Dimensions: { 20 | get: function (str) { 21 | return dimensions; 22 | } 23 | }, 24 | Platform: { 25 | OS 26 | }, 27 | View: 'View', 28 | Text: 'Text' 29 | } 30 | }; 31 | export const MockReact = { 32 | createClass(...args){ 33 | return args; 34 | }, 35 | createFactory(...args){ 36 | return args; 37 | } 38 | }; 39 | 40 | const log = (src)=> { 41 | console.log('\n\n---source--\n\n', src, '\n\n--- end of source---\n'); 42 | }; 43 | 44 | export const makeRequire = (dimensions, Platform)=> { 45 | const mockModules = { 46 | 'react-native': mockReactNative(dimensions, Platform), 47 | 'react': MockReact, 48 | 'postcss-react-native/src/listen': {default: listen}, 49 | 'postcss-react-native/src/features': {default: FEATURES}, 50 | 'postcss-react-native/src/flatten': {default: (_, v)=>v}, 51 | 'postcss-react-native/src/AnimatedCSS': {default: {}}, 52 | 'postcss-react-native/src/DimensionComponent': {default: (...args)=>args}, 53 | 'RCTDeviceEventEmitter': { 54 | addListener(){ 55 | } 56 | } 57 | }; 58 | return (path)=> { 59 | if (path in mockModules) { 60 | return mockModules[path]; 61 | } 62 | return require(path); 63 | 64 | }; 65 | }; 66 | 67 | function compile(sources, require) { 68 | const src = source(sources); 69 | 70 | try { 71 | const f = new Function(['require', 'exports'], src); 72 | 73 | const ret = ()=> { 74 | const exports = {}; 75 | try { 76 | f(require, exports); 77 | } catch (err) { 78 | log(src); 79 | console.trace(err); 80 | throw err; 81 | } 82 | return exports; 83 | }; 84 | ret._source = src; 85 | return ret; 86 | } catch (e) { 87 | log(src); 88 | console.trace(e, 'source', src, '\n\n', e.message + ''); 89 | throw e; 90 | } 91 | } 92 | 93 | export const test = function (name, callback = v => v, toJSON = v=>v) { 94 | var input = read('test/fixtures/' + name + '.css'); 95 | return postcss(plugin({ 96 | toStyleSheet: function (json, css) { 97 | callback((dimensions, platform = 'ios')=> { 98 | return compile(json, makeRequire(dimensions, platform))(); 99 | }, css); 100 | 101 | }, toJSON 102 | })).process(input, {from: name, to: name}); 103 | 104 | }; 105 | 106 | export const testTransform = (name, callback = v => v)=> { 107 | return callback((dimensions, platform = 'ios')=> { 108 | return transform(name, makeRequire(dimensions, platform)); 109 | }); 110 | }; 111 | 112 | /** 113 | * Takes a src css parses and compiles it, descending down the imports. 114 | * @param name 115 | * @param req 116 | * @param map 117 | * @returns {Promise} 118 | */ 119 | export const transform = (name, req, map = {})=> { 120 | const rq = path => map[path] || req(path); 121 | return new Promise((resolve, reject)=> { 122 | postcss(plugin({ 123 | toJSON: _ => _, 124 | toStyleSheet(json, css) { 125 | const {imports=[]} = json; 126 | return Promise.all(imports.map(v => map[v.url] ? map[v.url] : transform(v.url, rq, map))) 127 | .then(_ => { 128 | const src = compile(json, rq, map); 129 | const res = map[name] = src(); 130 | resolve(res, css, src._source); 131 | }, reject); 132 | } 133 | })).process(read('test/fixtures/' + name.replace(/\.\/(.*)\.css$/, '$1') + '.css'), { 134 | from: name, 135 | to: name 136 | }).then(null, reject); 137 | }); 138 | }; 139 | 140 | export const testString = function (input, opts) { 141 | return postcss(plugin(opts)).process(input, {from: 'from', to: 'to'}); 142 | }; 143 | //use for debugging; 144 | export const toJSON = (obj, {source:{input:{file='rules'}}})=> { 145 | console.log(`export const ${file.split('/').pop()} = ${JSON.stringify(obj, null, 2)}`); 146 | return obj; 147 | }; 148 | export const read = function (path) { 149 | return fs.readFileSync(path, 'utf-8'); 150 | }; 151 | 152 | 153 | export const json = (name)=> { 154 | return JSON.parse(read(`test/fixtures/${name}.css.json`)) 155 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import postcss from 'postcss'; 4 | import {parse} from './decls'; 5 | import {parseMedia} from './mediaSelector'; 6 | import source from './source'; 7 | import unescape from './util/unescape'; 8 | import selector from './selector'; 9 | 10 | const importRe = /^\s*(?:url\(([^)]*)\)|"([^"]*)"|'([^']*)')(?:\s*(.*)?)$/; 11 | const DEFAULT_OPTS = { 12 | toJSON(obj, {source:{input:{file='rules'}}}){ 13 | return source(obj); 14 | }, 15 | toStyleSheet(obj, {source:{input:{file='rules'}}}){ 16 | } 17 | }; 18 | const clsName = (name = '__current', pseudo='')=> { 19 | return `${name}${pseudo}`; 20 | }; 21 | const createWalker = ({css, tags})=> { 22 | 23 | return (rule) => { 24 | const decls = []; 25 | rule.walkDecls((decl)=> { 26 | const d = parse(decl.prop, decl.value, decls); 27 | if (d != null) 28 | decls && decls.push(d); 29 | }); 30 | if (decls.length) { 31 | selector(rule.selector).map((v)=> { 32 | if (v.tag) { 33 | const cName = clsName(v['class'], v.pseudo); 34 | const tName = `${v.namespace}|${v.tag}`; 35 | const t = tags[tName] || (tags[tName] = {}); 36 | (t[cName] || (t[cName] = [])).push(...decls); 37 | 38 | } else if (v.class || v.pseudo) { 39 | const cName = clsName(v['class'], v.pseudo); 40 | (css[cName] || (css[cName] = [])).push(...decls); 41 | } else { 42 | console.log('dunno about this.', v); 43 | } 44 | }); 45 | } 46 | }; 47 | }; 48 | module.exports = postcss.plugin('postcss-react-native', function (opts) { 49 | opts = Object.assign({}, DEFAULT_OPTS, opts); 50 | 51 | return (src, result) => { 52 | 53 | const ro = {css: {}, tags: {}}; 54 | const root = { 55 | rules: [ro], 56 | namespaces: {}, 57 | imports: [], 58 | exports: {} 59 | }; 60 | const walker = createWalker(ro); 61 | src.walkRules((rule)=> { 62 | if (rule.selector === ':export') { 63 | rule.walkDecls(function (decl) { 64 | root.exports[decl.prop] = decl.value; 65 | }); 66 | rule.remove(); 67 | return; 68 | } 69 | if (rule.parent.type !== 'root') { 70 | return; 71 | } 72 | walker(rule); 73 | }); 74 | src.walkAtRules((atrule)=> { 75 | const {name, params} = atrule; 76 | switch (name) { 77 | case 'media': 78 | { 79 | const aro = {css: {}, tags: {}, expressions: parseMedia(params)}; 80 | root.rules.push(aro); 81 | atrule.walkRules(createWalker(aro)); 82 | break; 83 | } 84 | case 'namespace': 85 | { 86 | const [prefix, ns] = postcss.list.space(params); 87 | const pns = unescape(ns); 88 | root.namespaces[pns ? prefix : pns] = pns; 89 | break; 90 | } 91 | case 'import': 92 | { 93 | const [match, url, squote, quote, tparam] =importRe.exec(params); 94 | root.imports.push({url: url || squote || quote, params: tparam}); 95 | break; 96 | 97 | } 98 | case 'keyframes': 99 | { 100 | const keyframes = root.keyframes || (root.keyframes = {}); 101 | const aro = keyframes[params] || (keyframes[params] = {css: {}}); 102 | atrule.walkRules((rule)=> { 103 | const selector = rule.selector.split(/\,\s*/); 104 | rule.walkDecls((decl)=> { 105 | const val = parse(decl.prop, decl.value, null, decl); 106 | for (const s of selector) { 107 | let n; 108 | switch (s) { 109 | case 'from': 110 | n = '0'; 111 | break; 112 | case 'to': 113 | n = '100'; 114 | break; 115 | default: 116 | n = s.replace(/%$/, ''); 117 | } 118 | if (!aro.css[n]) aro.css[n] = []; 119 | aro.css[n].push(val); 120 | } 121 | }) 122 | }); 123 | break; 124 | } 125 | default: 126 | console.warn(`unimplemented atrule ${name} ${params}`); 127 | } 128 | }); 129 | 130 | return opts.toStyleSheet(opts.toJSON(root, src, result), src, result); 131 | } 132 | }); -------------------------------------------------------------------------------- /test/fixtures/clazz.css.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "css": { 5 | "whatever": [ 6 | { 7 | "type": "margin", 8 | "vendor": false, 9 | "values": { 10 | "top": "10px", 11 | "right": "10px", 12 | "bottom": "10px", 13 | "left": "10px" 14 | } 15 | } 16 | ] 17 | }, 18 | "tags": { 19 | "View|MyView": { 20 | "combine": [ 21 | { 22 | "type": "height", 23 | "vendor": false, 24 | "values": "20px" 25 | } 26 | ], 27 | "__current": [ 28 | { 29 | "type": "flex", 30 | "vendor": false, 31 | "values": { 32 | "": "1" 33 | } 34 | }, 35 | { 36 | "type": "border", 37 | "vendor": false, 38 | "values": { 39 | "top": { 40 | "width": "1px", 41 | "style": "solid", 42 | "color": "red" 43 | }, 44 | "right": { 45 | "width": "1px", 46 | "style": "solid", 47 | "color": "red" 48 | }, 49 | "bottom": { 50 | "width": "1px", 51 | "style": "solid", 52 | "color": "red" 53 | }, 54 | "left": { 55 | "width": "1px", 56 | "style": "solid", 57 | "color": "red" 58 | } 59 | } 60 | }, 61 | { 62 | "type": "transition", 63 | "vendor": false, 64 | "values": [ 65 | { 66 | "property": "height", 67 | "duration": 1000, 68 | "timing-function": "ease-in-out", 69 | "delay": 0 70 | } 71 | ], 72 | "prefix": "transition" 73 | } 74 | ], 75 | "whatever": [ 76 | { 77 | "type": "margin", 78 | "vendor": false, 79 | "values": { 80 | "top": "15px", 81 | "right": "15px", 82 | "bottom": "15px", 83 | "left": "15px" 84 | } 85 | }, 86 | { 87 | "type": "flex", 88 | "vendor": false, 89 | "values": { 90 | "direction": "column" 91 | } 92 | } 93 | ], 94 | "appear": [ 95 | { 96 | "type": "height", 97 | "vendor": false, 98 | "values": "0px" 99 | }, 100 | { 101 | "type": "transition", 102 | "vendor": false, 103 | "values": [ 104 | { 105 | "property": "height", 106 | "duration": 500, 107 | "timing-function": "ease-in", 108 | "delay": 0 109 | } 110 | ], 111 | "prefix": "transition" 112 | } 113 | ], 114 | "appear-active": [ 115 | { 116 | "type": "height", 117 | "vendor": false, 118 | "values": "100px" 119 | } 120 | ], 121 | "leave": [ 122 | { 123 | "type": "height", 124 | "vendor": false, 125 | "values": "100px" 126 | }, 127 | { 128 | "type": "transition", 129 | "vendor": false, 130 | "values": [ 131 | { 132 | "property": "height", 133 | "duration": 500, 134 | "timing-function": "ease-in", 135 | "delay": 0 136 | } 137 | ], 138 | "prefix": "transition" 139 | } 140 | ], 141 | "leave-active": [ 142 | { 143 | "type": "height", 144 | "vendor": false, 145 | "values": "0px" 146 | } 147 | ], 148 | "enter": [ 149 | { 150 | "type": "height", 151 | "vendor": false, 152 | "values": "0px" 153 | }, 154 | { 155 | "type": "transition", 156 | "vendor": false, 157 | "values": [ 158 | { 159 | "property": "height", 160 | "duration": 500, 161 | "timing-function": "ease-in", 162 | "delay": 0 163 | } 164 | ], 165 | "prefix": "transition" 166 | } 167 | ], 168 | "enter-active": [ 169 | { 170 | "type": "height", 171 | "vendor": false, 172 | "values": "100px" 173 | } 174 | ] 175 | } 176 | } 177 | } 178 | ], 179 | "namespaces": { 180 | "View": "react-native.View" 181 | }, 182 | "imports": [], 183 | "exports": {} 184 | } -------------------------------------------------------------------------------- /src/decls.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import DECLS from './declarations'; 3 | import words from './words'; 4 | import parseColor from 'parse-color'; 5 | /** 6 | * 7 | * 1->1 1 1 1 8 | * 1 2 -> 1 2 1 2 9 | * 1 2 3 -> 1 2 3 2 10 | * 11 | * @param values 12 | */ 13 | 14 | const toMap = (o, v, i)=> { 15 | o[v] = i; 16 | return o 17 | }; 18 | 19 | 20 | const RTRBLV = ['top', 'right', 'bottom', 'left', 'vertical']; 21 | const TRBL = RTRBLV.reduce(toMap, {}); 22 | const TRBLV = Object.assign({}, TRBL, { 23 | vertical: 4 24 | }); 25 | 26 | const postfixWrap = (postfix, value)=> { 27 | return postfix ? {[postfix]: value} : value; 28 | }; 29 | 30 | function _box(postfix, values, table = TRBLV, rtable = RTRBLV) { 31 | const v0 = values.splice(0, 1)[0]; 32 | if (!v0) { 33 | return values; 34 | } 35 | if (postfix && postfix in table) { 36 | values[table[postfix]] = v0; 37 | } else if (values.length === 0) { 38 | values.push(v0, v0, v0, v0); 39 | } else if (values.length === 1) { 40 | values.unshift(v0); 41 | values.push(values[0], values[1]); 42 | } else if (values.length === 2) { 43 | values.unshift(v0); 44 | values.push(values[1]); 45 | } else if (values.length === 3) { 46 | values.unshift(v0); 47 | } 48 | 49 | return values.reduce((obj, val, idx)=> { 50 | if (val) { 51 | obj[rtable[idx]] = val; 52 | } 53 | return obj; 54 | }, {}); 55 | } 56 | function box(postfix, values, prefix, tag, decls, table = TRBLV) { 57 | return _box(postfix, words(values), table); 58 | } 59 | const color = (postfix, value)=> { 60 | const pc = parseColor(value); 61 | if (!pc || !pc.rgba) { 62 | console.log('could not parse color', postfix, value); 63 | return value; 64 | } 65 | const c = `rgba(${pc['rgba'].join(', ')})`; 66 | return postfix ? {[postfix]: c} : c; 67 | }; 68 | 69 | //const color = postfixWrap.bind(null, 'color'); 70 | 71 | const unit = (postfix, value)=>value; 72 | const enumer = (...enums)=>postfixWrap; 73 | const first = (postfix, value)=> { 74 | return value; 75 | }; 76 | export const HANDLERS = Object.assign({}, DECLS, { 77 | width: unit, 78 | height: unit, 79 | top: unit, 80 | left: unit, 81 | right: unit, 82 | bottom: unit, 83 | margin: box, 84 | padding: box, 85 | color, 86 | shadow: enumer('color', 'offset', 'opacity', 'radius'), 87 | position: enumer('absolute', 'relative'), 88 | flex(postfix, value){ 89 | switch (postfix) { 90 | case 'direction': 91 | return enumer('row', 92 | 'column')(postfix, value); 93 | case 'wrap': 94 | return enumer('wrap', 95 | 'nowrap')(postfix, value); 96 | case 'grow': 97 | case 'shrink': 98 | case 'basis': 99 | 100 | } 101 | return { 102 | ['']: value 103 | }; 104 | }, 105 | justify(postfix, value){ 106 | switch (postfix) { 107 | case 'content': 108 | return enumer('flex-start', 109 | 'flex-end', 110 | 'center', 111 | 'space-between', 112 | 'space-around')(postfix, value); 113 | } 114 | }, 115 | content(postfix, value, vendor, tag, config){ 116 | config.prefix = 'content'; 117 | return value; 118 | }, 119 | align(postfix, value){ 120 | switch (postfix) { 121 | case 'items': 122 | return enumer('flex-start', 123 | 'flex-end', 124 | 'center', 125 | 'stretch')(postfix, value); 126 | case 'self': 127 | return enumer('auto', 128 | 'flex-start', 129 | 'flex-end', 130 | 'center', 131 | 'stretch')(postfix, value); 132 | } 133 | }, 134 | opacity: first, 135 | tint(postfix, value){ 136 | switch (postfix) { 137 | case 'color': 138 | return color(value) 139 | } 140 | }, 141 | overlay(postfix, value){ 142 | switch (postfix) { 143 | case 'color': 144 | return color(value); 145 | } 146 | }, 147 | 148 | overflow: enumer('visible', 'hidden'), 149 | backface: enumer('visiblility'), 150 | background(postfix, value){ 151 | switch (postfix) { 152 | case 'color': 153 | return color(postfix, value); 154 | } 155 | }, 156 | text(postfix, value){ 157 | switch (postfix) { 158 | case 'shadow-offset': 159 | case 'shadow-radius': 160 | case 'shadow-color': 161 | case 'align': 162 | case 'align-vertical': 163 | case 'decoration-line': 164 | case 'decoration-style': 165 | case 'decoration-color': 166 | return postfixWrap(postfix, value); 167 | } 168 | }, 169 | writing(postfix, value){ 170 | switch (postfix) { 171 | case 'direction': 172 | return postfixWrap(postfix, value); 173 | } 174 | } 175 | }); 176 | const VENDORS = ['mox', 'ie', 'ios', 'android', 'native', 'webkit', 'o', 'ms']; 177 | const declRe = new RegExp('^(?:-(' + (VENDORS.join('|')) + ')-)?(.+?)(?:-(.+?))?$'); 178 | 179 | export function parse(decl, str, tag) { 180 | const [match, vendor=false, type, postfix] = declRe.exec(decl); 181 | const handler = HANDLERS[type]; 182 | if (!handler) { 183 | console.warn('unknown type', type, postfix); 184 | return; 185 | } 186 | const config = {}; 187 | const values = handler(postfix, str, vendor, tag, config); 188 | return values == null ? null : {type, vendor, values, ...config}; 189 | } 190 | 191 | export default ({parse}); -------------------------------------------------------------------------------- /bin/prn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('babel-register'); 3 | var path = require('path'); 4 | 5 | var fs = require('fs'); 6 | var rdir = path.join.bind(path, process.cwd()); 7 | var postcss = require('postcss'); 8 | var plugin = require('../src/index'); 9 | var args = process.argv.splice(2); 10 | var DEFAULT = { 11 | src: rdir('./src/styles'), 12 | dest: rdir('./dest/styles'), 13 | once: false 14 | }; 15 | var source = require('../src/source').default; 16 | var conf = Object.assign({}, DEFAULT); 17 | function compile(name, input, output) { 18 | var p = postcss(plugin({toJSON:source , toStyleSheet: output})) 19 | return p.process(input, {from: name, to: name}).then(null, (e)=>{ 20 | console.trace('postcss caught an error', e.stack+''); 21 | }); 22 | } 23 | function help(err) { 24 | if (err) { 25 | console.warn(err); 26 | } 27 | console.log(` 28 | ${process.argv.slice(2).join(' ')} in ${rdir()} 29 | -h --help This message. 30 | -1 --once ${DEFAULT.once} Run once and exit . 31 | -w --watch ${DEFAULT.src} Directory to watch 32 | -d --dest ${DEFAULT.dest} Where to write transformed to 33 | -i --index ${path.join(DEFAULT.dest, 'index.js')} 34 | 35 | config: 36 | ${JSON.stringify(conf)} 37 | `); 38 | process.exit(1); 39 | } 40 | for (var i = 0, l = args.length; i < l; i++) { 41 | switch (args[i]) { 42 | case '-h': 43 | case '--help': 44 | return help(); 45 | case '-1': 46 | case '--once': 47 | conf.once = true; 48 | break; 49 | case '-w': 50 | case '--watch': 51 | conf.src = rdir(args[++i]); 52 | break; 53 | case '-d': 54 | case '--dest': 55 | conf.dest = rdir(args[++i]); 56 | break; 57 | case '-i': 58 | case '--index': 59 | conf.index = rdir(args[++i]); 60 | break 61 | } 62 | } 63 | if (!conf.index) { 64 | conf.index = path.join(conf.dest, 'index.js') 65 | 66 | } 67 | 68 | function mkpath(dir) { 69 | var dirs = dir.split(path.sep); 70 | var file = dirs.pop(); 71 | var d = ''; 72 | while (dirs.length) { 73 | d = `${d}${path.sep}${dirs.shift()}`; 74 | if (!fs.existsSync(d)) { 75 | fs.mkdirSync(d); 76 | } 77 | } 78 | return file; 79 | } 80 | function init(cb) { 81 | handleCss(fs.readdirSync(conf.src).filter(v=>/\.s?css$/.test(v))).then(cb); 82 | } 83 | 84 | if (conf.once) { 85 | init(()=>process.exit(0)); 86 | 87 | } else { 88 | 89 | var watchman = require('fb-watchman'); 90 | 91 | init(()=> { 92 | console.log('watching', conf.src); 93 | var client = new watchman.Client(); 94 | client.capabilityCheck({optional: [], required: ['relative_root']}, function (error, resp) { 95 | if (error) { 96 | console.log('ERROR', error); 97 | client.end(); 98 | return; 99 | } 100 | 101 | // Initiate the watchers 102 | watcher(client, conf.src, ['*.scss', '*.css'], function (files) { 103 | console.log('watching', files); 104 | 105 | handleCss(files.map(v=>v.name)).then(null, (e)=> { 106 | console.warn('Error with file', e.message); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | } 113 | function handleCss(files) { 114 | const all = files.map((file)=> { 115 | if (/\.s?css/.test(file)) { 116 | const read = fs.readFileSync(path.join(conf.src, file)) + ''; 117 | try { 118 | return compile(file, read, function (source) { 119 | try { 120 | var f = mkpath(path.join(conf.dest, file + '.js')); 121 | fs.writeFileSync(path.join(conf.dest, f), source); 122 | } catch (e) { 123 | console.warn('could not write ', file); 124 | } 125 | }); 126 | } catch (e) { 127 | console.warn('error compiling', file, e); 128 | } 129 | } 130 | }); 131 | if (conf.index) { 132 | all.push(writeIndex(files, conf.index, /(\.s?css)$/)); 133 | } 134 | return Promise.all(all); 135 | } 136 | 137 | 138 | function writeIndex(files, index, re) { 139 | console.log('writing', index, files.join(',')); 140 | const file = mkpath(index); 141 | fs.writeFileSync(index, `module.exports = ${writeObj(files, re)}`); 142 | } 143 | 144 | function writeObj(files, re) { 145 | return `{\n${files.map((v)=>`\t${JSON.stringify(v.replace(re, ''))}: require(${JSON.stringify('./' + v)})`).join(',\n')}\n};\n`; 146 | } 147 | 148 | function watcher(client, index, pattern, handler) { 149 | client.command(['watch-project', rdir()], function (err, resp) { 150 | const rel = path.relative(rdir(), index) 151 | subscribe(client, resp.watch, rel, pattern, handler); 152 | }); 153 | } 154 | 155 | function subscribe(client, watch, relative_path, patterns, handler) { 156 | var subscribe = `subscribe-${relative_path.replace('/', '-')}`; 157 | 158 | client.command(['subscribe', watch, subscribe, { 159 | expression: ["anyof"].concat(patterns.map(pattern=>["match", pattern])), 160 | fields: ["name", "size", "exists", "type"], 161 | relative_root: relative_path 162 | }], 163 | function (error, resp) { 164 | if ('warning' in resp) { 165 | console.log('warning: ', resp.warning); 166 | } 167 | if (error) { 168 | // Probably an error in the subscription criteria 169 | console.error('failed to subscribe: ', error); 170 | return; 171 | } 172 | console.log('subscription ' + resp.subscribe + ' established'); 173 | }); 174 | 175 | client.on('subscription', function (resp) { 176 | if ('warning' in resp) { 177 | console.log('warning: ', resp.warning); 178 | } 179 | if (resp.subscription == subscribe) { 180 | console.log('file changed: ' + relative_path); 181 | handler(resp.files); 182 | } 183 | }); 184 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS React Native 2 | [PostCSS](https://github.com/postcss/postcss) plugin to make react native stylesheets 3 | 4 | This is kind of a CSS to JSX transpiler. It can be used like a CSS module, but it can 5 | also be used to define/extend components in CSS to add transition, animation and styling 6 | attributes. 7 | 8 | 9 | 10 | [postcss-react-native](https://github.com/jspears/postcss-react-native) 11 | 12 | ***IN DEVELOPMENT*** 13 | Currently a proof of concept. It roughly based on [react-native-css](https://github.com/sabeurthabti/react-native-css) 14 | although it shares no code. 15 | 16 | [![Screen Cast](https://github.com/jspears/postcss-react-native/blob/master/ScreenShot.png)](https://github.com/jspears/PostcssReactNativeDemo/raw/master/ReactNativeCSS.mov) 17 | 18 | 19 | 20 | ##Capablities 21 | - [x] Recalculate stylesheet based on media queries and current dimensions. 22 | - [x] -ios,-android vendor prefixes. 23 | - [x] Supports px, vh, vw,in, pt,em,pc,vmin,vax units. 24 | - [x] handles border shorthand. 25 | - [x] handles margin shorthand. 26 | - [x] supports checked pseudo selector. 27 | - [x] handles most font properties. 28 | - [x] Transitions 29 | - [x] Animations 30 | - [x] Imports 31 | - [x] Percentage units. 32 | - [ ] Nice import from. I.E import from styles rather than dist. 33 | - [ ] filter properties not supported by react-native. Tricky, because it is component dependent. 34 | - [ ] Support regular react (currently only react-native). 35 | - [ ] Implement content, before and after pseudo's. 36 | - [ ] Nested selectors (partial support) 37 | - [ ] Support props selectors View[color="green"] {}. 38 | - [ ] Support background images, via Image with children. 39 | 40 | ## Usage 41 | 42 | ```js 43 | postcss([ require('postcss-react-native') ]) 44 | ``` 45 | 46 | See the [PostCSS docs](https://github.com/postcss/postcss#usage) for examples for your environment. 47 | 48 | ## Watcher Usage 49 | Since most React Native environments do not have a css pipeline 50 | you can use the prn watcher. 51 | 52 | ```sh 53 | $ npm install postcss-react-native 54 | $ ./node_modules/.bin/prn -d dist -w ./style 55 | 56 | ``` 57 | 58 | ## Usage 59 | You should be able to include the said css via regular require 60 | 61 | styles/SpecialComponent.css 62 | 63 | ```css 64 | .name { 65 | border: 1px solid red; 66 | margin: 5px; 67 | } 68 | 69 | ``` 70 | 71 | Write your css using namespaces to import component. 72 | EX: ./styles/component.css 73 | ```css 74 | @namespace Native "react-native.View"; 75 | @namespace Text "react-native.Text"; 76 | 77 | Text|StyledText { 78 | color: red; 79 | background-color: yellow; 80 | } 81 | 82 | .name { 83 | border: 1px solid red; 84 | margin: 5px; 85 | } 86 | 87 | 88 | ``` 89 | 90 | Then import your component. 91 | 92 | ```jsx 93 | import React, {Component} from 'react'; 94 | import {View} from 'react-native'; 95 | import styles, {StyledText} from './dist/component.css'; 96 | 97 | export default class App extends Component { 98 | 99 | return 100 | Your Text Here 101 | //your stuff here. 102 | 103 | 104 | } 105 | 106 | ``` 107 | 108 | ###Transition Example 109 | 110 | Suppose you have transition.css. 111 | 112 | ```css 113 | /* @namespace imports a component to extend */ 114 | @namespace Text "react-native.Text"; 115 | 116 | /*This will export a component named FadeIn, that extends Text*/ 117 | Text|FadeIn { 118 | height: 20px; 119 | width: 100px; 120 | 121 | border-radius: 10px; 122 | text-align: center; 123 | opacity: .5; 124 | transform: translateX(0); 125 | background-color: darkgreen; 126 | color: darkorange; 127 | transition: transform 1s ease-in, opacity 1s ease-in, color 1s ease-in, background-color 1s ease-in; 128 | } 129 | 130 | /*This adds a psuedo selector of checked*/ 131 | Text|FadeIn:checked { 132 | opacity: 1; 133 | color: darkgreen; 134 | background-color: darkorange; 135 | transform: translateX(100px); 136 | transition: transform 1s ease-in, opacity 1s ease-in, color 1s ease-in, background-color 1s ease-in; 137 | 138 | } 139 | 140 | ``` 141 | 142 | Usage of transition.css 143 | 144 | ```js 145 | import {FadeIn} from './transition'; 146 | import {Component} from 'react'; 147 | 148 | export default class Test extends Component { 149 | 150 | render(){ 151 | return 152 | This Fades In/Out 153 | 154 | } 155 | } 156 | 157 | ``` 158 | 159 | 160 | ## ClassNames 161 | So you may want to add classNames to a component to change its styling. 162 | So in you css you might have 163 | ```css 164 | 165 | @namespace Native "react-native.View"; 166 | 167 | Native|ExampleView { 168 | border: 2px solid red; 169 | height: 100px; 170 | width: 200px; 171 | } 172 | 173 | Native|ExampleView.green { 174 | margin: 10px; 175 | } 176 | 177 | .green { 178 | border-color: green; 179 | } 180 | 181 | ``` 182 | 183 | In your JS(X) you can 184 | ```jsx 185 | 186 | import {ExampleView} from './example.css'; 187 | 188 | export default class Example extends Component { 189 | 190 | render(){ 191 | return ... 192 | } 193 | } 194 | 195 | 196 | ``` 197 | 198 | ## Animations 199 | 200 | ```css 201 | @namespace Text "react-native.Text"; 202 | 203 | Text|Bounce { 204 | height: 20px; 205 | width: 100px; 206 | background-color: yellow; 207 | border-radius: 10px; 208 | text-align: center; 209 | border:1px solid red; 210 | } 211 | 212 | Text|Bounce:checked { 213 | animation-name: bounce; 214 | animation-duration: 1s; 215 | animation-direction: alternate; 216 | animation-timing-function: linear; 217 | animation-iteration-count: 1; 218 | } 219 | 220 | @keyframes bounce { 221 | from { 222 | transform: translateY(0) 223 | } 224 | 20% { 225 | transform: translateY(0) 226 | } 227 | 40% { 228 | transform: translateY(-30) 229 | } 230 | 43% { 231 | transform: translateY(-30) 232 | } 233 | 53% { 234 | transform: translateY(0) 235 | } 236 | 70% { 237 | transform: translateY(-15) 238 | } 239 | 80% { 240 | transform: translateY(0) 241 | } 242 | 90% { 243 | transform: translateY(-4) 244 | } 245 | to { 246 | transform: translateY(0) 247 | } 248 | } 249 | 250 | ``` 251 | 252 | 253 | -------------------------------------------------------------------------------- /src/animation/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var max = Function.apply.bind(Math.max, Math); 4 | var list = require('postcss').list; 5 | var api = { 6 | EMPTY_OBJ: Object.freeze({}), 7 | selectors: ['enter', 'appear', 'leave'], 8 | classToRegexp(classes){ 9 | return new RegExp('(\\.' + (classes.join('|\\.')) + ')'); 10 | }, 11 | /** 12 | * Default locals update function. 13 | * @param locals Object - The object to change. 14 | * @param prop - {string} - The name of the prop (usually the className). 15 | * @param propname -{string} - The property that is being insepected. 16 | * @param declartion -{*} - The css properyt declaration value. 17 | * @returns {*} 18 | */ 19 | localUpdate(locals, prop, propname, declartion) { 20 | return locals['@' + api.camel(prop + '-' + propname)] = declartion; 21 | }, 22 | /** 23 | * Basically Math.max, but it accepts an array of numbers. 24 | */ 25 | max: max, 26 | /** 27 | * Given array of values, it finds the position 28 | * at the array repeating. 29 | * 30 | * @param pos 31 | * @param array 32 | * @returns {*} 33 | */ 34 | 35 | addAtEnd(arr, value) { 36 | var idx = arr.indexOf(value); 37 | if (idx !== -1) { 38 | arr.splice(idx, 1); 39 | } 40 | arr.push(value); 41 | return arr; 42 | }, 43 | /** 44 | * tries to convert from s/ms to seconds. 45 | * If the seconds representation is shorter than 46 | * return it. Otherwise return milliseconds or 47 | * the original value whichever is shorter. 48 | * 49 | * It will also drop insignificant zeros. 50 | * 51 | * @param oval 52 | * @returns {*} 53 | */ 54 | toNiceTimeUnits(oval){ 55 | var val = api.toMillis(oval); 56 | if (val === 0) { 57 | return val; 58 | } 59 | var sval = ('' + (val / 1000)).replace(/^0{1,}/, '') + 's'; 60 | oval = '' + oval; 61 | val = '' + val; 62 | if (sval.length < oval.length){ 63 | return val.length <= sval.length ? val : sval; 64 | }else{ 65 | return val.length < oval.length? val : oval; 66 | } 67 | 68 | }, 69 | replaceMillis(to, value) { 70 | if (typeof value === 'number') { 71 | return api.replace(to, [value]); 72 | } 73 | return api.replace(to, list.comma(value).map(api.toMillis)); 74 | }, 75 | retFirst(val) { 76 | return val; 77 | }, 78 | 79 | defReplace(arr, fn) 80 | { 81 | fn = fn || api.retFirst; 82 | return function (v) { 83 | if (arguments.length === 0) { 84 | return arr.concat(); 85 | } 86 | api.replace(arr, list.comma(v).map(fn || retFirst)); 87 | return this; 88 | } 89 | }, 90 | trueFunc() 91 | { 92 | return true; 93 | } 94 | , 95 | /** 96 | * replaces the first array with the contents of the second; 97 | * @param arr 98 | * @param arr2 99 | * @returns arr 100 | */ 101 | replace(arr, arr2) 102 | { 103 | arr.length = 0; 104 | arr.splice.apply(arr, [0, 0].concat(arr2)); 105 | return arr; 106 | } 107 | , 108 | /** 109 | * 110 | * Pass a string it will return a function that compares the string to the first argument. 111 | * Pass a function and it will pass the value of the into function and return what the function returns. 112 | * Pass a regex and it will call test on the regex against the value. 113 | * Pass an array and it will loop through the array of string/function/regexp and return true on the first true. 114 | * Pass an object and it will match the val to the key and resolve the value against the filter rules above. 115 | * 116 | * @param func [string,regexp,object,function, array[string,regex,object, function]] 117 | * @returns {boolean} 118 | */ 119 | filter(func) 120 | { 121 | if (func == null) { 122 | return api.trueFunc; 123 | } 124 | if (typeof func == 'function') { 125 | return func; 126 | } 127 | if (typeof func == 'string') { 128 | return function filter$string(val) { 129 | return val === func; 130 | } 131 | } 132 | if (func instanceof RegExp) { 133 | return function filter$regex(val) { 134 | return func.test(val); 135 | } 136 | } 137 | if (Array.isArray(func)) { 138 | func = func.map(api.filter); 139 | return function filter$array(val) { 140 | for (var i = 0, al = func.length; i < al; i++) { 141 | if (func[i].apply(null, arguments)) { 142 | return true; 143 | } 144 | } 145 | return false; 146 | }; 147 | } 148 | if (typeof func === 'object') { 149 | var keys = Object.keys(func), l = keys.length, values = keys.map(function (key) { 150 | return api.filter(func[key]); 151 | }); 152 | return function filter$object(val) { 153 | for (var i = 0; i < l; i++) { 154 | if (keys[i] === val && values[i].apply(null, arguments)) { 155 | return true; 156 | } 157 | } 158 | return false; 159 | } 160 | } 161 | throw new Error(`unknown filter type ${func}`); 162 | } 163 | , 164 | camel(prop) 165 | { 166 | return prop == null ? prop : prop.replace(/-([\w])/g, function (match, r) { 167 | return r.toUpperCase(); 168 | }); 169 | } 170 | , 171 | 172 | isTiming(v) 173 | { 174 | return v == null ? false : /^((\d*?(?:(\.\d+?))?)(?:(ms|s))?)$/.test(v); 175 | } 176 | , 177 | repeatAt(pos, array, def) 178 | { 179 | const len = array.length; 180 | return len === 0 ? def : array[pos % len]; 181 | } 182 | , 183 | 184 | /** 185 | * Takes a duration/delay timing string and returns the value in milliseconds. 186 | * @param {string} val - Duration timing string, EX. 100, 100s, 1000ms, 1.2s 187 | * @returns {number} 188 | */ 189 | toMillis(val) 190 | { 191 | if (val == null) { 192 | return 0; 193 | } 194 | if (typeof val === 'number') { 195 | return val; 196 | } 197 | var parts = /^(\d*(?:\.\d{1,})?)(ms|s)?$/.exec('' + val); 198 | 199 | if (!parts) return 0; 200 | if (!parts[1]) return 0; 201 | var v = parts[1]; 202 | switch (parts[2]) { 203 | case 's': 204 | return v * 1000; 205 | default: 206 | return v * 1; 207 | } 208 | } 209 | 210 | }; 211 | 212 | module.exports = api; -------------------------------------------------------------------------------- /src/declarations/transition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { 4 | repeatAt, 5 | toNiceTimeUnits, 6 | replace, 7 | replaceMillis, 8 | isTiming, 9 | addAtEnd, 10 | toMillis, 11 | filter 12 | } from '../animation/utils'; 13 | 14 | var list = require('postcss').list; 15 | 16 | const PROPS = ['delay', 'duration', 'property', 'timing-function']; 17 | const te = new RegExp(`^(?:transition)?(?:-(${PROPS.join('|')}))?\s*:\s*(.+?)$|^(.*)$`, 'i'); 18 | /** 19 | * An object|object factory that returns 20 | * a transition object. Meant to be called in the 21 | * order that the properties in css are exposed to 22 | * accurately calculate timeouts and expressions. 23 | * 24 | * @param trans 25 | * @returns {Transition} 26 | * @constructor 27 | */ 28 | function Transition(...trans) { 29 | if (!(this instanceof Transition)) { 30 | return new Transition(...trans); 31 | } 32 | 33 | //holds the settings. 34 | let property = []; 35 | let duration = []; 36 | let delay = []; 37 | let timingFunction = []; 38 | let transitionTo; 39 | /** 40 | * Returns an array of transition css objects. 41 | * @returns {Array} 42 | */ 43 | this.toJSON = ()=> { 44 | return property.map((transitionProperty, i)=> { 45 | var obj = { 46 | transitionProperty: transitionProperty, 47 | transitionDuration: toNiceTimeUnits(repeatAt(i, duration, 0)) 48 | }, tf, d; 49 | 50 | if ((tf = repeatAt(i, timingFunction))) { 51 | obj.transitionTimingFunction = tf; 52 | } 53 | if ((d = repeatAt(i, delay))) { 54 | obj.transitionDelay = toNiceTimeUnits(d); 55 | } 56 | return obj; 57 | }); 58 | }; 59 | this.toCSS = (f)=> { 60 | return property.filter(filter(f)).map(function (property, i) { 61 | return { 62 | property, 63 | duration: repeatAt(i, duration, 0), 64 | 'timing-function': repeatAt(i, timingFunction), 65 | delay: repeatAt(i, delay) 66 | }; 67 | }); 68 | }; 69 | this.description = (f)=> { 70 | return property.filter(filter(f)).map(function (prop, i) { 71 | 72 | var def = [ 73 | prop, 74 | toNiceTimeUnits(repeatAt(i, duration, 0)) 75 | ]; 76 | 77 | var tf = repeatAt(i, timingFunction); 78 | 79 | if (tf) { 80 | def.push(tf); 81 | } 82 | 83 | var d = repeatAt(i, delay); 84 | if (d != null && d != 0) { 85 | def.push(toNiceTimeUnits(d)); 86 | } 87 | return def.join(' '); 88 | }); 89 | }; 90 | 91 | this.timeout = (f) => { 92 | var iter = f ? property.filter(filter(f)) : delay.length > duration.length ? delay : duration; 93 | return iter.length === 0 ? 0 : max(iter.map(function timeout$map(prop, i) { 94 | return repeatAt(i, duration, 0) + repeatAt(i, delay, 0); 95 | })) 96 | }; 97 | 98 | this.toString = ()=> { 99 | return 'transition: ' + (this.description().join(', ')); 100 | }; 101 | /** 102 | * { 103 | * type:'margin 104 | * 105 | * } 106 | * @param toProps 107 | * @returns {*} 108 | */ 109 | this.transitionTo = (toProps)=> { 110 | return Object.keys(toProps).reduce((ret, tprop)=> { 111 | 112 | }); 113 | }; 114 | 115 | this.transition = (_property, value) => { 116 | if (!(_property || value)) { 117 | return this; 118 | } 119 | if (_property) { 120 | this[_property](value); 121 | return this; 122 | } 123 | 124 | let transition; 125 | 126 | if (value) { 127 | 128 | const [,tProp, tPropValue, tValue ] = te.exec(value); 129 | if (tProp) { 130 | this[tProp](tPropValue); 131 | return this; 132 | } else { 133 | transition = tPropValue || tValue; 134 | } 135 | } 136 | //is this correct, chrome resets on transition delcaration. 137 | //reset 138 | duration.length = delay.length = timingFunction.length = property.length = 0; 139 | /* 140 | transition: opacity 1s ease 2s, height 141 | ---- 142 | transition-delay 2s, 0s 143 | transition-duration 1s, 0s 144 | transition-property opacity, height 145 | transition-timing-function ease, ease 146 | */ 147 | list.comma(transition).forEach(function (c) { 148 | var parts = list.space(c); 149 | if (parts.length === 0) return; 150 | addAtEnd(property, parts.shift()); 151 | 152 | if (parts[0]) { 153 | duration.push(toMillis(parts.shift())); 154 | if (!parts[0]) { 155 | delay.push(0); 156 | timingFunction.push(Transition.defaultTimingFunction) 157 | } 158 | } else { 159 | duration.push(0); 160 | delay.push(0); 161 | timingFunction.push(Transition.defaultTimingFunction) 162 | } 163 | if (isTiming(parts[0])) { 164 | timingFunction.push(Transition.defaultTimingFunction); 165 | delay.push(toMillis(parts.shift())); 166 | } else if (parts[0]) { 167 | timingFunction.push(parts.shift()); 168 | delay.push(parts[0] ? toMillis(parts.shift()) : 0); 169 | } 170 | 171 | }); 172 | return this; 173 | }; 174 | 175 | this.property = (props) => { 176 | if (arguments.length !== 0) { 177 | list.comma(props).forEach(addAtEnd.bind(utils, property)); 178 | return this; 179 | } 180 | return property.concat(); 181 | }; 182 | 183 | this['timing-function'] = this.timingFunction = (tfs)=> { 184 | if (arguments.length !== 0) { 185 | replace(timingFunction, list.comma(tfs)); 186 | return this; 187 | } 188 | return timingFunction.concat(); 189 | }; 190 | this.delay = (delays)=> { 191 | if (arguments.length !== 0) { 192 | replaceMillis(delay, delays); 193 | return this; 194 | } 195 | return delay.concat(); 196 | }; 197 | this.duration = (durations)=> { 198 | if (arguments.length !== 0) { 199 | replaceMillis(duration, durations); 200 | return this; 201 | } 202 | return duration.concat(); 203 | }; 204 | 205 | this.fromJSON = Transition.fromJSON; 206 | 207 | if (trans.length) { 208 | this.transition(...trans); 209 | } 210 | } 211 | Transition.defaultTimingFunction = 'ease'; 212 | 213 | Transition.fromJSON = function (obj) { 214 | if (obj == null) return; 215 | 216 | obj = Array.isArray(obj) ? obj : [obj]; 217 | 218 | var transition = this instanceof Transition ? this : new Transition(); 219 | 220 | obj.forEach(function (o) { 221 | Object.keys(o).forEach(function (key) { 222 | var f = key.replace(/^transition([A-Z])/, function (match, letter) { 223 | return letter.toLowerCase(); 224 | }); 225 | transition[f](o[key]); 226 | }); 227 | }); 228 | return transition; 229 | }; 230 | 231 | export const transition = Transition; 232 | 233 | export default function (postfix, value, str, container, config) { 234 | config.prefix = 'transition'; 235 | const ret = Transition(postfix, value).toCSS(); 236 | return ret; 237 | } -------------------------------------------------------------------------------- /src/animation/animation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var list = require('postcss').list; 4 | var pu = require('./utils'); 5 | var defReplace = pu.defReplace; 6 | 7 | /** 8 | * An object|object factory that returns 9 | * a transition object. Meant to be called in the 10 | * order that the properties in css are exposed to 11 | * accurately calculate timeouts and expressions. 12 | * 13 | * @param trans 14 | * @returns {Animation} 15 | * @constructor 16 | */ 17 | 18 | 19 | function toIntOrInfinite(v) { 20 | if (v == 'infinite') { 21 | return v; 22 | } 23 | return parseFloat(v); 24 | } 25 | function isIteration(v) { 26 | if (v == 'infinite') { 27 | return true; 28 | } 29 | return !isNaN(parseFloat(v)); 30 | } 31 | 32 | function Animation(anim) { 33 | if (!(this instanceof Animation)) { 34 | return new Animation(anim); 35 | } 36 | //holds the settings. 37 | var property = []; 38 | var name = []; 39 | var duration = []; 40 | var delay = []; 41 | var timingFunction = []; 42 | var iterationCount = []; 43 | var fillMode = []; 44 | var playState = []; 45 | var direction = []; 46 | 47 | this.description = function description(filter) { 48 | return name.filter(pu.filter(filter)).map(function (prop, i) { 49 | // duration | timing-function | delay | iteration-count | direction | fill-mode | play-state | name 50 | return [ 51 | pu.toNiceTimeUnits(pu.repeatAt(i, duration, Animation.defaultDuration)), 52 | pu.repeatAt(i, timingFunction, Animation.defaultTimingFunction), 53 | pu.toNiceTimeUnits(pu.repeatAt(i, delay, Animation.defaultDelay)), 54 | pu.repeatAt(i, iterationCount, Animation.defaultIterationCount), 55 | pu.repeatAt(i, direction, Animation.defaultDirection), 56 | pu.repeatAt(i, fillMode, Animation.defaultFillMode), 57 | pu.repeatAt(i, playState, Animation.defaultPlayState), 58 | prop 59 | ].join(' '); 60 | }); 61 | }; 62 | this.toJSON = ()=> { 63 | return name.map((prop, i)=> { 64 | const p = {}; 65 | p.duration = pu.repeatAt(i, duration, Animation.defaultDuration); 66 | p.timingFunction = pu.repeatAt(i, timingFunction, Animation.defaultTimingFunction); 67 | p.delay = pu.repeatAt(i, delay, Animation.defaultDelay); 68 | p.iterationCount = pu.repeatAt(i, iterationCount, Animation.defaultIterationCount); 69 | p.direction = pu.repeatAt(i, direction, Animation.defaultDirection); 70 | p.fillMode = pu.repeatAt(i, fillMode, Animation.defaultFillMode); 71 | p.playState = pu.repeatAt(i, playState, Animation.defaultPlayState); 72 | p.name = prop; 73 | return p; 74 | }); 75 | }; 76 | 77 | this.timeout = function _timeout(filter) { 78 | var iter = filter ? name.filter(pu.filter(filter)) : delay.length > duration.length ? delay : duration; 79 | 80 | return iter.length === 0 ? 0 : pu.max(iter.map(function timeout$map(prop, i) { 81 | var ic = pu.repeatAt(i, iterationCount, Animation.defaultIterationCount); 82 | if (ic === 'infinite') { 83 | return 0; 84 | } 85 | var dur = pu.repeatAt(i, duration, Animation.defaultDuration); 86 | var del = pu.repeatAt(i, delay, Animation.defaultDelay); 87 | return (dur + del ) * ic; 88 | })) 89 | }; 90 | 91 | this.toString = function () { 92 | return `animation: ${this.description().join(', ')}`; 93 | }; 94 | 95 | this.animation = function _addAnimation(anim) { 96 | //is this correct, chrome resets on transition delcaration. 97 | //reset 98 | duration.length = delay.length = timingFunction.length = name.length = iterationCount.length = direction.length = fillMode.length = playState.length = 0; 99 | /* 100 | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state | name 101 | animation: 3s ease-in | 1s | 2 | reverse | both | paused | slidein; 102 | animation: 3s linear 1s slidein; 103 | animation: 3s slidein; 104 | 105 | ---- 106 | * animation-duration: as specified 107 | * animation-timing-function: as specified 108 | * animation-delay: as specified 109 | * animation-direction: as specified 110 | * animation-iteration-count: as specified 111 | * animation-fill-mode: as specified 112 | * animation-play-state: as specified 113 | * animation-name: as specified 114 | */ 115 | list.comma(anim).forEach(function (c) { 116 | var parts = list.space(c); 117 | var _duration, _timingFunction, _delay, _iterationCount, _direction, _fillMode, _playState, _name; 118 | 119 | if (parts[0]) { 120 | _duration = pu.toMillis(parts.shift()); 121 | } 122 | _name = parts.pop(); 123 | if (parts[0]) { 124 | _timingFunction = parts.shift(); 125 | } else { 126 | _timingFunction = Animation.defaultTimingFunction; 127 | } 128 | 129 | if (parts[0] && pu.isTiming(parts[0])) { 130 | _delay = pu.toMillis(parts.shift()); 131 | } else { 132 | _delay = 0; 133 | } 134 | 135 | if (parts[0] && isIteration(parts[0])) { 136 | _iterationCount = toIntOrInfinite(parts.shift()); 137 | } else { 138 | _iterationCount = 1; 139 | } 140 | if (parts[0]) { 141 | _direction = parts.shift(); 142 | } else { 143 | _direction = Animation.defaultDirection; 144 | } 145 | if (parts[0]) { 146 | _fillMode = parts.shift(); 147 | } else { 148 | _fillMode = Animation.defaultFillMode; 149 | } 150 | 151 | if (parts[0]) { 152 | _playState = parts.shift(); 153 | } else { 154 | _playState = Animation.defaultPlayState; 155 | } 156 | 157 | duration.push(_duration); 158 | timingFunction.push(_timingFunction); 159 | delay.push(_delay); 160 | direction.push(_direction); 161 | iterationCount.push(_iterationCount); 162 | fillMode.push(_fillMode); 163 | playState.push(_playState); 164 | name.push(_name); 165 | 166 | }); 167 | return this; 168 | }; 169 | 170 | this['animation-name'] = this.name = defReplace(name); 171 | this['animation-duration'] = this.duration = defReplace(duration, pu.toMillis) 172 | this['animation-timing-function'] = this.timingFunction = defReplace(timingFunction); 173 | this['animation-delay'] = this.delay = defReplace(delay, pu.toMillis); 174 | this['animation-direction'] = this.direction = defReplace(direction); 175 | this['animation-iteration-count'] = this.iterationCount = defReplace(iterationCount, toIntOrInfinite); 176 | this['animation-fill-mode'] = this.fillMode = defReplace(fillMode); 177 | this['animation-play-state'] = this.playState = defReplace(playState); 178 | 179 | if (anim) { 180 | this.animation(anim); 181 | } 182 | } 183 | Animation.defaultDirection = 'normal'; 184 | Animation.defaultFillMode = 'none'; 185 | Animation.defaultPlayState = 'running'; 186 | Animation.defaultTimingFunction = 'ease'; 187 | Animation.defaultDelay = 0; 188 | Animation.defaultDuration = 0; 189 | Animation.defaultIterationCount = 1; 190 | Animation.fromJSON = (json)=> { 191 | return Object.keys(json).reduce((ret, key)=> { 192 | const value = json[key]; 193 | ret.name(key); 194 | Object.keys(value).reduce((r, k)=> { 195 | ret[k](value[k]); 196 | }, ret); 197 | return ret; 198 | }, new Animation()); 199 | }; 200 | module.exports = Animation; -------------------------------------------------------------------------------- /test/postcss-react-native-test.js: -------------------------------------------------------------------------------- 1 | import {testString, testTransform, test, json} from './support'; 2 | import {expect} from 'chai'; 3 | 4 | describe('postcss-react-native', function () { 5 | /* 6 | transition: margin-left 4s; 7 | transition: margin-left 4s 1s; 8 | transition: margin-left 4s ease-in-out 1s; 9 | transition: margin-left 4s, color 1s; 10 | transition: all 0.5s ease-out; 11 | 12 | */ 13 | it('should parse simple-component', ()=> { 14 | return test('simple-component', (f)=> { 15 | const css = f({height: 1024, width: 768, scale: 1}); 16 | expect(css.default).to.exist; 17 | expect(css.StyledText).to.exist; 18 | return css; 19 | }); 20 | }); 21 | 22 | it('should parse transition', function () { 23 | /** 24 | * TODO - Make transition conform to dela, duration, property,timing-function 25 | * Initial value as each of the properties of the shorthand: 26 | transition-delay: 0s 27 | transition-duration: 0s 28 | transition-property: all 29 | transition-timing-function: ease 30 | */ 31 | return testString('.t1 { transition: margin-left 4s, border 1s ease-in, opacity 30 linear 1s}', { 32 | toJSON(css, input){ 33 | expect(css).to.eql({ 34 | "rules": [ 35 | { 36 | "css": { 37 | "t1": [ 38 | { 39 | "prefix": "transition", 40 | "type": "transition", 41 | "vendor": false, 42 | "values": [ 43 | { 44 | "property": "margin-left", 45 | "duration": 4000, 46 | "timing-function": "ease", 47 | "delay": 0 48 | }, 49 | { 50 | "property": "border", 51 | "duration": 1000, 52 | "timing-function": "ease-in", 53 | "delay": 0 54 | }, 55 | { 56 | "property": "opacity", 57 | "duration": 30, 58 | "timing-function": "linear", 59 | "delay": 1000 60 | } 61 | ] 62 | } 63 | ] 64 | }, 65 | "tags": {} 66 | } 67 | ], 68 | "namespaces": {}, 69 | "imports": [], 70 | "exports": {} 71 | }); 72 | }, 73 | toStyleSheet(json, input){ 74 | 75 | } 76 | }) 77 | }); 78 | 79 | it('media query stuff for ios', function () { 80 | return test('media', (f, source)=> { 81 | const css = f({height: 1024, width: 768, scale: 1}, 'ios'); 82 | 83 | expect(css.StyleSheet).to.eql({ 84 | "stuff": { 85 | "borderBottomWidth": 6.666666666666666, 86 | "borderTopWidth": 3, 87 | "marginTop": 20, 88 | "marginRight": 10, 89 | "marginBottom": 5, 90 | "marginLeft": 2, 91 | "color": "rgba(255, 255, 0, 1)", 92 | "borderWidth": 5, 93 | "borderTopColor": "green", 94 | "borderStyle": "solid" 95 | }, 96 | "other": { 97 | "opacity": 0.5 98 | } 99 | }); 100 | }, v=>v, {}); 101 | }); 102 | it('media should parse import', function () { 103 | return test('import', v=>v=> { 104 | return v; 105 | }, (f, source)=> { 106 | 107 | expect(f).to.exist; 108 | return f; 109 | 110 | }); 111 | }); 112 | it('media query stuff for android', function () { 113 | return test('media', (f, source)=> { 114 | const css = f({height: 1024, width: 768, scale: 1}, 'android'); 115 | expect(css.default).to.eql({ 116 | "stuff": { 117 | "borderBottomWidth": 6.666666666666666, 118 | "borderTopWidth": 3, 119 | "marginTop": 20, 120 | "marginRight": 10, 121 | "marginBottom": 5, 122 | "marginLeft": 2, 123 | "color": "purble", 124 | "borderWidth": 5, 125 | "borderTopColor": "green", 126 | "borderStyle": "solid" 127 | 128 | }, 129 | "other": { 130 | "opacity": 0.5 131 | } 132 | }); 133 | }); 134 | }); 135 | it('should parse font', ()=> { 136 | return test('font', (f, source)=> { 137 | const css = f({height: 1024, width: 768, scale: 1}, 'android'); 138 | expect(css.default).to.eql({ 139 | "font1": { 140 | "fontSize": 2, 141 | "fontFamily": "Open Sans" 142 | }, 143 | "font2": { 144 | "fontSize": 192 145 | } 146 | }); 147 | }); 148 | }); 149 | it('should parse welcome', function () { 150 | return test('welcome', (f, source)=> { 151 | const css = f({height: 1024, width: 768, scale: 1}, 'android'); 152 | 153 | expect(css.default).to.eql({ 154 | "container": { 155 | "flex": 1, 156 | "justifyContent": "center", 157 | "alignItems": "center", 158 | "backgroundColor": "rgba(192, 192, 192, 1)" 159 | }, 160 | "welcome": { 161 | "fontSize": 20, 162 | "fontFamily": "Thonburi", 163 | "textAlign": "center", 164 | "marginTop": 0, 165 | "marginRight": 0, 166 | "marginBottom": 0, 167 | "marginLeft": 0 168 | }, 169 | "instructions": { 170 | "textAlign": "center", 171 | "color": "rgba(51, 51, 51, 1)", 172 | "marginBottom": 5, 173 | "borderWidth": 1, 174 | "borderStyle": "solid", 175 | "borderColor": "green" 176 | } 177 | }); 178 | }); 179 | }); 180 | it('should compile clazz', ()=> { 181 | return test('clazz', (f)=> { 182 | const css = f({height: 1024, width: 768, scale: 1}); 183 | expect(css.default).to.exist; 184 | expect(css.MyView).to.exist; 185 | return css; 186 | }); 187 | }); 188 | 189 | it('should parse clazz', function () { 190 | return test('clazz', v=>v, (css, source)=> { 191 | expect(css).to.eql(json('clazz')); 192 | return css; 193 | }); 194 | }); 195 | it('should parse component', function (done) { 196 | return testTransform('component', (f, source)=> { 197 | return f({height: 1024, width: 768, scale: 1}).then((ret, css, src)=> { 198 | expect(ret).to.exist; 199 | done(); 200 | }, done); 201 | }); 202 | }); 203 | 204 | it('should parse clazz-pseudo', function (done) { 205 | return testTransform('clazz-pseudo', (f, source)=> { 206 | return f({height: 1024, width: 768, scale: 1}).then((ret, css, src)=> { 207 | expect(ret).to.exist; 208 | done(); 209 | }, done); 210 | }); 211 | }); 212 | it('should parse real-import', function (done) { 213 | return testTransform('real-import', (f)=> { 214 | return f({height: 1024, width: 768, scale: 1}).then((ret, css, src)=> { 215 | expect(ret).to.exist; 216 | done(); 217 | }, done); 218 | }); 219 | }); 220 | 221 | it('should parse :export selector', function () { 222 | return test('exports', (f, source) => { 223 | const css = f({height: 1024, width: 768, scale: 1}, 'android'); 224 | expect(css.color).to.eql('#FF0000'); 225 | expect(css.default).to.eql({ 226 | "other": { 227 | "opacity": 0.5 228 | } 229 | }); 230 | }); 231 | }); 232 | }); -------------------------------------------------------------------------------- /src/source.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import unit from './unit'; 3 | import camel from './util/camel'; 4 | import isObjectLike from './util/isObjectLike'; 5 | 6 | const quote = JSON.stringify.bind(JSON); 7 | 8 | const vendorIf = (vendor, str)=> { 9 | if (vendor) { 10 | return `if (vendor === 'native' || vendor === ${JSON.stringify(vendor)}){\n${str}\n}`; 11 | } 12 | return str; 13 | }; 14 | 15 | const rhsunit = (u)=> { 16 | const un = unit(u); 17 | let ret; 18 | if (un && typeof un.value === 'number') { 19 | if (un.unit == 'deg') { 20 | ret = `'${un.value} deg'` 21 | } else { 22 | ret = un.unit ? `(${un.value} * units["${un.unit}"])` : un.value; 23 | } 24 | } else { 25 | ret = quote(u); 26 | } 27 | 28 | return ret; 29 | }; 30 | 31 | 32 | const rhs = (val)=> { 33 | if (typeof val === 'number' || typeof val == 'boolean') { 34 | return val; 35 | } else { 36 | return rhsunit(val); 37 | } 38 | }; 39 | 40 | const writeNakedObj = (obj, unit = rhs)=> { 41 | return `{${Object.keys(obj).map(v=>`${quote(v)}:${unit(obj[v])}`).join(',\n')}}`; 42 | }; 43 | const writeArray = (root, type, values)=> { 44 | const rt = `${root}.${type}`; 45 | return ` 46 | if (!${rt}) ${rt} = []; 47 | ${values.map((v)=>`${rt}.push(${writeNakedObj(v)})`).join(';\n')} 48 | ` 49 | }; 50 | 51 | const pdecl = (root, type, values) => { 52 | if (!isObjectLike(values)) { 53 | const vvv = rhs(values); 54 | return `${root}.${camel(type)} = ${vvv};`; 55 | } 56 | if (type === 'transform' && Array.isArray(values)) { 57 | return writeArray(root, type, values); 58 | } 59 | if (type === 'transition' && Array.isArray(values)) { 60 | return values.map((v)=> { 61 | const prop = quote(camel(v.property)); 62 | const tr = `${root}[${prop}]`; 63 | return ` 64 | if (!${tr}) ${tr} = {}; 65 | ${tr}.delay = ${v.delay}; 66 | ${tr}.duration = ${v.duration}; 67 | ${tr}.timingFunction = ${quote(v['timing-function'])}; 68 | ${tr}.property = ${prop}; 69 | ` 70 | 71 | }).join('\n'); 72 | 73 | } 74 | const dstr = Object.keys(values).reduce((str, key)=> { 75 | const v = values[key]; 76 | if (!isObjectLike(v)) { 77 | const ct = camel(type, key); 78 | return `${str}\n ${root}.${ct} = ${rhs(v)};`; 79 | } else if (Array.isArray(v)) { 80 | return `${str}\n ${root}.${camel(type, key)} = ${v.map(rhs).join(',')};`; 81 | } else if (typeof v === 'object') { 82 | return Object.keys(v).reduce((ret, kv)=> { 83 | if (type === 'border' && kv === 'style') { 84 | //does not support different styles on different sides. 85 | return `${ret}\n ${root}.${camel(type, kv)} = ${rhs(v[kv])};`; 86 | } 87 | return `${ret}\n ${root}.${camel(type, key, kv)} = ${rhs(v[kv])};`; 88 | }, str); 89 | } else { 90 | console.log('wtf?', v); 91 | } 92 | return str; 93 | }, ''); 94 | return dstr; 95 | }; 96 | 97 | const writeRule = ({css, expressions = []}) => { 98 | 99 | const src = Object.keys(css).map((key)=> { 100 | 101 | const decls = css[key]; 102 | const qkey = JSON.stringify(key); 103 | const bases = {}; 104 | const strDecls = decls.map((decl)=> { 105 | const type = decl.prefix || (decl.type == 'animation' || decl.type == 'transition') ? decl.type : 'style'; 106 | const cbase = `current.__${type}`; 107 | bases[cbase] = true; 108 | return vendorIf(decl.vendor, `${pdecl(cbase, decl.type, decl.values)}`); 109 | }).join('\n'); 110 | 111 | return ` 112 | internals[${qkey}] = function (parent, config){ 113 | const current = parent && parent(config) || {}; 114 | ${Object.keys(bases).map(base=>`if (!${base}) ${base}={}`).join(';\n')}; 115 | ${writeVars()} 116 | ${writeSheet(strDecls, expressions)} 117 | return current; 118 | }.bind(null, internals[${qkey}])` 119 | }).filter(v=>!/^\s*$/.test(v)).join(';\n'); 120 | 121 | return src; 122 | }; 123 | 124 | 125 | const writeExpression = (expr)=> { 126 | return `(FEATURES['${expr.modifier ? expr.modifier + '-' : ''}${expr.feature}'] && FEATURES['${expr.modifier ? expr.modifier + '-' : ''}${expr.feature}']( ${rhsunit(expr.value)}, config ))\n`; 127 | }; 128 | 129 | const writeExpressions = ({expressions, inverse})=> { 130 | const expr = expressions.map(writeExpression).join(' && '); 131 | 132 | return inverse ? `!(${expr})` : expr; 133 | }; 134 | 135 | const writeSheet = (cssStr, expressions = []) => { 136 | if (expressions.length) { 137 | return ` 138 | if (${expressions.map(writeExpressions).join(' || ')}){\n${cssStr}\n} 139 | `; 140 | } else { 141 | return ` 142 | ${cssStr} 143 | `; 144 | } 145 | }; 146 | 147 | export const writeVars = ()=> { 148 | return `const px = 1, 149 | vendor = config.vendor, 150 | inch = 96, 151 | vh = config.height / 100, 152 | vw = config.width / 100, 153 | percentageW = config.clientWidth || config.width, 154 | percentageH = config.clientHeight|| 0, 155 | units = { 156 | px : px, 157 | vh : vh, 158 | vw : vw, 159 | '%':!percentageW ? 0 : percentageW / 100, 160 | 'in':inch, 161 | pt:(inch/72), 162 | em:1, 163 | pc:12 * (inch/72), 164 | vmin:Math.min(vw, vh), 165 | vmax: Math.max(vw, vh) 166 | };`; 167 | }; 168 | 169 | export const writeKeyframes = (keyframes)=> { 170 | if (!keyframes) return 'var keyframes = {};'; 171 | 172 | return Object.keys(keyframes).reduce((ret, key)=> { 173 | const rules = [keyframes[key]]; 174 | return `${ret} 175 | keyframes[${JSON.stringify(key)}] = function(){ 176 | const internals = {}; 177 | ${join(rules, writeRule)} 178 | return internals 179 | } 180 | ; 181 | 182 | `; 183 | }, 'var keyframes = {};'); 184 | }; 185 | 186 | const join = (array, fn, char = '\n')=> { 187 | return array ? array.map(fn).join(char) : ''; 188 | }; 189 | 190 | 191 | export const writeImports = (imports = []) => { 192 | const setup = ` 193 | const IMPORTS = []; 194 | let importedInternals = {}; 195 | `; 196 | if (!imports || imports.length == 0) { 197 | return setup; 198 | } 199 | let str = imports.reduce((ret, {url})=> { 200 | return ` 201 | ${ret}importCss(require(${quote(url)}));\n`; 202 | }, ''); 203 | return ` 204 | ${setup} 205 | function importCss(imp){ 206 | IMPORTS.push(imp.default); 207 | importedInternals = imp.internals(importedInternals); 208 | Object.keys(imp).map((key)=>{ 209 | if (key === 'default' || key == 'onChange' || key == 'internal' || key === 'DimensionComponent' || key == 'unpublish') 210 | return; 211 | exports[key] = imp[key]; 212 | }); 213 | } 214 | ${str} 215 | `; 216 | }; 217 | 218 | export const writeExports = (exportsObj = {}) => { 219 | return Object.keys(exportsObj).map( 220 | key => `exports['${key}'] = '${exportsObj[key]}';`).join('\n'); 221 | }; 222 | 223 | export const rulesAreEqual = (ruleA, ruleB) => { 224 | const ruleAProps = Object.keys(ruleA); 225 | const ruleBProps = Object.keys(ruleB); 226 | return ruleAProps.every(key => ruleAProps[key] === ruleBProps[key]) && 227 | ruleAProps.length === ruleBProps.length; 228 | }; 229 | 230 | export const optimizeBorderRule = (rule) => { 231 | const keys = Object.keys(rule.values); 232 | if (rule.values.top && rule.values.bottom && rule.values.left && rule.values.right) { 233 | if (keys.every(side => rulesAreEqual(rule.values[side], rule.values.top))) { 234 | rule.values = rule.values.top; 235 | } 236 | } 237 | return rule; 238 | }; 239 | 240 | const OPTIMIZE_HANDLERS = { 241 | border: optimizeBorderRule 242 | }; 243 | 244 | export const optimizeCssRule = (cssRule) => { 245 | cssRule.forEach((rule, index) => { 246 | if (OPTIMIZE_HANDLERS.hasOwnProperty(rule.type)) { 247 | const optimizeHandler = OPTIMIZE_HANDLERS[rule.type]; 248 | cssRule[index] = optimizeHandler(rule); 249 | } 250 | }); 251 | return cssRule; 252 | }; 253 | 254 | export const optimizeCssRules = (cssRules) => { 255 | Object.keys(cssRules).forEach(rule => { 256 | cssRules[rule] = optimizeCssRule(cssRules[rule]); 257 | }); 258 | return cssRules; 259 | }; 260 | 261 | export const optimizeRules = (rules) => { 262 | rules.forEach(rule => { 263 | rule.css = optimizeCssRules(rule.css); 264 | }); 265 | return rules; 266 | }; 267 | 268 | export const source = (model)=> { 269 | return ` 270 | "use strict"; 271 | var listen = require('postcss-react-native/src/listen').default; 272 | var FEATURES = require('postcss-react-native/src/features').default; 273 | var flatten = require('postcss-react-native/src/flatten').default; 274 | var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); 275 | 276 | const publish = listen(); 277 | const unpublish = listen(); 278 | function makeConfig(){ 279 | var c = Dimensions.get('window'); 280 | c.vendor = Platform.OS; 281 | return c; 282 | } 283 | RCTDeviceEventEmitter.addListener('didUpdateDimensions', function(update) { 284 | publish(makeConfig()); 285 | }); 286 | 287 | Object.defineProperty(exports, "__esModule", { 288 | value: true 289 | }); 290 | 291 | //imports 292 | ${writeImports(model.imports)} 293 | 294 | //exports 295 | ${writeExports(model.exports)} 296 | 297 | //internals 298 | exports.internals = function(_internals){ 299 | const internals = _internals || {}; 300 | //rules 301 | ${join(optimizeRules(model.rules), writeRule)} 302 | 303 | return internals; 304 | 305 | }; 306 | 307 | 308 | 309 | //keyframes 310 | ${writeKeyframes(model.keyframes)}; 311 | 312 | exports.unsubscribe = unpublish.subscribe(publish.property(exports, 'StyleSheet', 313 | (config)=>{ 314 | const internals = exports.internals(importedInternals); 315 | return StyleSheet.create(Object.keys(internals).reduce((ret,internal)=>{ 316 | const value = internals[internal](config).__style; 317 | if (typeof (value) !== 'undefined') { 318 | ret[internal] = value; 319 | } 320 | return ret; 321 | }, {})); 322 | })); 323 | 324 | exports.onChange = publish; 325 | 326 | var ReactNative = require('react-native'); 327 | 328 | var Dimensions = ReactNative.Dimensions; 329 | var StyleSheet = ReactNative.StyleSheet; 330 | var Platform = ReactNative.Platform; 331 | var React = require('react'); 332 | 333 | 334 | //namespace require 335 | ${namespaceToRequire(model.namespaces)} 336 | 337 | //tagsToType 338 | ${tagsToTypes(model)} 339 | 340 | //publish current config; 341 | publish(makeConfig()); 342 | //export default 343 | exports.default = exports.StyleSheet; 344 | 345 | ` 346 | }; 347 | 348 | export const PSEUDO = { 349 | ':checked': { 350 | handler(){ 351 | return ` 352 | handleChecked(){ 353 | this.setState({checked:!(this.state && this.state.checked)}); 354 | }` 355 | }, 356 | prop(){ 357 | return ` 358 | props.onPress = this.handleChecked; 359 | if (this.state && this.state.checked){ 360 | 361 | props.style = handleClass(props.style, '__current:checked'); 362 | } 363 | ` 364 | } 365 | 366 | }, 367 | ':before': {}, 368 | ':after': {} 369 | }; 370 | 371 | 372 | export const namespaceToRequire = (namespaces)=> { 373 | if (!namespaces) return ''; 374 | const keys = Object.keys(namespaces); 375 | if (keys.length === 0) { 376 | return ''; 377 | } 378 | const str = keys.map((key)=> { 379 | const [pkg, dots] = namespaces[key].split('.', 2); 380 | 381 | return `pkgs.${key} = require(${JSON.stringify(pkg)})${dots ? '.' + dots : ''};` 382 | }).join('\n'); 383 | 384 | 385 | return ` 386 | var pkgs = {}; 387 | ${str} 388 | ` 389 | }; 390 | export const buildAnimationSrc = (transition)=> { 391 | if (!transition) return; 392 | return transition.reduce((ret, val)=> { 393 | Object.assign(ret[val.property] || (ret[val.property] = {}), val); 394 | return ret; 395 | }, {}); 396 | }; 397 | /** 398 | * Any rule may have a tag in it. If it does, than 399 | * we need to collapse them. This little dusy does that. 400 | * @param rules 401 | */ 402 | export const tagsToTypes = ({rules = []})=> { 403 | 404 | const ret = {}; 405 | 406 | rules.forEach(({tags, expressions})=> { 407 | Object.keys(tags).forEach((tagKey)=> { 408 | const rtag = ret[tagKey] || (ret[tagKey] = []); 409 | const css = tags[tagKey]; 410 | rtag.push({css, expressions}) 411 | }); 412 | }); 413 | 414 | const str = tagsToType(ret); 415 | return str; 416 | 417 | 418 | }; 419 | 420 | 421 | export const tagsToType = (conf)=> { 422 | if (!conf) return ''; 423 | const keys = Object.keys(conf); 424 | if (keys.length === 0) { 425 | return ''; 426 | } 427 | 428 | return keys.map((key)=> { 429 | const [namespace, name] = key.split('|', 2); 430 | const stringKey = quote(name); 431 | return ` 432 | exports[${stringKey}] = (function(){ 433 | const styler = ()=>{ 434 | const internals = exports.internals(); 435 | ${join(conf[key], writeRule)} 436 | return internals; 437 | } 438 | return require('postcss-react-native/src/DimensionComponent').default(pkgs.${namespace}, styler, keyframes, ${stringKey}); 439 | })(); 440 | ` 441 | }).join(';\n') 442 | }; 443 | export const omit = (obj, ...props)=> { 444 | if (!obj) return; 445 | if (props.length === 0) return Object.assign({}, obj); 446 | return Object.keys(obj).reduce((ret, key)=> { 447 | if (props.indexOf(key) === -1) { 448 | ret[key] = obj[key]; 449 | } 450 | return ret; 451 | }, {}); 452 | 453 | 454 | }; 455 | 456 | export default source; -------------------------------------------------------------------------------- /src/AnimatedCSS.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | PropTypes, 4 | } from 'react'; 5 | 6 | import { 7 | Animated, 8 | Easing, 9 | Dimensions, 10 | StyleSheet, 11 | } from 'react-native'; 12 | 13 | import {WINDOW} from './componentHelpers'; 14 | 15 | // These styles are not number based and thus needs to be interpolated 16 | const INTERPOLATION_STYLE_PROPERTIES = [ 17 | 18 | // View styles 19 | 'backgroundColor', 20 | 'borderColor', 21 | 'borderTopColor', 22 | 'borderRightColor', 23 | 'borderBottomColor', 24 | 'borderLeftColor', 25 | 'shadowColor', 26 | // Text styles 27 | 'color', 28 | 'textDecorationColor' 29 | ]; 30 | 31 | const EASING_FUNCTIONS = { 32 | 'linear': Easing.linear, 33 | 'ease': Easing.ease, 34 | 'ease-in': Easing.in(Easing.ease), 35 | 'ease-out': Easing.out(Easing.ease), 36 | 'ease-in-out': Easing.inOut(Easing.ease), 37 | }; 38 | 39 | 40 | // Determine to what value the animation should tween to 41 | function getAnimationTarget(iteration, animationDirection) { 42 | switch (animationDirection) { 43 | case 'reverse': 44 | return 0; 45 | case 'alternate': 46 | return (iteration % 2) ? 0 : 1; 47 | case 'alternate-reverse': 48 | return (iteration % 2) ? 1 : 0; 49 | case 'normal': 50 | default: 51 | return 1; 52 | } 53 | } 54 | 55 | // Like getAnimationTarget but opposite 56 | function getAnimationOrigin(iteration, animationDirection) { 57 | return getAnimationTarget(iteration, animationDirection) ? 0 : 1; 58 | } 59 | 60 | const asc = (a, b)=>a - b; 61 | 62 | const rangeCheck = ({inputRange, outputRange})=> { 63 | return (inputRange && outputRange && inputRange.length > 1 && outputRange.length > 1) 64 | }; 65 | 66 | // Make (almost) any component animatable, similar to Animated.createAnimatedComponent 67 | export function createAnimatableComponent(component, keyframes, name) { 68 | const Animatable = Animated.createAnimatedComponent(component); 69 | return class AnimatableComponent extends Component { 70 | 71 | static displayName = `${name || component.displayName || component.name}AnimatableComponent`; 72 | 73 | static propTypes = { 74 | onAnimationBegin: PropTypes.func, 75 | onAnimationEnd: PropTypes.func, 76 | animation: PropTypes.arrayOf(PropTypes.object), 77 | transition: PropTypes.object 78 | 79 | }; 80 | 81 | static defaultProps = { 82 | iterationCount: 1, 83 | onAnimationBegin() { 84 | }, 85 | onAnimationEnd() { 86 | } 87 | }; 88 | 89 | constructor(props) { 90 | super(props); 91 | this.state = this.createState(props); 92 | 93 | } 94 | 95 | createState(props) { 96 | const config = props.animation && props.animation[0]; 97 | if (!config) { 98 | return {}; 99 | } 100 | const state = { 101 | animationValue: new Animated.Value(getAnimationOrigin(0, config.animationDirection)), 102 | animationStyle: {}, 103 | transitionStyle: {}, 104 | transitionValues: {}, 105 | currentTransitionValues: {} 106 | }; 107 | return state; 108 | } 109 | 110 | 111 | setNativeProps(nativeProps) { 112 | if (this._root) { 113 | this._root.setNativeProps(nativeProps); 114 | } 115 | } 116 | 117 | componentDidMount() { 118 | if (!this.state) { 119 | this.setState(this.createState(this.props)); 120 | } 121 | if (this.props.transition) { 122 | this.transitionTo(this.props.transition); 123 | } else { 124 | this.triggerAnimation(this.props); 125 | } 126 | } 127 | 128 | triggerAnimation(props) { 129 | const {animation, onAnimationBegin, onAnimationEnd} = props; 130 | const conf = animation && animation[0]; 131 | if (!conf) return; 132 | const runAnimation = this.createAnimate(conf); 133 | const {animationDelay, animationName} = conf; 134 | if (animationName) { 135 | if (animationDelay) { 136 | this.setState({animationName}); 137 | this._timer = setTimeout(() => { 138 | onAnimationBegin(); 139 | this.setState({animationName: false}, () => runAnimation().then(onAnimationEnd)); 140 | this._timer = false; 141 | }, animationDelay); 142 | return; 143 | } 144 | /* for (let i = LAYOUT_DEPENDENT_ANIMATIONS.length - 1; i >= 0; i--) { 145 | if (animation.indexOf(LAYOUT_DEPENDENT_ANIMATIONS[i]) === 0) { 146 | this.setState({animationName: name}); 147 | return; 148 | } 149 | }*/ 150 | onAnimationBegin(); 151 | runAnimation().then(onAnimationEnd); 152 | } 153 | } 154 | 155 | componentWillUnmount() { 156 | if (this._timer) { 157 | clearTimeout(this._timer); 158 | } 159 | } 160 | 161 | componentWillReceiveProps(props) { 162 | const {animation, transition, onAnimationBegin, onAnimationEnd} = props; 163 | 164 | if (transition) { 165 | this.transitionTo(transition); 166 | } else if (animation !== this.props.animation) { 167 | if (animation[0]) { 168 | if (this.state.animationName) { 169 | this.setState({animationName: animation[0].animationName}); 170 | } else { 171 | this.triggerAnimation(props); 172 | } 173 | } else { 174 | this.stopAnimation(); 175 | } 176 | } 177 | } 178 | 179 | _handleLayout(event) { 180 | const {onLayout} = this.props; 181 | if (!this.state) return; 182 | const {animationName} = this.state; 183 | 184 | this._layout = event.nativeEvent.layout; 185 | if (onLayout) { 186 | onLayout(event); 187 | } 188 | 189 | if (animationName && !this._timer) { 190 | this.setState({animationName: false}, () => { 191 | this.triggerAnimation(this.props); 192 | }); 193 | } 194 | } 195 | 196 | makeInteropolate(animationValue, keyframe) { 197 | const itop = Object.keys(keyframe).sort(asc).reduce((ret, keyframeKey, i)=> { 198 | 199 | const frame = keyframe[keyframeKey]; 200 | const v = keyframeKey - 0; 201 | 202 | const range = v === 0 ? 0 : v / 100; 203 | 204 | Object.keys(frame).map(fk=> { 205 | if (fk in frame) { 206 | if (fk === 'transform') { 207 | const c = ret[fk] || (ret[fk] = {}); 208 | for (const transform of frame[fk]) { 209 | for (const transformKey of Object.keys(transform)) { 210 | const tc = c[transformKey] || (c[transformKey] = { 211 | inputRange: [], 212 | outputRange: [] 213 | } ); 214 | tc.inputRange.push(range); 215 | tc.outputRange.push(transform[transformKey]); 216 | } 217 | } 218 | } else { 219 | const c = ret[fk] || (ret[fk] = {inputRange: [], outputRange: []}); 220 | c.inputRange.push(range); 221 | c.outputRange.push(frame[fk]); 222 | } 223 | } 224 | }); 225 | return ret; 226 | }, {}); 227 | return Object.keys(itop).reduce((ret, key)=> { 228 | const itk = itop[key]; 229 | if (key === 'transform') { 230 | Object.keys(itk).forEach((transformKey)=> { 231 | if (rangeCheck(itk[transformKey])) { 232 | const tr = ret[key] || (ret[key] = []); 233 | tr.push({[transformKey]: animationValue.interpolate(itk[transformKey])}) 234 | } 235 | }); 236 | } else if (rangeCheck(itk)) { 237 | ret[key] = animationValue.interpolate(itk); 238 | } 239 | return ret; 240 | }, {}); 241 | } 242 | 243 | createAnimate(conf) { 244 | const mix = this._layout ? Object.assign({ 245 | clientHeight: this._layout.height, 246 | clientWidth: this._layout.width 247 | }, WINDOW) : WINDOW; 248 | const kf = keyframes[conf.animationName](mix); 249 | const ref = Object.keys(kf).reduce((ret, key)=> { 250 | ret[key] = kf[key](mix).__style; 251 | return ret; 252 | }, {}); 253 | return ()=> { 254 | return this.animate(conf, this.makeInteropolate(this.state.animationValue, ref)); 255 | } 256 | } 257 | 258 | animate(conf, animationStyle) { 259 | return new Promise(resolve => { 260 | this.setState({animationStyle}, () => { 261 | this._startAnimation(conf, 0, resolve); 262 | }); 263 | }); 264 | } 265 | 266 | stopAnimation() { 267 | this.setState({ 268 | animationName: false, 269 | animationStyle: {}, 270 | }); 271 | this.state.animationValue.stopAnimation(); 272 | if (this._timer) { 273 | clearTimeout(this._timer); 274 | this._timer = false; 275 | } 276 | } 277 | 278 | _startAnimation(conf, currentIteration = 0, callback) { 279 | let {animationDirection='normal', animationDuration=1000, animationDelay=0, animationIterationCount=1, animationTimingFunction='ease-in-out'} = conf; 280 | const {animationValue} = this.state; 281 | const fromValue = getAnimationOrigin(currentIteration, animationDirection); 282 | const toValue = getAnimationTarget(currentIteration, animationDirection); 283 | animationValue.setValue(fromValue); 284 | 285 | // This is on the way back reverse 286 | if (( 287 | (animationDirection === 'reverse') || 288 | (animationDirection === 'alternate' && !toValue) || 289 | (animationDirection === 'alternate-reverse' && !toValue) 290 | ) && animationTimingFunction.match(/^ease\-(in|out)$/)) { 291 | if (animationTimingFunction.indexOf('-in') !== -1) { 292 | animationTimingFunction = animationTimingFunction.replace('-in', '-out'); 293 | } else { 294 | animationTimingFunction = animationTimingFunction.replace('-out', '-in'); 295 | } 296 | } 297 | Animated.timing(animationValue, { 298 | toValue: toValue, 299 | easing: EASING_FUNCTIONS[animationTimingFunction], 300 | isInteraction: !animationIterationCount, 301 | duration: animationDuration, 302 | }).start(endState => { 303 | currentIteration++; 304 | if (endState.finished && !this.props.transition && (animationIterationCount === 'infinite' || currentIteration < animationIterationCount)) { 305 | this._startAnimation(conf, currentIteration, callback); 306 | } else if (callback) { 307 | callback(endState); 308 | } 309 | }); 310 | } 311 | 312 | 313 | transitionTo(transit) { 314 | const {transitionStyle, transitionValues} = this.state; 315 | 316 | let doTransition = false; 317 | 318 | Object.keys(transit).forEach(property => { 319 | if (transitionStyle && transitionValues && INTERPOLATION_STYLE_PROPERTIES.indexOf(property) === -1 && transitionStyle[property] && transitionStyle[property] === transitionValues[property]) { 320 | return this._transitionToValue(transitionValues[property], transit[property]); 321 | } else { 322 | doTransition = true; 323 | } 324 | }); 325 | 326 | if (doTransition) { 327 | this.transition(transit); 328 | } 329 | } 330 | 331 | transition(transit) { 332 | const {transitionValues={}, currentTransitionValues={}, transitionStyle={}} = this.state; 333 | Object.keys(transit).forEach(property => { 334 | const fromValue = transit[property].from; 335 | const toValue = transit[property].to; 336 | const transitionValue = transitionValues[property] || ( transitionValues[property] = new Animated.Value(0)); 337 | transitionStyle[property] = transitionValue; 338 | if (property == 'transform') { 339 | transitionValue.setValue(0); 340 | transitionStyle.transform = fromValue.reduce((r, fv, i)=> { 341 | const tv = toValue[i]; 342 | Object.keys(fv).forEach(transformKey => { 343 | r.push({ 344 | [transformKey]: transitionValue.interpolate({ 345 | inputRange: [0, 1], 346 | outputRange: [fv[transformKey], tv[transformKey]] 347 | }) 348 | }); 349 | }); 350 | return r; 351 | }, []); 352 | currentTransitionValues[property] = toValue; 353 | // toValues[property] = 1; 354 | } else if (INTERPOLATION_STYLE_PROPERTIES.includes(property)) { 355 | transitionValue.setValue(0); 356 | transitionStyle[property] = transitionValue.interpolate({ 357 | inputRange: [0, 1], 358 | outputRange: [fromValue, toValue] 359 | }); 360 | } else { 361 | transitionValue.setValue(fromValue); 362 | } 363 | }); 364 | 365 | this.setState({ 366 | transitionValues, 367 | transitionStyle, 368 | currentTransitionValues 369 | }, this._transitionToValues.bind(this, transit)); 370 | } 371 | 372 | _transitionToValues(transit) { 373 | const {transitionValues={}} = this.state; 374 | Object.keys(transit).forEach(property => { 375 | this._transitionToValue(transitionValues[property], transit[property]); 376 | }); 377 | } 378 | 379 | _transitionToValue(transitionValue, {toValue = 1, duration = 1000, timingFunction = 'ease-in-out'}) { 380 | Animated.timing(transitionValue, { 381 | toValue, 382 | duration, 383 | easing: EASING_FUNCTIONS[timingFunction] 384 | }).start(); 385 | } 386 | 387 | render() { 388 | const {style, children, onLayout, animation, animationDuration, animationDelay, transition, ...props} = this.props; 389 | if (animation && transition) { 390 | throw new Error('You cannot combine animation and transition props'); 391 | } 392 | 393 | return ( 394 | this._root = element} 397 | onLayout={event => this._handleLayout(event)} 398 | style={[ 399 | style, 400 | this.state.animationStyle, 401 | this.state.transitionStyle 402 | ]}>{children} 403 | ); 404 | } 405 | }; 406 | } 407 | /* 408 | 409 | export const View = createAnimatableComponent(ReactNative.View); 410 | export const Text = createAnimatableComponent(ReactNative.Text); 411 | export const Image = createAnimatableComponent(ReactNative.Image); 412 | export default View;*/ 413 | --------------------------------------------------------------------------------