UNPKG

cssobj-core

Version:

Generate Virtual CSS middle format, support for all cssobj functions and plugins.

540 lines (445 loc) 15.3 kB
/** cssobj-core 1.1.8 Fri Mar 16 2018 16:19:39 GMT+0800 (CST) commit 319d94d9d6c0ee455ed0dfe0c7f796298a145250 IE ES3 need below polyfills: * Array.prototype.forEach * Array.prototype.indexOf * Array.prototype.map * Array.prototype.some * Array.prototype.reduce * Object.keys **/ 'use strict'; // helper functions for cssobj // check n is numeric, or string of numeric function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n) } function own(o, k) { return {}.hasOwnProperty.call(o, k) } // set default option (not deeply) function defaults(options, defaultOption) { options = options || {}; for (var i in defaultOption) { if (own(defaultOption, i) && !(i in options)) options[i] = defaultOption[i]; } return options } // Object.assgin polyfill // console.log(assign({}, {a:1}, {a:2}, {b:3})) // convert js prop into css prop (dashified) // capitalize str // repeat str for num times // random string, should used across all cssobj plugins // console.log(isEmpty([]), isEmpty(), isEmpty(null), isEmpty(''), isEmpty({}), isEmpty(23)) // set object path value, any Primitive/Non-exists will be set to {} // var obj={a:{b:{c:1}}}; // objSet(obj, {} ,{x:1}); // objSet(obj,'a.b.c.d.e',{x:1}); // objSet(obj,'a.f.d.s'.split('.'), {y:1}); // console.log(JSON.stringify(obj)) // return object path with only object type // var obj={a:{b:{c:1}}}; // console.log(objGetObj(obj)) // console.log(objGetObj(obj, [])) // console.log(objGetObj(obj, 'a')) // console.log(objGetObj(obj, 'a.b')) // console.log(objGetObj(obj, 'a.b.c.e')) // extend obj from source, if it's no key in obj, create one function extendObj (obj, key, source) { obj[key] = obj[key] || {}; for(var args = arguments, i = 2; i < args.length; i++) { source = args[i]; for (var k in source) if (own(source, k)) obj[key][k] = source[k]; } return obj[key] } // ensure obj[k] as array, then push v into it function arrayKV (obj, k, v, reverse, unique) { var d = obj[k]; d = obj[k] = k in obj ? (Array.isArray(d) ? d : [d]) : []; if(unique && d.indexOf(v)>-1) return reverse ? d.unshift(v) : d.push(v); } // var d={}; arrayKV(d,'a',1) // console.log(d) // var d={a:2}; arrayKV(d,'a',1) // console.log(d) // var d={a:[2]}; arrayKV(d,'a',1) // console.log(d) // replace find in str, with rep function result // get parents array from node (when it's passed the test) function getParents (node, test, key, childrenKey, parentKey) { var i, v, p = node, path = []; while (p) { if (test(p)) { if (childrenKey) { for (i = 0; i < path.length; i++) { arrayKV(p, childrenKey, path[i], false, true); } } if (path[0] && parentKey) { path[0][parentKey] = p; } path.unshift(p); } p = p.parent; } for (i = 0; i < path.length; i++) { v = path[i]; path[i] = key ? v[key] : v; } return path } // split selector with comma, aware of css attributes // split selector with splitter, aware of css attributes function splitSelector (sel, splitter) { if (sel.indexOf(splitter) < 0) return [sel] for (var c, i = 0, n = 0, instr = '', prev = 0, d = []; c = sel.charAt(i); i++) { if (instr) { if (c == instr) instr = ''; continue } if (c == '"' || c == '\'') instr = c; if (c == '(' || c == '[') n++; if (c == ')' || c == ']') n--; if (!n && c == splitter) d.push(sel.substring(prev, i)), prev = i + 1; } return d.concat(sel.substring(prev)) } // split char aware of syntax // checking for valid css value function isValidCSSValue (val) { // falsy: '', NaN, Infinity, [], {} return typeof val=='string' && val || typeof val=='number' && isFinite(val) } // using var as iteral to help optimize var KEY_ID = '$id'; var KEY_ORDER = '$order'; var KEY_TEST = '$test'; var TYPE_GROUP = 'group'; // helper function var keys = Object.keys; // type check helpers var type = {}.toString; var ARRAY = type.call([]); var OBJECT = type.call({}); // only array, object now treated as iterable function isIterable (v) { return type.call(v) == OBJECT || type.call(v) == ARRAY } // check if it's function function isFunction (v) { return typeof v == 'function' } // regexp constants // @page rule: CSSOM: // IE returned: not implemented error // FF, Chrome actually is not groupRule(not cssRules), same as @font-face rule // https://developer.mozilla.org/en-US/docs/Web/API/CSSGroupingRule // CSSPageRule is listed as derived from CSSGroupingRule, but not implemented yet. // Here added @page as GroupRule, but plugin should take care of this. var reGroupRule = /^@(media|document|supports|page|[\w-]*keyframes)/i; var reAtRule = /^\s*@/i; /** * convert simple Object into node data * input data format: {"a":{"b":{"c":{"":[{color:1}]}}}, "abc":123, '@import':[2,3,4], '@media (min-width:320px)':{ d:{ok:1} }} * 1. every key is folder node * 2. "":[{rule1}, {rule2}] will split into several rules * 3. & will replaced by parent, \\& will escape * 4. all prop should be in dom.style camelCase * * @param {object|array} d - simple object data, or array * @param {object} result - the reulst object to store config and root node * @param {object} [previousNode] - also act as parent for next node * @param {boolean} init whether it's the root call * @returns {object} node data object */ function parseObj (d, result, node, init) { if (init) { result.nodes = []; result.ref = {}; if (node) result.diff = {}; } node = node || {}; node.obj = d; if (type.call(d) == ARRAY) { var nodes = []; /* for array type, each children have a parent that not on the virtual tree, see test case of @media-array for example, the array node obj=Array, but have node.selPart(no selText) So have to set the right node.at/node.type from the node.key, to get right selText for children */ node.at = reAtRule.exec(node.key); for(var i = 0; i < d.length; i++) { var prev = node[i]; var n = parseObj(d[i], result, node[i] || {parent: node, src: d, parentNode: nodes, index: i}); if(result.diff && prev!=n) arrayKV(result.diff, n ? 'added' : 'removed', n||prev); nodes.push(n); } return nodes } else { if (d[KEY_ID]) result.ref[d[KEY_ID]] = node; // it's no need to check (type.call(d) == OBJECT) // isIterable will filter only ARRAY/OBJECT // other types will goto parseProp function var prevVal = node.prevVal = node.lastVal; // at first stage check $test if (KEY_TEST in d) { var test = isFunction(d[KEY_TEST]) ? d[KEY_TEST](!node.disabled, node, result) : d[KEY_TEST]; // if test false, remove node completely // if it's return function, going to stage 2 where all prop rendered if(!test) { return } node.test = test; } var children = node.children = node.children || {}; node.lastRaw = node.rawVal || {}; node.lastVal = {}; node.rawVal = {}; node.prop = {}; node.diff = {}; var order = d[KEY_ORDER] | 0; var funcArr = []; var processObj = function (obj, k, nodeObj) { var haveOldChild = k in children; var newNode = extendObj(children, k, nodeObj); // don't overwrite selPart for previous node newNode.selPart = newNode.selPart || splitSelector(k, ','); var n = parseObj(obj, result, newNode); if(n) children[k] = n; // it's new added node if (prevVal) !haveOldChild ? n && arrayKV(result.diff, 'added', n) : !n && arrayKV(result.diff, 'removed', children[k]); // for first time check, remove from parent (no diff) if(!n) delete nodeObj.parent.children[k]; }; // only there's no selText, getSel if(!('selText' in node)) getSel(node, result); for (var k in d) { // here $key start with $ is special // k[0] == '$' ... but the core will calc it into node. // Plugins should take $ with care and mark as a special case. e.g. ignore it if (!own(d, k)) continue if (!isIterable(d[k]) || type.call(d[k]) == ARRAY && !isIterable(d[k][0])) { // it's inline at-rule: @import etc. if (k[0]=='@') { processObj( // map @import: [a,b,c] into {a:1, b:1, c:1} [].concat(d[k]).reduce(function(prev, cur) { prev[cur] = ';'; return prev }, {}), k, {parent: node, src: d, key: k, inline:true}); continue } var r = function (_k) { // skip $test key if(_k != KEY_TEST) parseProp(node, d, _k, result); }; order ? funcArr.push([r, k]) : r(k); } else { processObj(d[k], k, {parent: node, src: d, key: k}); } } // when it's second time visit node if (prevVal) { // children removed for (k in children) { if (!(k in d)) { arrayKV(result.diff, 'removed', children[k]); delete children[k]; } } // prop changed var diffProp = function () { var newKeys = keys(node.lastVal); var removed = keys(prevVal).filter(function (x) { return newKeys.indexOf(x) < 0 }); if (removed.length) node.diff.removed = removed; if (keys(node.diff).length) arrayKV(result.diff, 'changed', node); }; order ? funcArr.push([diffProp, null]) : diffProp(); } if (order) arrayKV(result, '_order', {order: order, func: funcArr}); result.nodes.push(node); return node } } function getSel(node, result) { var opt = result.config; // array index don't have key, // fetch parent key as ruleNode var ruleNode = getParents(node, function (v) { return v.key }).pop(); node.parentRule = getParents(node.parent, function (n) { return n.type == TYPE_GROUP }).pop() || null; if (ruleNode) { var isMedia, sel = ruleNode.key; var groupRule = sel.match(reGroupRule); if (groupRule) { node.type = TYPE_GROUP; node.at = groupRule.pop(); isMedia = node.at == 'media'; // only media allow nested and join, and have node.selPart if (isMedia) node.selPart = splitSelector(sel.replace(reGroupRule, ''), ','); // combinePath is array, 'str' + array instead of array.join(',') node.groupText = isMedia ? '@' + node.at + combinePath(getParents(node, function (v) { return v.type == TYPE_GROUP }, 'selPart', 'selChild', 'selParent'), '', ' and') : sel; node.selText = getParents(node, function (v) { return v.selText && !v.at }, 'selText').pop() || ''; } else if (reAtRule.test(sel)) { node.type = 'at'; node.selText = sel; } else { node.selText = '' + combinePath(getParents(ruleNode, function (v) { return v.selPart && !v.at }, 'selPart', 'selChild', 'selParent'), '', ' ', true), opt; } node.selText = applyPlugins(opt, 'selector', node.selText, node, result); if (node.selText) node.selTextPart = splitSelector(node.selText, ','); if (node !== ruleNode) node.ruleNode = ruleNode; } } /** * Parse property of object d's key, with propKey as a candidate key name * @param {} node: v-node of cssobj * @param {} d: source object * @param {} key: any numeric will be ignored, then converted to string * @param {} result: cssobj result object * @param {} propKey: candidate prop key name Accept only key as string, numeric will be ignored color: function(){return ['red', 'blue']} will expand color: function(){return {fontSize: '12px', float:'right'}} will be replaced */ function parseProp (node, d, key, result, propKey) { var prevVal = node.prevVal; var lastVal = node.lastVal; // the prop name get from object key or candidate key var propName = isNumeric(key) ? propKey : key; // NEXT: propName can be changed by user // now it's not used, since propName ensure exists // corner case: propKey==='' ?? below line will do wrong!! // if(!propName) return var raw = node.lastRaw[propName], prev = prevVal && prevVal[propName], argObj = {node:node, result:result}; if (raw) argObj.raw = raw[0]; ![].concat(d[key]).forEach(function (v) { // prepare value function args argObj.cooked = prev; // pass lastVal if it's function argObj.raw = raw = isFunction(v) ? v(argObj) : v; var val = applyPlugins(result.config, 'value', raw, propName, node, result, propKey); // check and merge only format as Object || Array of Object, other format not accepted! if (isIterable(val)) { for (var k in val) { if (own(val, k)) parseProp(node, val, k, result, propName); } } else { arrayKV ( node.rawVal, propName, raw, true ); if (isValidCSSValue(val)) { // only valid val can enter node.prop and lastVal // push every val to prop arrayKV( node.prop, propName, val, true ); prev = lastVal[propName] = val; } } }); if (prevVal) { if (!(propName in prevVal)) { arrayKV(node.diff, 'added', propName); } else if (prevVal[propName] != lastVal[propName]) { arrayKV(node.diff, 'changed', propName); } } } function combinePath (array, parentSel, seperator, replaceAmpersand) { return !array.length ? parentSel : array[0].reduce(function (result, value) { var part, str = parentSel ? parentSel + seperator : parentSel; if (replaceAmpersand) { part = splitSelector( value, '&' ); str = part.length > 1 ? part.join(parentSel) : str + value; } else { str += value; } return result.concat(combinePath(array.slice(1), str, seperator, replaceAmpersand)) }, []) } function applyPlugins (opt, type) { var args = [].slice.call(arguments, 2); var plugin = opt.plugins; // plugin is always Array, so here we don't check it return [].concat(plugin).reduce( function (pre, plugin) { return plugin[type] ? plugin[type].apply(null, [pre].concat(args)) : pre }, args.shift() ) } function applyOrder (opt) { if (opt._order==null) return opt._order .sort(function (a, b) { return a.order - b.order }) .forEach(function (v) { v.func.forEach(function (f) { f[0](f[1]); }); }); opt._order = []; } function cssobj (config) { config = defaults(config, { plugins: [], intros: [] }); return function (initObj, initState) { var updater = function (obj, state) { if (arguments.length>1) result.state = state || {}; if (obj) result.obj = isFunction(obj) ? obj() : obj; result.root = parseObj(extendObj({}, '', result.intro, result.obj), result, result.root, true); applyOrder(result); result = applyPlugins(config, 'post', result); isFunction(config.onUpdate) && config.onUpdate(result); return result }; var result = { intro: {}, update: updater, config: config }; ![].concat(config.intros).forEach( function(v) { extendObj(result, 'intro', isFunction(v) ? v(result) : v); } ); updater(initObj, initState || config.state); return result } } module.exports = cssobj;