falcon-styler
Version:
Falcon <style> transformer
274 lines (246 loc) • 9.51 kB
JavaScript
var css = require('css')
var util = require('./lib/util')
var validateItem = require('./lib/validator').validate
var shorthandParser = require('./lib/shorthand-parser')
// padding & margin shorthand parsing
function convertLengthShorthand(rule, prop) {
for (var i = 0; i < rule.declarations.length; i++) {
var declaration = rule.declarations[i]
if (declaration.property === prop) {
var values = declaration.value.split(/\s+/)
// values[0] = values[0] || 0
values[1] = values[1] || values[0]
values[2] = values[2] || values[0]
values[3] = values[3] || values[1]
rule.declarations.splice(i, 1)
rule.declarations.splice(i, 0, { type: 'declaration', property: prop + '-left', value: values[3], position: declaration.position })
rule.declarations.splice(i, 0, { type: 'declaration', property: prop + '-bottom', value: values[2], position: declaration.position })
rule.declarations.splice(i, 0, { type: 'declaration', property: prop + '-right', value: values[1], position: declaration.position })
rule.declarations.splice(i, 0, { type: 'declaration', property: prop + '-top', value: values[0], position: declaration.position })
// break
}
}
}
/**
* 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) {
var type = rule.type
var ruleResult = {}
var ruleLog = []
if (type === 'rule') {
if (rule.declarations && rule.declarations.length) {
rule.declarations = shorthandParser(rule.declarations)
// padding & margin shorthand parsing
convertLengthShorthand(rule, 'padding')
convertLengthShorthand(rule, 'margin')
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
// 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') {
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) {
if (selector.match(/^\.[A-Za-z0-9_\-:]+$/)) {
var className = selector.slice(1)
// handle pseudo class
var pseudoIndex = className.indexOf(':')
if (pseudoIndex > -1) {
var pseudoCls = className.slice(pseudoIndex)
className = className.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)
jsonStyle['@TRANSITION'] = jsonStyle['@TRANSITION'] || {}
jsonStyle['@TRANSITION'][className] = jsonStyle['@TRANSITION'][className] || {}
jsonStyle['@TRANSITION'][className][realProp] = ruleResult[prop]
}
jsonStyle[className] = jsonStyle[className] || {}
jsonStyle[className][prop] = ruleResult[prop]
})
}
else {
log.push({
line: rule.position.start.line,
column: rule.position.start.column,
reason: 'ERROR: Selector `' + selector + '` is not supported. Weex only support single-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)
}
} else if (type === 'keyframes') {
//将内容转成格式:
/*
{
"keyframmove": {
"left": [
{ "p": 0, "v": "0" },
{ "p": 0.2, "v": "100px" },
{ "p": 1, "v": "200px" }
],
"opacity": [
{ "p": 0, "v": "0.1" },
{ "p": 0.2, "v": "0.6" },
{ "p": 1, "v": "1" }
],
"top": [
{ "p": 0, "v": "0px" },
{ "p": 0.2, "v": "200px" },
{ "p": 1, "v": "300px" }
]
}
}
*/
if (rule.name && rule.keyframes && rule.keyframes.length) {
const name = rule.name;
jsonStyle["@KEYFRAME"] = jsonStyle["@KEYFRAME"] || {};
const result = jsonStyle["@KEYFRAME"][name] = {};
rule.keyframes.forEach((keyframe) => {
if (keyframe.type == "keyframe") {
const values = keyframe.values.map(v => {
if (v === 'from') { return 0; }
if (v === 'to') { return 1; }
return (parseInt(v) || 0) / 100;
});
if (keyframe.declarations && keyframe.declarations.length) {
keyframe.declarations.forEach((declaration) => {
const camelCasedName = util.hyphenedToCamelCase(declaration.property);
const subResult = validateItem(camelCasedName, declaration.value);
const value = subResult.value;
if (subResult.log) {
subResult.log.line = declaration.position.start.line
subResult.log.column = declaration.position.start.column
ruleLog.push(subResult.log)
}
if (camelCasedName && typeof value === 'number' || typeof value === 'string') {
result[camelCasedName] = result[camelCasedName] || [];
values.forEach((p) => {
result[camelCasedName].push({ p: p, v: value })
})
}
})
}
}
});
}
}
})
}
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
}