cssobj-converter
Version:
Convert from normal css/LESS/SASS/SCSS to cssobj
311 lines (268 loc) • 7.88 kB
JavaScript
var cssobjPluginGencss = require('cssobj-plugin-gencss')
var cssobjCore = require('cssobj-core')
var postcss = require('postcss')
var selectorParser = require('postcss-selector-parser')
var scss = require('postcss-scss')
var less = require('postcss-less')
var util = require('util')
var src = 'h1#ioj.app{font-size:12px;color:blue;}\n@media(max-width: 800px){color:purple; p{color:red;}}'
var reOneRule = /^(?:charset|import|namespace)/
var reGroupRule = /^(?:media|document|supports|page|keyframes)/
// for old node(0.10), using \\ instead of \\\\
// var backSlash = util.inspect({'\\_':1}).length===12 ? '\\' : '\\\\'
function camelCase (input) {
// skip --color props
if(/^-{2,}/.test(input)) return input
// make -ms-prop result in msProp
if(input.indexOf('-ms-')===0) input = input.slice(1)
return input.toLowerCase().replace(/-(.)/g, function (match, char) {
return char.toUpperCase()
})
}
function parseMixin(sel) {
var match = sel && sel.match(/^\s*([\.\#a-z0-9\&_-]+)\s*\((.*)\)\s*$/i)
if(match) {
return [match[1], match[2].split(/\s*[;,]\s*/g)]
}
}
function parseExtend(sel) {
var match = sel && sel.match(/^\s*([^@]+)\s*:extend\((.*)\)\s*$/i)
if(match){
return [match[1], match[2]]
}
}
function joinLines(str) {
return str.split(/[\n\r]/).map(function(s) {
return s.trim()
}).join(' ')
}
function strToObj(str) {
if(/^\s*{/.test(str)) str = 'exports='+str
var ret = new Function('exports', str + ';return exports')({})
if(ret.__esModule) ret = ret.default
return ret
}
var syntax = {
'scss': scss,
'less': less,
'css': ''
}
function convertObj (src, format, option) {
option = option||{}
if(format=='js') {
var cssobj = cssobjCore({plugins:[
cssobjPluginGencss(option)
]})
var ret = cssobj(src && typeof src==='object' ? src : strToObj(src))
return ret.css
}
var store = {}
var curObj = null
try {
var ast = postcss([]).process(src, { parser: syntax[format] }).result.root
// var ast = postcss.parse(src).toResult().root
}catch(e) {
console.log('parse error', e)
return e
}
if(!ast) return {}
// get class, id selector names
var nameStore = {
classes: {},
ids: {}
}
const extractNames = selectors => {
selectors.walkClasses((selector) => {
// do something with the selector
// console.log('class:', String(selector))
const name = String(selector).slice(1)
if(/[\(\[,\s\>\~\+]/.test(name)) return
nameStore.classes[name] = String(name)
})
selectors.walkIds((selector) => {
// do something with the selector
// console.log('id:', String(selector))
const name = String(selector).slice(1)
nameStore.ids[name] = String(name)
})
}
var selectorProcessor = selectorParser(extractNames)
const selectorPlugin = postcss.plugin('extractNames', (options) => {
return (root) => {
root.walkRules(rule => {
selectorProcessor.processSync(rule);
});
};
})
var name = function (v) {
// if(v.selector) {
// console.log(v.selector)
// }
if (v.type == 'atrule') return util.format(
'@%s %s',
v.name,
joinLines(v.params)
)
if (v.type == 'rule') return joinLines(v.selector)
/* no need since cssobj-core 1.1.1
.replace( /&/g,
// how to deal with & ?
// stylus using \& to escape, SCSS/LESS replace only in selector
format==='less'
? '&'
: backSlash + '&'
)
*/
}
var getObj = function (v) {
var n = store, path = []
while(v.parent && v.parent.type != 'root'){
path.unshift(name(v.parent))
v = v.parent
}
for (var i = 0; i < path.length; i++) {
n = n[path[i]]
}
return n
}
ast.walk(function(v) {
var obj = v.parent && v.parent.obj || store
switch (v.type) {
case 'atrule':
if(reOneRule.test(v.name)) {
arrayKV(obj, '@'+v.name, v.params)
} else {
var sel = name(v)
var body = {}
if(Array.isArray(obj)){
obj.push({[sel]: body})
} else if (sel in obj) {
arrayKV(obj, sel, body)
} else {
obj[sel] = body
}
// if (Array.isArray(obj)) obj.push({[key]: body})
// else if(!(key in obj)) obj[key] = body
// else body = obj[key]
v.obj = body
}
break
case 'rule':
if(format=='less') {
var arrExt = parseExtend(v.selector)
var arrMix = parseMixin(v.selector)
if(arrExt) v.selector = arrExt[0]
}
selectorProcessor.processSync(v.selector)
var sel = name(v)
var body = {}
// it's LESS :extend / mixin
if(v.ruleWithoutBody) {
// console.log(sel, parseExtend(sel), parseMixin(sel))
if(v.extendRule) {
// combine & selector into parent
if(arrExt[0]=='&') {
sel = '$extend'
body = arrExt[1]||''
} else {
sel = arrExt[0]
body.$extend = arrExt[1]||''
}
} else if (arrMix) {
sel = '$mixin'
body[arrMix[0]] = arrMix[1].map(function(value) {
return Number(value)==value ? Number(value) : value
})
}
} else {
// mixin & extend in selector with prop
if(arrMix) {
// fix $vars order poblem
body.$vars = {}
}
if(arrExt) {
body.$extend = arrExt[1]||''
}
}
if(Array.isArray(obj)){
obj.push({[sel]: body})
} else if (sel in obj) {
arrayKV(obj, sel, body)
} else {
obj[sel] = body
}
v.obj = body
break
case 'decl':
// put back IE hacks from v.raws
var prop = ''
var important = v.important ? ' !important' : ''
var value = v.value + important
if(!obj) return
// if(obj && obj.constructor == Array) obj = obj[obj.length-1]
// css hacks stored in v.raws.before
var prefix = v.raws.before.match(/[*_]+$/)
if(prefix) prop += prefix.pop()
if(Number(value)==value) value = Number(value)
// remove prefix prop version from result
if(!option.keepVendor) {
!['-webkit-', '-moz-', 'ms-', '-o-'].forEach(function(vendor) {
var p = camelCase(vendor + v.prop)
if(p in obj && value==obj[p]) {
delete obj[p]
}
})
}
// @prop don't camelcase
prop += /^\s*@/i.test(v.prop) ? v.prop : camelCase(v.prop)
if(prop[0]=='@' && !prop[0].match(/^\s*@(import|namespace|charset)\b/)) {
obj['$vars'] = obj['$vars'] || {}
obj['$vars'][prop.slice(1)] = value
return
}
if(prop in obj) {
arrayKV(obj, prop, value)
} else {
obj[prop] = value
}
}
})
var obj = transformMixin(store)
const {classes, ids} = nameStore
if(option.reactNative) {
const newObj = {}
for(let k in obj) {
newObj[k.replace(/^\./, '')] = obj[k]
}
obj = newObj
}
return option.nameStore ? {
obj,
classes,
ids
} : obj
}
function transformMixin(obj) {
var $mixins = {}
for(var k in obj) {
var arr = parseMixin(k)
if(arr) {
obj[k].$vars = obj[k].$vars || {}
arr[1].forEach(function(v) {
v = v.split(/[:=]/)
obj[k].$vars[v[0].replace(/^\s*@/,'')] = (v[1]||'').trim()
})
$mixins[arr[0]] = obj[k]
delete obj[k]
}
}
if(Object.keys($mixins).length) obj.$mixins = $mixins
return obj
}
function arrayKV (obj, k, v, reverse, unique) {
obj[k] = k in obj ? [].concat(obj[k]) : []
if(unique && obj[k].indexOf(v)>-1) return
reverse ? obj[k].unshift(v) : obj[k].push(v)
}
module.exports = convertObj
// console.log( convertObj(src) )