first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present, songsiqi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,124 @@
# Weex `<style>` Transformer
[![NPM version][npm-image]][npm-url]
[![Build status][circle-image]][circle-url]
[![Downloads][downloads-image]][downloads-url]
[npm-image]: https://img.shields.io/npm/v/weex-styler.svg?style=flat-square
[npm-url]: https://npmjs.org/package/weex-styler
[circle-image]: https://circleci.com/gh/alibaba/weex_toolchain.svg?style=svg
[circle-url]: https://circleci.com/gh/alibaba/weex_toolchain/tree/master
[downloads-image]: https://img.shields.io/npm/dm/weex-styler.svg?style=flat-square
[downloads-url]: https://npmjs.org/package/weex-styler
## Features
- convert a `<style>` element to JSON format
- autofix common mistakes
- friendly warnings
## API
- `parse(code, done)`
- `validate(json, done)`
- `validateItem(name, value)`
### util api
- `util.hyphenedToCamelCase(value)`
- `util.camelCaseToHyphened(value)`
```javascript
/**
* Parse `<style>` code to a JSON Object and log errors & warnings
*
* @param {string} code
* @param {function} done
*/
function parse(code, done) {}
/**
* Validate a JSON Object and log errors & warnings
*
* @param {object} json
* @param {function} done
*/
function validate(json, done) {}
/**
* Result callback
*
* data
* - jsonStyle{}: `classname.propname.value`-like object
* - log[{line, column, reason}]
*
* @param {Error} err
* @param {object} data
*/
function done(err, data) {}
/**
* Validate a single name-value pair
*
* @param {string} name camel cased
* @param {string} value
* @return {object}
* - value
* - log{reason}
*/
function validateItem(name, value) {}
```
## Validation
- rule check: only common rule type supported, othres will be ignored
- selector check: only single-classname selector is supported, others will be ignored
- prop name check: out-of-defined prop name will be warned but preserved
- prop value check: common prop value mistakes will be autofixed or ignored
+ color type: keywords, `#xxx` -> warning: `#xxxxxx`
+ color type: `transparent` -> error: not supported
+ length type: `100px` -> warning: `100`
## Demo
```javascript
var styler = require('weex-styler')
var code = 'html {color: #000000;} .foo {color: red; -webkit-transform: rotate(90deg); width: 200px;}'
styler.parse(code, function (err, data) {
// syntax error
// format: {line, column, reason, ...}
err
// result
// {foo: {color: '#ff0000', webkitTransform: 'rotate(90deg)', width: 200}}
data.jsonStyle
// format: {line, column, reason}
// - Error: Selector `html` is not supported. Weex only support single-classname selector
// - Warning: prop value `red` is autofixed to `#ff0000`
// - Warning: prop name `-webkit-transform` is not supported
// - Warning: prop value `200px` is autofixed to `200`
data.log[]
})
var jsonStyle = {
foo: {
color: 'red',
webkitTransform: 'rotate(90deg)',
width: '200px'
}
}
styler.validate(json, function (err, data) {
// syntax error
err
// result
// {foo: {color: '#ff0000', webkitTransform: 'rotate(90deg)', width: 200}}
data.jsonStyle
// format: {reason}
// - Warning: prop value `red` is autofixed to `#ff0000`
// - Warning: prop name `-webkit-transform` is not supported
// - Warning: prop value `200px` is autofixed to `200`
data.log[]
})
```
@@ -0,0 +1,3 @@
machine:
node:
version: 4.2.1
@@ -0,0 +1,25 @@
var gulp = require('gulp')
var jscs = require('gulp-jscs')
var mocha = require('gulp-mocha')
gulp.task('default', ['test'], function() {
console.log('done')
})
gulp.task('watch', function () {
return gulp.watch('**/*.js', ['test'])
})
gulp.task('test', ['jscs', 'mocha'], function () {
console.log('test done')
})
gulp.task('mocha', function () {
return gulp.src([
'test/*.js'
]).pipe(mocha())
})
gulp.task('jscs', function () {
return gulp.src('**/*.js').pipe(jscs())
})
@@ -0,0 +1,231 @@
'use strict'
var css = require('css')
var util = require('./lib/util')
var validateItem = require('./lib/validator').validate
var shorthandParser = require('./lib/shorthand-parser')
/**
* mergeStyle
* @param {*} object
* @param {*} classNames
* @param {*} preClassNames
* @param {*} ruleResult
* @param {*} prop
* @param {*} index
*/
function mergeStyle (object, classNames, preClassNames, ruleResult, prop, index) {
if (!process.env.UNI_USING_NVUE_STYLE_COMPILER) {
object[classNames] = object[classNames] || {}
object[classNames][prop] = ruleResult[prop]
return
}
classNames = classNames.split('.').map(str => '.' + str).slice(1)
var className = classNames.find(className => className in object) || classNames[0]
// 假设选择器已经去重简化
preClassNames += classNames.filter(str => str !== className).sort().join('')
var rules = object[className] = object[className] || {}
var style = rules[preClassNames] = rules[preClassNames] || {}
// 增加其他权重信息
style[prop] = [...ruleResult[prop], preClassNames.split('.').length - 1, index]
}
/**
* Parse `<style>` code to a JSON Object and log errors & warnings
*
* @param {string} code
* @param {function} done which will be called with
* - err:Error
* - data.jsonStyle{}: `classname.propname.value`-like object
* - data.log[{line, column, reason}]
*/
function parse (code, done) {
var ast, err, jsonStyle = {}, log = []
// css parse
ast = css.parse(code, { silent: true })
// catch syntax error
if (ast.stylesheet.parsingErrors && ast.stylesheet.parsingErrors.length) {
err = ast.stylesheet.parsingErrors
err.forEach(function (error) {
log.push({ line: error.line, column: error.column, reason: error.toString().replace('Error', 'ERROR') })
})
}
// walk all
/* istanbul ignore else */
if (ast && ast.type === 'stylesheet' && ast.stylesheet &&
ast.stylesheet.rules && ast.stylesheet.rules.length) {
ast.stylesheet.rules.forEach(function (rule, index) {
var type = rule.type
var ruleResult = {}
var ruleLog = []
if (type === 'rule') {
if (rule.declarations && rule.declarations.length) {
rule.declarations = shorthandParser(rule.declarations)
rule.declarations.forEach(function (declaration) {
var subType = declaration.type
var name, value, line, column, subResult, camelCasedName
/* istanbul ignore if */
if (subType !== 'declaration') {
return
}
name = declaration.property
value = declaration.value
var newValue = value.replace(/\s*!important/g, '')
var importantWeight = Number(value !== newValue)
value = newValue
// validate declarations and collect them to result
camelCasedName = util.hyphenedToCamelCase(name)
subResult = validateItem(camelCasedName, value)
/* istanbul ignore else */
if (typeof subResult.value === 'number' || typeof subResult.value === 'string') {
if (process.env.UNI_USING_NVUE_STYLE_COMPILER) {
var oldValue = ruleResult[camelCasedName]
// 增加 important 权重信息
ruleResult[camelCasedName] = Array.isArray(oldValue) && oldValue[1] > importantWeight ? oldValue : [subResult.value, importantWeight]
} else {
ruleResult[camelCasedName] = subResult.value
}
}
if (subResult.log) {
subResult.log.line = declaration.position.start.line
subResult.log.column = declaration.position.start.column
ruleLog.push(subResult.log)
}
})
rule.selectors.forEach(function (selector) {
selector = selector.replace(/\s*([\+\~\>])\s*/g, '$1').replace(/\s+/, ' ')
// 支持组合选择器
const res = selector.match(process.env.UNI_USING_NVUE_STYLE_COMPILER ? /^((?:(?:\.[A-Za-z0-9_\-]+)+[\+\~\> ])*)((?:\.[A-Za-z0-9_\-\:]+)+)$/ : /^(\.)([A-Za-z0-9_\-:]+)$/)
if (res) {
var preClassNames = res[1]
var classNames = res[2]
// handle pseudo class
var pseudoIndex = classNames.indexOf(':')
if (pseudoIndex > -1) {
var pseudoCls = classNames.slice(pseudoIndex)
classNames = classNames.slice(0, pseudoIndex)
var pseudoRuleResult = {}
Object.keys(ruleResult).forEach(function (prop) {
pseudoRuleResult[prop + pseudoCls] = ruleResult[prop]
})
ruleResult = pseudoRuleResult
}
// merge style
Object.keys(ruleResult).forEach(function (prop) {
// // handle transition
// if (prop.indexOf('transition') === 0 && prop !== 'transition') {
// var realProp = prop.replace('transition', '')
// realProp = realProp[0].toLowerCase() + realProp.slice(1)
// var object = jsonStyle['@TRANSITION'] = jsonStyle['@TRANSITION'] || {}
// mergeStyle(object, classNames, preClassNames, ruleResult, prop, index)
// }
mergeStyle(jsonStyle, classNames, preClassNames, ruleResult, prop, index)
})
}
else {
log.push({
line: rule.position.start.line,
column: rule.position.start.column,
reason: 'ERROR: Selector `' + selector + '` is not supported. Weex only support classname selector'
})
}
})
log = log.concat(ruleLog)
}
}
/* istanbul ignore else */
else if (type === 'font-face') {
/* istanbul ignore else */
if (rule.declarations && rule.declarations.length) {
rule.declarations.forEach(function (declaration) {
/* istanbul ignore if */
if (declaration.type !== 'declaration') {
return
}
var name = util.hyphenedToCamelCase(declaration.property)
var value = declaration.value
if (name === 'fontFamily' && '\"\''.indexOf(value[0]) > -1) { // FIXME: delete leading and trailing quotes
value = value.slice(1, value.length - 1)
}
ruleResult[name] = value
})
if (!jsonStyle['@FONT-FACE']) {
jsonStyle['@FONT-FACE'] = []
}
jsonStyle['@FONT-FACE'].push(ruleResult)
}
}
})
}
jsonStyle['@VERSION'] = 2
done(err, { jsonStyle: jsonStyle, log: log })
}
/**
* Validate a JSON Object and log errors & warnings
*
* @param {object} json
* @param {function} done which will be called with
* - err:Error
* - data.jsonStyle{}: `classname.propname.value`-like object
* - data.log[{reason}]
*/
function validate (json, done) {
var log = []
var err
try {
json = JSON.parse(JSON.stringify(json))
}
catch (e) {
err = e
json = {}
}
Object.keys(json).forEach(function (selector) {
var declarations = json[selector]
Object.keys(declarations).forEach(function (name) {
var value = declarations[name]
var result = validateItem(name, value)
if (typeof result.value === 'number' || typeof result.value === 'string') {
declarations[name] = result.value
}
else {
delete declarations[name]
}
if (result.log) {
log.push(result.log)
}
})
})
done(err, {
jsonStyle: json,
log: log
})
}
module.exports = {
parse: parse,
validate: validate,
validateItem: validateItem,
util: util
}
@@ -0,0 +1,233 @@
function generateDeclaration (property, value, important, position) {
return {
type: 'declaration',
property,
value: value + (important ? ' !important' : ''),
position
}
}
function clearImportant (value) {
var newValue = value.replace(/\s*!important/g, '')
return {
value: newValue,
important: value !== newValue
}
}
function transition (declaration) {
var CHUNK_REGEXP = /^([a-z-_]\S*)(\s+[\d.]+m?s)?(\s+[a-z-_]\S*)?(\s+[\d.]+m?s)?/
var { value, important } = clearImportant(declaration.value)
var values = value.replace(/(\d)\s*,\s*/g, '$1#').split(',')
var position = declaration.position
var result = []
var map = {
'transition-property': [],
'transition-duration': [],
'transition-timing-function': [],
'transition-delay': []
}
if (values.length) {
for (var i1 = 0; i1 < values.length; i1++) {
var match = values[i1].trim().match(CHUNK_REGEXP)
if (!match) {
return []
}
map['transition-property'].push(match[1] || 'all')
map['transition-duration'].push((match[2] || '0s').trim())
map['transition-timing-function'].push((match[3] || 'ease').trim().replace(/#/g, ', '))
map['transition-delay'].push((match[4] || '0s').trim())
}
for (var key in map) {
var value = map[key]
value = value.find(item => item !== value[0]) ? value.join(', ') : value[0]
result.push(generateDeclaration(key, value, important, position))
}
}
return result
}
function createParser (property) {
return function (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
var splitResult = value.split(/\s+/)
var result = []
switch (splitResult.length) {
case 1:
splitResult.push(splitResult[0], splitResult[0], splitResult[0])
break
case 2:
splitResult.push(splitResult[0], splitResult[1])
break
case 3:
splitResult.push(splitResult[1])
break
}
result.push(
generateDeclaration(property + '-top', splitResult[0], important, position),
generateDeclaration(property + '-right', splitResult[1], important, position),
generateDeclaration(property + '-bottom', splitResult[2], important, position),
generateDeclaration(property + '-left', splitResult[3], important, position)
)
return result
}
}
var margin = createParser('margin')
var padding = createParser('padding')
function border (declaration) {
var { value, important } = clearImportant(declaration.value)
var property = declaration.property
var position = declaration.position
var splitResult = value.replace(/\s*,\s*/g, ',').split(/\s+/)
var result = [/^[\d\.]+\S*$/, /^(solid|dashed|dotted)$/, /\S+/].map(item => {
var index = splitResult.findIndex(str => item.test(str))
return index < 0 ? null : splitResult.splice(index, 1)[0]
})
if (splitResult.length) {
return declaration
}
return [
generateDeclaration(property + '-width', (result[0] || '0').trim(), important, position),
generateDeclaration(property + '-style', (result[1] || 'solid').trim(), important, position),
generateDeclaration(property + '-color', (result[2] || '#000000').trim(), important, position)
]
}
function borderProperty (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
var property = declaration.property.split('-')[1]
var splitResult = value.replace(/\s*,\s*/g, ',').split(/\s+/)
var result = []
switch (splitResult.length) {
case 1:
return declaration
case 2:
splitResult.push(splitResult[0], splitResult[1])
break
case 3:
splitResult.push(splitResult[1])
break
}
result.push(
generateDeclaration('border-top-' + property, splitResult[0], important, position),
generateDeclaration('border-right-' + property, splitResult[1], important, position),
generateDeclaration('border-bottom-' + property, splitResult[2], important, position),
generateDeclaration('border-left-' + property, splitResult[3], important, position)
)
return result
}
function borderRadius (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
var splitResult = value.split(/\s+/)
var result = []
if (value.includes('/')) {
return declaration
}
switch (splitResult.length) {
case 1:
return declaration
case 2:
splitResult.push(splitResult[0], splitResult[1])
break
case 3:
splitResult.push(splitResult[1])
break
}
result.push(
generateDeclaration('border-top-left-radius', splitResult[0], important, position),
generateDeclaration('border-top-right-radius', splitResult[1], important, position),
generateDeclaration('border-bottom-right-radius', splitResult[2], important, position),
generateDeclaration('border-bottom-left-radius', splitResult[3], important, position)
)
return result
}
function flexFlow (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
var splitResult = value.split(/\s+/)
var result = [/^(column|column-reverse|row|row-reverse)$/, /^(nowrap|wrap|wrap-reverse)$/].map(item => {
var index = splitResult.findIndex(str => item.test(str))
return index < 0 ? null : splitResult.splice(index, 1)[0]
})
if (splitResult.length) {
return declaration
}
return [
generateDeclaration('flex-direction', result[0] || 'column', important, position),
generateDeclaration('flex-wrap', result[1] || 'nowrap', important, position)
]
}
function font (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
var splitResult = value.replace(/,\s*/g, ',').replace(/\s*\/\s*/, '/').replace(/['"].+?['"]/g, str => str.replace(/\s+/g, '#')).split(/\s+/)
var result = []
var styleValues = ['normal', 'italic', 'oblique']
result.push(generateDeclaration('font-style', styleValues[Math.max(0, styleValues.indexOf(splitResult[0]))], important, position))
var weight = splitResult.slice(0, -2).find(str => /normal|bold|lighter|bolder|\d+/.test(str))
if (weight) {
result.push(generateDeclaration('font-weight', weight, important, position))
}
splitResult = splitResult.slice(-2)
if (/[\d\.]+\S*(\/[\d\.]+\S*)?/.test(splitResult[0])) {
var [size, height] = splitResult[0].split('/')
result.push(
generateDeclaration('font-size', size, important, position),
generateDeclaration('line-height', height || 'normal', important, position),
generateDeclaration('font-family', splitResult[1].replace(/#/g, ' '), important, position)
)
return result
}
return []
}
function background (declaration) {
var { value, important } = clearImportant(declaration.value)
var position = declaration.position
if (/^#?\S+$/.test(value) || /^rgba?(.+)$/.test(value)) {
return generateDeclaration('background-color', value, important, position)
} else if (/^linear-gradient(.+)$/.test(value)) {
return generateDeclaration('background-image', value, important, position)
} else {
return declaration
}
}
var parserCollection = {
transition,
margin,
padding,
border,
'border-top': border,
'border-right': border,
'border-bottom': border,
'border-left': border,
'border-style': borderProperty,
'border-width': borderProperty,
'border-color': borderProperty,
'border-radius': borderRadius,
'flex-flow': flexFlow,
font,
background
}
module.exports = function (declarations) {
return declarations.reduce((result, declaration) => {
var parser = parserCollection[declaration.property]
if (parser) {
return result.concat(parser(declaration))
} else {
result.push(declaration)
return result
}
}, [])
}
@@ -0,0 +1,30 @@
/**
* rules:
* - abc-def -> abcDef
* - -abc-def -> AbcDef
*
* @param {string} value
* @return {string}
*/
exports.hyphenedToCamelCase = function hyphenedToCamelCase(value) {
return value.replace(/-([a-z])/g, function(s, m) {
return m.toUpperCase()
})
}
/**
* rules:
* - abcDef -> abc-def
* - AbcDef -> -abc-def
*
* @param {string} value
* @return {string}
*/
exports.camelCaseToHyphened = function camelCaseToHyphened(value) {
return value.replace(/([A-Z])/g, function(s, m) {
if (typeof m === 'string') {
return '-' + m.toLowerCase()
}
return m
})
}
@@ -0,0 +1,725 @@
var util = require('./util')
// http://www.w3.org/TR/css3-color/#html4
var BASIC_COLOR_KEYWORDS = {
black: '#000000',
silver: '#C0C0C0',
gray: '#808080',
white: '#FFFFFF',
maroon: '#800000',
red: '#FF0000',
purple: '#800080',
fuchsia: '#FF00FF',
green: '#008000',
lime: '#00FF00',
olive: '#808000',
yellow: '#FFFF00',
navy: '#000080',
blue: '#0000FF',
teal: '#008080',
aqua: '#00FFFF'
}
// http://www.w3.org/TR/css3-color/#svg-color
var EXTENDED_COLOR_KEYWORDS = {
aliceblue: '#F0F8FF',
antiquewhite: '#FAEBD7',
aqua: '#00FFFF',
aquamarine: '#7FFFD4',
azure: '#F0FFFF',
beige: '#F5F5DC',
bisque: '#FFE4C4',
black: '#000000',
blanchedalmond: '#FFEBCD',
blue: '#0000FF',
blueviolet: '#8A2BE2',
brown: '#A52A2A',
burlywood: '#DEB887',
cadetblue: '#5F9EA0',
chartreuse: '#7FFF00',
chocolate: '#D2691E',
coral: '#FF7F50',
cornflowerblue: '#6495ED',
cornsilk: '#FFF8DC',
crimson: '#DC143C',
cyan: '#00FFFF',
darkblue: '#00008B',
darkcyan: '#008B8B',
darkgoldenrod: '#B8860B',
darkgray: '#A9A9A9',
darkgreen: '#006400',
darkgrey: '#A9A9A9',
darkkhaki: '#BDB76B',
darkmagenta: '#8B008B',
darkolivegreen: '#556B2F',
darkorange: '#FF8C00',
darkorchid: '#9932CC',
darkred: '#8B0000',
darksalmon: '#E9967A',
darkseagreen: '#8FBC8F',
darkslateblue: '#483D8B',
darkslategray: '#2F4F4F',
darkslategrey: '#2F4F4F',
darkturquoise: '#00CED1',
darkviolet: '#9400D3',
deeppink: '#FF1493',
deepskyblue: '#00BFFF',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1E90FF',
firebrick: '#B22222',
floralwhite: '#FFFAF0',
forestgreen: '#228B22',
fuchsia: '#FF00FF',
gainsboro: '#DCDCDC',
ghostwhite: '#F8F8FF',
gold: '#FFD700',
goldenrod: '#DAA520',
gray: '#808080',
green: '#008000',
greenyellow: '#ADFF2F',
grey: '#808080',
honeydew: '#F0FFF0',
hotpink: '#FF69B4',
indianred: '#CD5C5C',
indigo: '#4B0082',
ivory: '#FFFFF0',
khaki: '#F0E68C',
lavender: '#E6E6FA',
lavenderblush: '#FFF0F5',
lawngreen: '#7CFC00',
lemonchiffon: '#FFFACD',
lightblue: '#ADD8E6',
lightcoral: '#F08080',
lightcyan: '#E0FFFF',
lightgoldenrodyellow: '#FAFAD2',
lightgray: '#D3D3D3',
lightgreen: '#90EE90',
lightgrey: '#D3D3D3',
lightpink: '#FFB6C1',
lightsalmon: '#FFA07A',
lightseagreen: '#20B2AA',
lightskyblue: '#87CEFA',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#B0C4DE',
lightyellow: '#FFFFE0',
lime: '#00FF00',
limegreen: '#32CD32',
linen: '#FAF0E6',
magenta: '#FF00FF',
maroon: '#800000',
mediumaquamarine: '#66CDAA',
mediumblue: '#0000CD',
mediumorchid: '#BA55D3',
mediumpurple: '#9370DB',
mediumseagreen: '#3CB371',
mediumslateblue: '#7B68EE',
mediumspringgreen: '#00FA9A',
mediumturquoise: '#48D1CC',
mediumvioletred: '#C71585',
midnightblue: '#191970',
mintcream: '#F5FFFA',
mistyrose: '#FFE4E1',
moccasin: '#FFE4B5',
navajowhite: '#FFDEAD',
navy: '#000080',
oldlace: '#FDF5E6',
olive: '#808000',
olivedrab: '#6B8E23',
orange: '#FFA500',
orangered: '#FF4500',
orchid: '#DA70D6',
palegoldenrod: '#EEE8AA',
palegreen: '#98FB98',
paleturquoise: '#AFEEEE',
palevioletred: '#DB7093',
papayawhip: '#FFEFD5',
peachpuff: '#FFDAB9',
peru: '#CD853F',
pink: '#FFC0CB',
plum: '#DDA0DD',
powderblue: '#B0E0E6',
purple: '#800080',
red: '#FF0000',
rosybrown: '#BC8F8F',
royalblue: '#4169E1',
saddlebrown: '#8B4513',
salmon: '#FA8072',
sandybrown: '#F4A460',
seagreen: '#2E8B57',
seashell: '#FFF5EE',
sienna: '#A0522D',
silver: '#C0C0C0',
skyblue: '#87CEEB',
slateblue: '#6A5ACD',
slategray: '#708090',
slategrey: '#708090',
snow: '#FFFAFA',
springgreen: '#00FF7F',
steelblue: '#4682B4',
tan: '#D2B48C',
teal: '#008080',
thistle: '#D8BFD8',
tomato: '#FF6347',
turquoise: '#40E0D0',
violet: '#EE82EE',
wheat: '#F5DEB3',
white: '#FFFFFF',
whitesmoke: '#F5F5F5',
yellow: '#FFFF00',
yellowgreen: '#9ACD32'
}
var LENGTH_REGEXP = /^[-+]?\d*\.?\d+(\S*)$/
var SUPPORT_CSS_UNIT = ['px', 'pt', 'wx']
if(process.env.UNI_USING_NVUE_COMPILER){
SUPPORT_CSS_UNIT.push('upx')
SUPPORT_CSS_UNIT.push('rpx')
}
var ANYTHING_VALIDATOR = function ANYTHING_VALIDATOR(v) {
return { value: v }
}
/**
* the values below is valid
* - number
* - number + 'px'
*
* @param {string} v
* @return {function} a function to return
* - value: number|null
* - reason(k, v, result)
*/
var LENGTH_VALIDATOR = function LENGTH_VALIDATOR(v) {
v = (v || '').toString()
var match = v.match(LENGTH_REGEXP)
if (match) {
var unit = match[1]
if (!unit) {
return {value: parseFloat(v)}
}
else if (SUPPORT_CSS_UNIT.indexOf(unit) > -1) {
return {value: v}
}
else {
return {
value: parseFloat(v),
reason: function reason(k, v, result) {
return 'NOTE: unit `' + unit + '` is not supported and property value `' + v + '` is autofixed to `' + result + '`'
}
}
}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (only number and pixel values are supported)'
}
}
}
/**
* the values below is valid
* - number {1,4}
* - number + 'px' {1,4}
*
* @param {string} v
* @return {function} a function to return
* - value: number|null
* - reason(k, v, result)
*/
var SHORTHAND_LENGTH_VALIDATOR = function SHORTHAND_LENGTH_VALIDATOR(v) {
v = (v || '').toString()
var value = []
var reason = []
var results = v.split(/\s+/).map(LENGTH_VALIDATOR)
for (var i = 0; i < results.length; ++i) {
var res = results[i]
if (!res.value) {
value = null
reason = res.reason
break
}
value.push(res.value)
reason.push(res.reason)
}
if (!value) {
return {
value: value,
reason: reason
}
} else {
return {
value: value.join(' '),
reason: function (k, v, result) {
return reason.map(function (res) {
if (typeof res === 'function') {
return res(k, v, result)
}
}).join('\n')
}
}
}
}
/**
* the values below is valid
* - hex color value (#xxxxxx or #xxx)
* - basic and extended color keywords in CSS spec
*
* @param {string} v
* @return {function} a function to return
* - value: string|null
* - reason(k, v, result)
*/
var COLOR_VALIDATOR = function COLOR_VALIDATOR(v) {
v = (v || '').toString()
if (v.match(/^#[0-9a-fA-F]{6}$/)) {
return {value: v}
}
if (v.match(/^#[0-9a-fA-F]{3}$/)) {
return {
value: '#' + v[1] + v[1] + v[2] + v[2] + v[3] + v[3],
reason: function reason(k, v, result) {
return 'NOTE: property value `' + v + '` is autofixed to `' + result + '`'
}
}
}
if (EXTENDED_COLOR_KEYWORDS[v]) {
return {
value: EXTENDED_COLOR_KEYWORDS[v],
reason: function reason(k, v, result) {
return 'NOTE: property value `' + v + '` is autofixed to `' + result + '`'
}
}
}
var arrColor, r, g, b, a
var RGB_REGEXP = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/gi
var RGBA_REGEXP = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d*\.?\d+)\s*\)$/gi
if (arrColor = RGB_REGEXP.exec(v)) {
r = parseInt(arrColor[1])
g = parseInt(arrColor[2])
b = parseInt(arrColor[3])
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
return {value: 'rgb(' + [r, g, b].join(',') + ')'}
}
}
if (arrColor = RGBA_REGEXP.exec(v)) {
r = parseInt(arrColor[1])
g = parseInt(arrColor[2])
b = parseInt(arrColor[3])
a = parseFloat(arrColor[4])
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1) {
return {value: 'rgba(' + [r, g, b, a].join(',') + ')'}
}
}
if (v === 'transparent') {
return {value: 'rgba(0,0,0,0)'}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not valid for `' + util.camelCaseToHyphened(k) + '`'
}
}
}
/**
* only integer or float value is valid
*
* @param {string} v
* @return {function} a function to return
* - value: number|null
* - reason(k, v, result)
*/
var NUMBER_VALIDATOR = function NUMBER_VALIDATOR(v) {
v = (v || '').toString()
var match = v.match(LENGTH_REGEXP)
if (match && !match[1]) {
return {value: parseFloat(v)}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (only number is supported)'
}
}
}
/**
* only integer value is valid
*
* @param {string} v
* @return {function} a function to return
* - value: number|null
* - reason(k, v, result)
*/
var INTEGER_VALIDATOR = function INTEGER_VALIDATOR(v) {
v = (v || '').toString()
if (v.match(/^[-+]?\d+$/)) {
return {value: parseInt(v, 10)}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (only integer is supported)'
}
}
}
/**
* transition-property: only css property is valid
*
* @param {string} v
* @return {function} a function to return
* - value: string|null
* - reason(k, v, result)
*/
var TRANSITION_PROPERTY_VALIDATOR = function TRANSITION_PROPERTY_VALIDATOR(v) {
v = (v || '').toString()
v = v.split(/\s*,\s*/).map(util.hyphenedToCamelCase).join(',')
if (v.split(/\s*,\s*/).every(p => !!validatorMap[p])) {
return {value: v}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (only css property is valid)'
}
}
}
/**
* transition-duration & transition-delay: only number of seconds or milliseconds is valid
*
* @param {string} v
* @return {function} a function to return
* - value: number|null
* - reason(k, v, result)
*/
var TRANSITION_INTERVAL_VALIDATOR = function TRANSITION_INTERVAL_VALIDATOR(v) {
v = (v || 0).toString()
var match, num, ret
if (match = v.match(/^\d*\.?\d+(ms|s)?$/)) {
num = parseFloat(match[0])
if (!match[1]) {
ret = {value: parseInt(num)}
}
else {
if (match[1] === 's') {
num *= 1000
}
ret = {
value: parseInt(num),
reason: function reason(k, v, result) {
return 'NOTE: property value `' + v + '` is autofixed to `' + result + '`'
}
}
}
return ret
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (only number of seconds and milliseconds is valid)'
}
}
}
/**
* transition-timing-function: only linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n) is valid
*
* @param {string} v
* @return {function} a function to return
* - value: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n)|null
* - reason(k, v, result)
*/
var TRANSITION_TIMING_FUNCTION_VALIDATOR = function TRANSITION_TIMING_FUNCTION_VALIDATOR(v) {
v = (v || '').toString()
if (v.match(/^linear|ease|ease-in|ease-out|ease-in-out$/)) {
return {value: v}
}
var match, ret
var NUM_REGEXP = /^[-]?\d*\.?\d+$/
if (match = v.match(/^cubic-bezier\(\s*(.*)\s*,\s*(.*)\s*,\s*(.*)\s*,\s*(.*)\s*\)$/)) {
/* istanbul ignore else */
if (match[1].match(NUM_REGEXP) && match[2].match(NUM_REGEXP) && match[3].match(NUM_REGEXP) && match[4].match(NUM_REGEXP)) {
ret = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3]), parseFloat(match[4])].join(',')
return {value: 'cubic-bezier(' + ret + ')'}
}
}
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (supported values are: `linear`|`ease`|`ease-in`|`ease-out`|`ease-in-out`|`cubic-bezier(n,n,n,n)`)'
}
}
}
var TRANSFORM_VALIDATOR = function TRANSFORM_VALIDATOR (v) {
// TODO
return { value: v }
}
/**
* generate a function to check whether a value is in `list`
* - first value: default, could be removed
* - not in `list`: error
*
* @param {Array} list
* @return {function} a function(v) which returns a function to return
* - value: string|null
* - reason(k, v, result)
*/
function genEnumValidator(list) {
return function ENUM_VALIDATOR(v) {
var index = list.indexOf(v)
if (index > 0) {
return {value: v}
}
if (index === 0) {
return {
value: v,
reason: function reason(k, v, result) {
return 'NOTE: property value `' + v + '` is the DEFAULT value for `' + util.camelCaseToHyphened(k) + '` (could be removed)'
}
}
}
else {
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (supported values are: `' + list.join('`|`') + '`)'
}
}
}
}
}
function FLEW_WRAP_VALIDATOR (v) {
var list = ['nowrap', 'wrap', 'wrap-reverse']
var index = list.indexOf(v)
if (index > 0) {
return {
value: v,
reason: function reason(k, v, result) {
return 'NOTE: the ' + util.camelCaseToHyphened(k) + ' property may have compatibility problem on native'
}
}
}
if (index === 0) {
return {
value: v,
reason: function reason(k, v, result) {
return 'NOTE: property value `' + v + '` is the DEFAULT value for `' + util.camelCaseToHyphened(k) + '` (could be removed)'
}
}
}
else {
return {
value: null,
reason: function reason(k, v, result) {
return 'ERROR: property value `' + v + '` is not supported for `' + util.camelCaseToHyphened(k) + '` (supported values are: `' + list.join('`|`') + '`)'
}
}
}
}
var PROP_NAME_GROUPS = {
boxModel: {
display: genEnumValidator(['flex']),
width: LENGTH_VALIDATOR,
height: LENGTH_VALIDATOR,
overflow: genEnumValidator(['hidden']),
padding: SHORTHAND_LENGTH_VALIDATOR,
paddingLeft: LENGTH_VALIDATOR,
paddingRight: LENGTH_VALIDATOR,
paddingTop: LENGTH_VALIDATOR,
paddingBottom: LENGTH_VALIDATOR,
margin: SHORTHAND_LENGTH_VALIDATOR,
marginLeft: LENGTH_VALIDATOR,
marginRight: LENGTH_VALIDATOR,
marginTop: LENGTH_VALIDATOR,
marginBottom: LENGTH_VALIDATOR,
borderWidth: LENGTH_VALIDATOR,
borderLeftWidth: LENGTH_VALIDATOR,
borderTopWidth: LENGTH_VALIDATOR,
borderRightWidth: LENGTH_VALIDATOR,
borderBottomWidth: LENGTH_VALIDATOR,
borderColor: COLOR_VALIDATOR,
borderLeftColor: COLOR_VALIDATOR,
borderTopColor: COLOR_VALIDATOR,
borderRightColor: COLOR_VALIDATOR,
borderBottomColor: COLOR_VALIDATOR,
borderStyle: genEnumValidator(['dotted', 'dashed', 'solid']),
borderTopStyle: genEnumValidator(['dotted', 'dashed', 'solid']),
borderRightStyle: genEnumValidator(['dotted', 'dashed', 'solid']),
borderBottomStyle: genEnumValidator(['dotted', 'dashed', 'solid']),
borderLeftStyle: genEnumValidator(['dotted', 'dashed', 'solid']),
borderRadius: LENGTH_VALIDATOR,
borderBottomLeftRadius: LENGTH_VALIDATOR,
borderBottomRightRadius: LENGTH_VALIDATOR,
borderTopLeftRadius: LENGTH_VALIDATOR,
borderTopRightRadius: LENGTH_VALIDATOR
},
flexbox: {
flex: NUMBER_VALIDATOR,
flexWrap: FLEW_WRAP_VALIDATOR,
flexDirection: genEnumValidator(['column', 'row', 'column-reverse', 'row-reverse']),
justifyContent: genEnumValidator(['flex-start', 'flex-end', 'center', 'space-between','space-around']),
alignItems: genEnumValidator(['stretch', 'flex-start', 'flex-end', 'center'])
},
position: {
position: genEnumValidator(['relative', 'absolute', 'sticky', 'fixed']),
top: LENGTH_VALIDATOR,
bottom: LENGTH_VALIDATOR,
left: LENGTH_VALIDATOR,
right: LENGTH_VALIDATOR,
zIndex: INTEGER_VALIDATOR
},
common: {
opacity: NUMBER_VALIDATOR,
boxShadow: ANYTHING_VALIDATOR,
backgroundColor: COLOR_VALIDATOR,
backgroundImage: ANYTHING_VALIDATOR
},
text: {
lines: INTEGER_VALIDATOR,
color: COLOR_VALIDATOR,
fontSize: LENGTH_VALIDATOR,
fontStyle: genEnumValidator(['normal', 'italic']),
fontFamily: ANYTHING_VALIDATOR,
fontWeight: genEnumValidator(['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']),
textDecoration: genEnumValidator(['none', 'underline', 'line-through']),
textAlign: genEnumValidator(['left', 'center', 'right']),
textOverflow: genEnumValidator(['clip', 'ellipsis', 'unset', 'fade']),
lineHeight: LENGTH_VALIDATOR
},
transition: {
transitionProperty: TRANSITION_PROPERTY_VALIDATOR,
transitionDuration: TRANSITION_INTERVAL_VALIDATOR,
transitionDelay: TRANSITION_INTERVAL_VALIDATOR,
transitionTimingFunction: TRANSITION_TIMING_FUNCTION_VALIDATOR
},
transform: {
transform: TRANSFORM_VALIDATOR,
transformOrigin: TRANSFORM_VALIDATOR,// fixed by xxxxxx
},
customized: {
itemSize: LENGTH_VALIDATOR,
itemColor: COLOR_VALIDATOR,
itemSelectedColor: COLOR_VALIDATOR,
textColor: COLOR_VALIDATOR,
timeColor: COLOR_VALIDATOR,
textHighlightColor: COLOR_VALIDATOR
}
}
var SUGGESTED_PROP_NAME_GROUP = {
background: 'backgroundColor'
}
var validatorMap = {}
/**
* flatten `PROP_NAME_GROUPS` to `validatorMap`
*/
function genValidatorMap() {
var groupName, group, name
for (groupName in PROP_NAME_GROUPS) {
group = PROP_NAME_GROUPS[groupName]
for (name in group) {
validatorMap[name] = group[name]
}
}
}
genValidatorMap()
/**
* validate a CSS name/value pair
*
* @param {string} name camel cased
* @param {string} value
* @return {object}
* - value:string or null
* - log:{reason:string} or undefined
*/
function validate(name, value) {
var result, log
var validator = validatorMap[name]
if (typeof validator === 'function') {
if (typeof value !== 'function') {
result = validator(value)
}
/* istanbul ignore else */
else {
result = {value: value}
}
if (result.reason) {
log = {reason: result.reason(name, value, result.value)}
}
}
else {
// ensure number type, no `px`
/* istanbul ignore else */
if (typeof value !== 'function') {
var match = value.match(LENGTH_REGEXP)
if (match && (!match[1] || SUPPORT_CSS_UNIT.indexOf(match[1]) === -1)) {
value = parseFloat(value)
}
}
result = {value: value}
var suggestedName = SUGGESTED_PROP_NAME_GROUP[name]
var suggested = suggestedName ? ', suggest `' + util.camelCaseToHyphened(suggestedName) + '`' : ''
log = {reason: 'WARNING: `' + util.camelCaseToHyphened(name) + '` is not a standard property name (may not be supported)' + suggested}
}
return {
value: result.value,
log: log
}
}
module.exports = {
BASIC_COLOR_KEYWORDS: BASIC_COLOR_KEYWORDS,
EXTENDED_COLOR_KEYWORDS: EXTENDED_COLOR_KEYWORDS,
LENGTH_VALIDATOR: LENGTH_VALIDATOR,
COLOR_VALIDATOR: COLOR_VALIDATOR,
NUMBER_VALIDATOR: NUMBER_VALIDATOR,
INTEGER_VALIDATOR: INTEGER_VALIDATOR,
genEnumValidator: genEnumValidator,
TRANSITION_PROPERTY_VALIDATOR: TRANSITION_PROPERTY_VALIDATOR,
TRANSITION_DURATION_VALIDATOR: TRANSITION_INTERVAL_VALIDATOR,
TRANSITION_DELAY_VALIDATOR: TRANSITION_INTERVAL_VALIDATOR,
TRANSITION_TIMING_FUNCTION_VALIDATOR: TRANSITION_TIMING_FUNCTION_VALIDATOR,
PROP_NAME_GROUPS: PROP_NAME_GROUPS,
map: validatorMap,
validate: validate
}
@@ -0,0 +1,67 @@
{
"_args": [
[
"weex-styler@0.3.1",
"/Users/fxy/Documents/DCloud/HBuilderX/uniapp-cli"
]
],
"_development": true,
"_from": "weex-styler@0.3.1",
"_id": "weex-styler@0.3.1",
"_inBundle": false,
"_integrity": "sha512-xkX5/wS/QLiJXKwbdpeytbLN0kHviQwj9CLdvBxqu+RRZABZpTniKZr1oxjh9Q0+n/aRC+smwFpQpUKvXh9V1g==",
"_location": "/weex-styler",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "weex-styler@0.3.1",
"name": "weex-styler",
"escapedName": "weex-styler",
"rawSpec": "0.3.1",
"saveSpec": null,
"fetchSpec": "0.3.1"
},
"_requiredBy": [
"/weex-vue-loader"
],
"_resolved": "https://registry.npmjs.org/weex-styler/-/weex-styler-0.3.1.tgz",
"_spec": "0.3.1",
"_where": "/Users/fxy/Documents/DCloud/HBuilderX/uniapp-cli",
"author": {
"name": "songsiqi",
"email": "songsiqi2006@126.com"
},
"bugs": {
"url": "https://github.com/weexteam/weex-styler/issues"
},
"dependencies": {
"css": "~2.2.1"
},
"description": "Weex <style> transformer",
"devDependencies": {
"chai": "~3.4.1",
"gulp": "~3.9.0",
"gulp-jscs": "~3.0.2",
"gulp-mocha": "~2.2.0",
"isparta": "~4.0.0",
"sinon": "~1.17.2",
"sinon-chai": "~2.8.0"
},
"homepage": "https://github.com/weexteam/weex-styler#readme",
"keywords": [
"weex"
],
"license": "MIT",
"main": "index.js",
"name": "weex-styler",
"repository": {
"type": "git",
"url": "git+https://github.com/weexteam/weex-styler.git"
},
"scripts": {
"cover": "node node_modules/isparta/bin/isparta cover node_modules/gulp-mocha/node_modules/.bin/_mocha -- --reporter dot",
"test": "gulp test && npm run cover"
},
"version": "0.3.1"
}
@@ -0,0 +1,289 @@
var chai = require('chai')
var sinon = require('sinon')
var sinonChai = require('sinon-chai')
var expect = chai.expect
chai.use(sinonChai)
var styler = require('../')
describe('parse', function () {
it('parse normal style code', function (done) {
var code = 'html {color: #000000;}\n\n.foo {color: red; background-color: rgba(255,255,255,0.6); -webkit-transform: rotate(90deg); width: 200px; left: 0; right: 0px; border-width: 1pt; font-weight: 100}\n\n.bar {background: red}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {color: '#FF0000', backgroundColor: 'rgba(255,255,255,0.6)', WebkitTransform: 'rotate(90deg)', width: '200px', left: 0, right: '0px', borderWidth: '1pt', fontWeight: '100'}, bar: {background: 'red'}})
expect(data.log).eql([
{line: 1, column: 1, reason: 'ERROR: Selector `html` is not supported. Weex only support single-classname selector'},
{line: 3, column: 7, reason: 'NOTE: property value `red` is autofixed to `#FF0000`'},
{line: 3, column: 60, reason: 'WARNING: `-webkit-transform` is not a standard property name (may not be supported)'},
{line: 5, column: 7, reason: 'WARNING: `background` is not a standard property name (may not be supported), suggest `background-color`'}
])
done()
})
})
it('parse and fix prop value', function (done) {
var code = '.foo {font-size: 200px;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {fontSize: '200px'}})
done()
})
})
it('parse and ensure number type value', function (done) {
var code = '.foo {line-height: 40;}\n\n .bar {line-height: 20px;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {lineHeight: 40}, bar: {lineHeight: '20px'}})
done()
})
})
it('handle complex class definition', function (done) {
var code = '.foo, .bar {font-size: 20;}\n\n .foo {color: #ff5000;}\n\n .bar {color: #000000;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {fontSize: 20, color: '#ff5000'},
bar: {fontSize: 20, color: '#000000'}
})
done()
})
})
it('handle more complex class definition', function (done) {
var code = '.foo, .bar {font-size: 20; color: #000000}\n\n .foo, .bar, .baz {color: #ff5000; height: 30;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {fontSize: 20, color: '#ff5000', height: 30},
bar: {fontSize: 20, color: '#ff5000', height: 30},
baz: {color: '#ff5000', height: 30}
})
done()
})
})
it('parse transition', function (done) {
var code = '.foo {transition-property: margin-top; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({foo: {property: 'marginTop', duration: 300, delay: 200, timingFunction: 'ease-in'}})
expect(data.jsonStyle.foo).eql({
transitionDelay: 200,
transitionDuration: 300,
transitionProperty: "marginTop",
transitionTimingFunction: "ease-in"
})
expect(data.log).eql([
{line: 1, column: 40, reason: 'NOTE: property value `300ms` is autofixed to `300`'},
{line: 1, column: 68, reason: 'NOTE: property value `0.2s` is autofixed to `200`'}
])
done()
})
})
it('parse transition transform', function (done) {
var code = '.foo {transition-property: transform; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in-out;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({foo: {property: 'transform', duration: 300, delay: 200, timingFunction: 'ease-in-out'}})
expect(data.jsonStyle.foo).eql({
transitionDelay: 200,
transitionDuration: 300,
transitionProperty: "transform",
transitionTimingFunction: "ease-in-out"
})
done()
})
})
it('parse multi transition properties', function (done) {
var code = '.foo {transition-property: margin-top, height; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in-out;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({foo: {property: 'marginTop,height', duration: 300, delay: 200, timingFunction: 'ease-in-out'}})
expect(data.jsonStyle.foo).eql({
transitionDelay: 200,
transitionDuration: 300,
transitionProperty: "marginTop,height",
transitionTimingFunction: "ease-in-out"
})
done()
})
})
it('parse complex transition', function (done) {
var code = '.foo {font-size: 20; color: #000000}\n\n .foo, .bar {color: #ff5000; height: 30; transition-property: margin-top; transition-duration: 300ms; transition-delay: 0.2s; transition-timing-function: ease-in;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({
foo: {property: 'marginTop', duration: 300, delay: 200, timingFunction: 'ease-in'},
bar: {property: 'marginTop', duration: 300, delay: 200, timingFunction: 'ease-in'}
})
expect(data.jsonStyle.foo).eql({
fontSize: 20, color: '#ff5000', height: 30,
transitionDelay: 200,
transitionDuration: 300,
transitionProperty: "marginTop",
transitionTimingFunction: "ease-in"
})
expect(data.jsonStyle.bar).eql({
color: '#ff5000', height: 30,
transitionDelay: 200,
transitionDuration: 300,
transitionProperty: "marginTop",
transitionTimingFunction: "ease-in"
})
expect(data.log).eql([
{line: 3, column: 75, reason: 'NOTE: property value `300ms` is autofixed to `300`'},
{line: 3, column: 103, reason: 'NOTE: property value `0.2s` is autofixed to `200`'}
])
done()
})
})
it('parse transition shorthand', function (done) {
var code = '.foo {font-size: 20; transition: margin-top 500ms ease-in-out 1s}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({foo: {property: 'marginTop', duration: 500, delay: 1000, timingFunction: 'ease-in-out' }})
expect(data.jsonStyle.foo).eql({
fontSize: 20,
transitionDelay: 1000,
transitionDuration: 500,
transitionProperty: "marginTop",
transitionTimingFunction: "ease-in-out"
})
expect(data.log).eql([
{line: 1, column: 22, reason: 'NOTE: property value `500ms` is autofixed to `500`'},
{line: 1, column: 22, reason: 'NOTE: property value `1s` is autofixed to `1000`'}
])
done()
})
})
it.skip('override transition shorthand', function (done) {
var code = '.foo {font-size: 32px; transition: margin-top 500ms ease-in-out 1s; transition-duration: 300ms}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle['@TRANSITION']).eql({foo: {property: 'marginTop', duration: 300, delay: 1000, timingFunction: 'ease-in-out' }})
expect(data.jsonStyle.foo).eql({
fontSize: 32,
transitionDelay: 1000,
transitionDuration: 300,
transitionProperty: "marginTop",
transitionTimingFunction: "ease-in-out"
})
done()
})
})
it('parse padding & margin shorthand', function (done) {
var code = '.foo { padding: 20px; margin: 30px 40; } .bar { margin: 10px 20 30; padding: 10 20px 30px 40;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle.foo).eql({
paddingTop: '20px',
paddingRight: '20px',
paddingBottom: '20px',
paddingLeft: '20px',
marginTop: '30px',
marginRight: 40,
marginBottom: '30px',
marginLeft: 40
})
expect(data.jsonStyle.bar).eql({
paddingTop: 10,
paddingRight: '20px',
paddingBottom: '30px',
paddingLeft: 40,
marginTop: '10px',
marginRight: 20,
marginBottom: 30,
marginLeft: 20
})
done()
})
})
it('override padding & margin shorthand', function (done) {
var code = '.foo { padding: 20px; padding-left: 30px; } .bar { margin: 10px 20; margin-bottom: 30px;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle.foo).eql({
paddingTop: '20px',
paddingRight: '20px',
paddingBottom: '20px',
paddingLeft: '30px'
})
expect(data.jsonStyle.bar).eql({
marginTop: '10px',
marginRight: 20,
marginBottom: '30px',
marginLeft: 20
})
done()
})
})
it('handle pseudo class', function (done) {
var code = '.class-a {color: #0000ff;} .class-a:last-child:focus {color: #ff0000;}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
'class-a': {
color: '#0000ff',
'color:last-child:focus': '#ff0000'
}
})
done()
})
})
it('handle iconfont', function (done) {
var code = '@font-face {font-family: "font-family-name-1"; src: url("font file url 1-1") format("truetype");} @font-face {font-family: "font-family-name-2"; src: url("font file url 2-1") format("truetype"), url("font file url 2-2") format("woff");}'
styler.parse(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
'@FONT-FACE': [
{fontFamily: 'font-family-name-1', src: 'url("font file url 1-1") format("truetype")'},
{fontFamily: 'font-family-name-2', src: 'url("font file url 2-1") format("truetype"), url("font file url 2-2") format("woff")'}
]
})
done()
})
})
it('handle syntax error', function (done) {
var code = 'asdf'
styler.parse(code, function (err, data) {
expect(err).is.an.array
expect(err[0].toString()).eql('Error: undefined:1:5: missing \'{\'')
expect(err[0].reason).eql('missing \'{\'')
expect(err[0].filename).eql(undefined)
expect(err[0].line).eql(1)
expect(err[0].column).eql(5)
expect(err[0].source).eql('')
expect(data.log).eql([{line: 1, column: 5, reason: 'ERROR: undefined:1:5: missing \'{\''}])
done()
})
})
})
@@ -0,0 +1,238 @@
var chai = require('chai')
var sinon = require('sinon')
var sinonChai = require('sinon-chai')
var expect = chai.expect
chai.use(sinonChai)
var shorthandParser = require('../lib/shorthand-parser')
describe('shorthand-parser', function () {
it('parse transition', function () {
var declarations = [
{
type: 'declaration',
property: 'transition',
value: 'margin-top 500ms ease-in-out 1s',
position: {}
}
]
var result = shorthandParser(declarations)
expect(result).eql([
{
type: 'declaration',
property: 'transition-property',
value: 'margin-top',
position: {}
},
{
type: 'declaration',
property: 'transition-duration',
value: '500ms',
position: {}
},
{
type: 'declaration',
property: 'transition-timing-function',
value: 'ease-in-out',
position: {}
},
{
type: 'declaration',
property: 'transition-delay',
value: '1s',
position: {}
}
])
expect(shorthandParser([{
type: 'declaration',
property: 'transition',
value: 'width 2s ease-in-out, height 1s 1s, top cubic-bezier(0.1, 0.7, 1.0, 0.1)',
position: {}
}])).eql([{
type: 'declaration',
property: 'transition-property',
value: 'width, height, top',
position: {}
},
{
type: 'declaration',
property: 'transition-duration',
value: '2s, 1s, 0s',
position: {}
},
{
type: 'declaration',
property: 'transition-timing-function',
value: 'ease-in-out, ease, cubic-bezier(0.1, 0.7, 1.0, 0.1)',
position: {}
},
{
type: 'declaration',
property: 'transition-delay',
value: '0s, 1s, 0s',
position: {}
}])
expect(shorthandParser([{
type: 'declaration',
property: 'transition',
value: 'width 2s, height 2s',
position: {}
}])).eql([{
type: 'declaration',
property: 'transition-property',
value: 'width, height',
position: {}
},
{
type: 'declaration',
property: 'transition-duration',
value: '2s',
position: {}
},
{
type: 'declaration',
property: 'transition-timing-function',
value: 'ease',
position: {}
},
{
type: 'declaration',
property: 'transition-delay',
value: '0s',
position: {}
}])
})
it('parse margin', function () {
var declarations = [
{
type: 'declaration',
property: 'margin',
value: '1px',
position: {}
},
{
type: 'declaration',
property: 'margin',
value: '21px 22px',
position: {}
},
{
type: 'declaration',
property: 'margin',
value: '31px 32px 33px',
position: {}
},
{
type: 'declaration',
property: 'margin',
value: '41px 42px 43px 44px',
position: {}
}
]
var result = shorthandParser(declarations)
expect(result).eql([
{
type: 'declaration',
property: 'margin-top',
value: '1px',
position: {}
},
{
type: 'declaration',
property: 'margin-right',
value: '1px',
position: {}
},
{
type: 'declaration',
property: 'margin-bottom',
value: '1px',
position: {}
},
{
type: 'declaration',
property: 'margin-left',
value: '1px',
position: {}
},
{
type: 'declaration',
property: 'margin-top',
value: '21px',
position: {}
},
{
type: 'declaration',
property: 'margin-right',
value: '22px',
position: {}
},
{
type: 'declaration',
property: 'margin-bottom',
value: '21px',
position: {}
},
{
type: 'declaration',
property: 'margin-left',
value: '22px',
position: {}
},
{
type: 'declaration',
property: 'margin-top',
value: '31px',
position: {}
},
{
type: 'declaration',
property: 'margin-right',
value: '32px',
position: {}
},
{
type: 'declaration',
property: 'margin-bottom',
value: '33px',
position: {}
},
{
type: 'declaration',
property: 'margin-left',
value: '32px',
position: {}
},
{
type: 'declaration',
property: 'margin-top',
value: '41px',
position: {}
},
{
type: 'declaration',
property: 'margin-right',
value: '42px',
position: {}
},
{
type: 'declaration',
property: 'margin-bottom',
value: '43px',
position: {}
},
{
type: 'declaration',
property: 'margin-left',
value: '44px',
position: {}
}
])
})
})
@@ -0,0 +1,445 @@
var chai = require('chai')
var sinon = require('sinon')
var sinonChai = require('sinon-chai')
var expect = chai.expect
chai.use(sinonChai)
var styler = require('../')
describe('validate', function () {
it('parse normal style code', function (done) {
var code = {
foo: {
color: '#FF0000',
width: '200',
position: 'sticky',
zIndex: 4
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {color: '#FF0000', width: 200, position: 'sticky', zIndex: 4}})
expect(data.log).eql([])
done()
})
})
it('parse length', function (done) {
var code = {
foo: {
width: '200px',
paddingLeft: '300',
borderWidth: '1pt',
left: '0',
right: '0px',
marginRight: 'asdf'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {
width: '200px',
paddingLeft: 300,
borderWidth: '1pt',
left: 0,
right: '0px'
}})
expect(data.log).eql([
{reason: 'ERROR: property value `asdf` is not supported for `margin-right` (only number and pixel values are supported)'}
])
done()
})
})
it('parse number', function (done) {
var code = {
foo: {
opacity: '1'
},
bar: {
opacity: '0.5'
},
baz: {
opacity: 'a'
},
boo: {
opacity: '0.5a'
},
zero: {
opacity: '0'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
opacity: 1
},
bar: {
opacity: 0.5
},
baz: {},
boo: {},
zero: {
opacity: 0
}
})
expect(data.log).eql([
{reason: 'ERROR: property value `a` is not supported for `opacity` (only number is supported)'},
{reason: 'ERROR: property value `0.5a` is not supported for `opacity` (only number is supported)'}
])
done()
})
})
it('parse integer', function (done) {
var code = {
foo: {
zIndex: '1'
},
bar: {
zIndex: '0.5'
},
baz: {
zIndex: 'a'
},
boo: {
zIndex: '0.5a'
},
zero: {
zIndex: '0'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
zIndex: 1
},
bar: {},
baz: {},
boo: {},
zero: {
zIndex: 0
}
})
expect(data.log).eql([
{reason: 'ERROR: property value `0.5` is not supported for `z-index` (only integer is supported)'},
{reason: 'ERROR: property value `a` is not supported for `z-index` (only integer is supported)'},
{reason: 'ERROR: property value `0.5a` is not supported for `z-index` (only integer is supported)'}
])
done()
})
})
it('parse color', function (done) {
var code = {
foo: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
bar: {
color: '#F00',
backgroundColor: '#f00'
},
baz: {
color: 'red',
backgroundColor: 'lightpink'
},
rgba: {
color: 'rgb(23, 0, 255)',
backgroundColor: 'rgba(234, 45, 99, .4)'
},
transparent: {
color: 'transparent',
backgroundColor: 'asdf'
},
errRgba: {
color: 'rgb(266,0,255)',
backgroundColor: 'rgba(234,45,99,1.3)'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
bar: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
baz: {
color: '#FF0000',
backgroundColor: '#FFB6C1'
},
rgba: {
color: 'rgb(23,0,255)',
backgroundColor: 'rgba(234,45,99,0.4)'
},
transparent: {
color: 'rgba(0,0,0,0)'
},
errRgba: {}
})
expect(data.log).eql([
{reason: 'NOTE: property value `#F00` is autofixed to `#FF0000`'},
{reason: 'NOTE: property value `#f00` is autofixed to `#ff0000`'},
{reason: 'NOTE: property value `red` is autofixed to `#FF0000`'},
{reason: 'NOTE: property value `lightpink` is autofixed to `#FFB6C1`'},
{reason: 'ERROR: property value `asdf` is not valid for `background-color`'},
{reason: 'ERROR: property value `rgb(266,0,255)` is not valid for `color`'},
{reason: 'ERROR: property value `rgba(234,45,99,1.3)` is not valid for `background-color`'}
])
done()
})
})
it('parse color', function (done) {
var code = {
foo: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
bar: {
color: '#F00',
backgroundColor: '#f00'
},
baz: {
color: 'red',
backgroundColor: 'lightpink'
},
rgba: {
color: 'rgb(23, 0, 255)',
backgroundColor: 'rgba(234, 45, 99, .4)'
},
transparent: {
color: 'transparent',
backgroundColor: 'asdf'
},
errRgba: {
color: 'rgb(266,0,255)',
backgroundColor: 'rgba(234,45,99,1.3)'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
bar: {
color: '#FF0000',
backgroundColor: '#ff0000'
},
baz: {
color: '#FF0000',
backgroundColor: '#FFB6C1'
},
rgba: {
color: 'rgb(23,0,255)',
backgroundColor: 'rgba(234,45,99,0.4)'
},
transparent: {
color: 'rgba(0,0,0,0)'
},
errRgba: {}
})
expect(data.log).eql([
{reason: 'NOTE: property value `#F00` is autofixed to `#FF0000`'},
{reason: 'NOTE: property value `#f00` is autofixed to `#ff0000`'},
{reason: 'NOTE: property value `red` is autofixed to `#FF0000`'},
{reason: 'NOTE: property value `lightpink` is autofixed to `#FFB6C1`'},
{reason: 'ERROR: property value `asdf` is not valid for `background-color`'},
{reason: 'ERROR: property value `rgb(266,0,255)` is not valid for `color`'},
{reason: 'ERROR: property value `rgba(234,45,99,1.3)` is not valid for `background-color`'}
])
done()
})
})
it('parse flex-wrap', function (done) {
var code = {
foo: { flexWrap: 'nowrap' },
bar: { flexWrap: 'wrap' }
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: { flexWrap: 'nowrap' },
bar: { flexWrap: 'wrap' }
})
expect(data.log).eql([
{reason: 'NOTE: property value `nowrap` is the DEFAULT value for `flex-wrap` (could be removed)'},
{reason: 'NOTE: the flex-wrap property may have compatibility problem on native'},
])
done()
})
})
it('parse transition-property', function (done) {
var code = {
foo: {
transitionProperty: 'margin-top'
},
bar: {
transitionProperty: 'height'
},
foobar: {
transitionProperty: 'margin-top, height'
},
baz: {
transitionProperty: 'abc'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
transitionProperty: 'marginTop'
},
bar: {
transitionProperty: 'height'
},
foobar: {
transitionProperty: 'marginTop,height'
},
baz: {}
})
expect(data.log).eql([
{reason: 'ERROR: property value `abc` is not supported for `transition-property` (only css property is valid)'}
])
done()
})
})
it('parse transition-duration & transition-delay', function (done) {
var code = {
foo: {
transitionDuration: '200ms',
transitionDelay: '0.5s'
},
bar: {
transitionDuration: '200',
transitionDelay: 'abc'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
transitionDuration: 200,
transitionDelay: 500
},
bar: {
transitionDuration: 200
}
})
expect(data.log).eql([
{reason: 'NOTE: property value `200ms` is autofixed to `200`'},
{reason: 'NOTE: property value `0.5s` is autofixed to `500`'},
{reason: 'ERROR: property value `abc` is not supported for `transition-delay` (only number of seconds and milliseconds is valid)'}
])
done()
})
})
it('parse transition-timing-function', function (done) {
var code = {
foo: {
transitionTimingFunction: 'ease-in-out'
},
bar: {
transitionTimingFunction: 'cubic-bezier(.88, 1.0, -0.67, 1.37)'
},
baz: {
transitionTimingFunction: 'abc'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
transitionTimingFunction: 'ease-in-out'
},
bar: {
transitionTimingFunction: 'cubic-bezier(0.88,1,-0.67,1.37)'
},
baz: {}
})
expect(data.log).eql([
{reason: 'ERROR: property value `abc` is not supported for `transition-timing-function` (supported values are: `linear`|`ease`|`ease-in`|`ease-out`|`ease-in-out`|`cubic-bezier(n,n,n,n)`)'}
])
done()
})
})
it('parse unknown', function (done) {
var code = {
foo: {
background: '#ff0000',
abc: '123',
def: '456px',
ghi: '789pt',
AbcDef: '456',
abcDef: 'abc'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({
foo: {
background: '#ff0000',
abc: 123,
def: '456px',
ghi: '789pt',
AbcDef: 456,
abcDef: 'abc'
}
})
expect(data.log).eql([
{reason: 'WARNING: `background` is not a standard property name (may not be supported), suggest `background-color`'},
{reason: 'WARNING: `abc` is not a standard property name (may not be supported)'},
{reason: 'WARNING: `def` is not a standard property name (may not be supported)'},
{reason: 'WARNING: `ghi` is not a standard property name (may not be supported)'},
{reason: 'WARNING: `-abc-def` is not a standard property name (may not be supported)'},
{reason: 'WARNING: `abc-def` is not a standard property name (may not be supported)'}
])
done()
})
})
it('parse complex style code', function (done) {
var code = {
foo: {
color: 'red',
WebkitTransform: 'rotate(90deg)',
width: '200px'
}
}
styler.validate(code, function (err, data) {
expect(err).is.undefined
expect(data).is.an.object
expect(data.jsonStyle).eql({foo: {color: '#FF0000', WebkitTransform: 'rotate(90deg)', width: '200px'}})
expect(data.log).eql([
{reason: 'NOTE: property value `red` is autofixed to `#FF0000`'},
{reason: 'WARNING: `-webkit-transform` is not a standard property name (may not be supported)'}
])
done()
})
})
})