From f48b96714a7fd66721d3365c9f95f2fdd823913c Mon Sep 17 00:00:00 2001 From: Sergey Melyukov Date: Wed, 7 Sep 2016 17:54:18 +0300 Subject: [PATCH 1/3] error handling initial nth handler --- data/complex/child.css | 19 ++++++ data/complex/child.tmpl | 6 ++ data/complex/panel.css | 13 +++- src/module/list/index.js | 1 + src/module/stage/index.js | 5 +- src/utils/builder.js | 17 +++-- src/utils/emulators/contentFactory.js | 2 +- src/utils/emulators/index.js | 4 +- src/utils/emulators/nthFactory.js | 71 +++++++++++++++++++++ src/utils/emulators/nthHandler.js | 23 +++++++ src/utils/emulators/pseudoClassFactory.js | 33 +++++++--- src/utils/emulators/pseudoElementFactory.js | 48 +++++++++++--- src/utils/styleDomMapper.js | 12 +++- 13 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 data/complex/child.css create mode 100644 data/complex/child.tmpl create mode 100644 src/utils/emulators/nthFactory.js create mode 100644 src/utils/emulators/nthHandler.js diff --git a/data/complex/child.css b/data/complex/child.css new file mode 100644 index 0000000..cc7116a --- /dev/null +++ b/data/complex/child.css @@ -0,0 +1,19 @@ +.container::before1 +{ + content: 'before'; + display: block; + color: blue; +} + +.container::after +{ + content: 'after'; + display: block; + color: red; +} + +.block:first-child +{ + color: blueviolet; + font-size: large; +} diff --git a/data/complex/child.tmpl b/data/complex/child.tmpl new file mode 100644 index 0000000..d84f565 --- /dev/null +++ b/data/complex/child.tmpl @@ -0,0 +1,6 @@ + + + +
+ some content +
diff --git a/data/complex/panel.css b/data/complex/panel.css index 81bc9ee..81b6ab1 100644 --- a/data/complex/panel.css +++ b/data/complex/panel.css @@ -10,6 +10,17 @@ font-size: 32px; } -.panel .button { +.panel .button +{ margin: 0 10px; } + +.buttons::before +{ + content: 'before buttons'; +} + +.buttons .button:first-child::before +{ + content: 'first button'; +} diff --git a/src/module/list/index.js b/src/module/list/index.js index 38d10b2..decb2c2 100644 --- a/src/module/list/index.js +++ b/src/module/list/index.js @@ -30,6 +30,7 @@ module.exports = new Node({ 'data/checkbox.tmpl', 'data/window.tmpl', 'data/box.tmpl', + 'data/complex/child.tmpl', 'data/complex/panel.tmpl', 'data/complex/button.tmpl' ].map(function(url) { diff --git a/src/module/stage/index.js b/src/module/stage/index.js index 5fc5b6a..593546f 100644 --- a/src/module/stage/index.js +++ b/src/module/stage/index.js @@ -63,7 +63,10 @@ function rebuildStage() { var resource = basis.resource(url); - vBuilder = new VariantBuilder(resource, [emulators.active, emulators.hover, emulators.focus, emulators.after, emulators.before]); + vBuilder = new VariantBuilder(resource, Object.keys(emulators).map(function(name) { + return emulators[name]; + })); + vBuilder.styles.forEach(function(style) { style.original.attach(rebuildStage); }); diff --git a/src/utils/builder.js b/src/utils/builder.js index 0a65d1b..a002973 100644 --- a/src/utils/builder.js +++ b/src/utils/builder.js @@ -81,6 +81,11 @@ function Builder(resource, emulators) { }, this); this.states = this.combine(this.states); + + if (!this.states.length) { + this.states.push({hasState: false}); + } + this.states.forEach(function(state) { var newVariant = this.buildVariant(state); @@ -139,13 +144,17 @@ Builder.prototype.generateSourceMap = function(sourceAst, processedAST) { // todo возможно есть более простой способ сфлэтить все селекторы из ast walk(sourceAst, { SimpleSelector: function(token) { - token.sequence.each(sourceTokens.push.bind(sourceTokens)); + token.sequence.each(function(part) { + sourceTokens.push(part) + }); } }); walk(processedAST, { SimpleSelector: function(token) { - token.sequence.each(processedTokens.push.bind(processedTokens)); + token.sequence.each(function(part) { + processedTokens.push(part) + }); } }); @@ -166,9 +175,9 @@ Builder.prototype.handleStyle = function(style, emulators) { var newStates; emulator.handleToken(part, parent, style.processedAST, style.sourceMap); - newStates = emulator.getStates(); + newStates = emulator.getStates(style.sourceAST) || []; - if (!newStates || !newStates.length) { + if (!newStates.length) { if (this.emulators['*'].indexOf(emulator) < 0) { this.emulators['*'].push(emulator) } diff --git a/src/utils/emulators/contentFactory.js b/src/utils/emulators/contentFactory.js index 2b59e94..6d82f77 100644 --- a/src/utils/emulators/contentFactory.js +++ b/src/utils/emulators/contentFactory.js @@ -36,4 +36,4 @@ module.exports = function pseudoContentFactory(pseudoContent) { } }); } -} +}; diff --git a/src/utils/emulators/index.js b/src/utils/emulators/index.js index 56a1eef..d86b9df 100644 --- a/src/utils/emulators/index.js +++ b/src/utils/emulators/index.js @@ -1,10 +1,12 @@ var pseudoClassFactory = require('./pseudoClassFactory'); var pseudoElementFactory = require('./pseudoElementFactory'); +var hthHandler = require('./nthHandler'); module.exports = { active: pseudoClassFactory('active'), hover: pseudoClassFactory('hover'), focus: pseudoClassFactory('focus'), after: pseudoElementFactory('after'), - before: pseudoElementFactory('before', true) + before: pseudoElementFactory('before', true), + hthHandler: hthHandler }; diff --git a/src/utils/emulators/nthFactory.js b/src/utils/emulators/nthFactory.js new file mode 100644 index 0000000..b64180e --- /dev/null +++ b/src/utils/emulators/nthFactory.js @@ -0,0 +1,71 @@ +var List = require('csso:utils/list.js'); + +module.exports = function nthFactory(nth, element) { + var normalizedArgs; + var classMap = { + 'first-child': { + a: '0n', + b: 1 + }, + 'last-child': { + a: '0n', + b: element.parentNode.children.length + } + }; + var argMap = { + even: { + a: '2n', + b: 0 + }, + odd: { + a: '2n', + b: 1 + } + }; + + if (['first-child', 'last-child', 'nth-child', 'nth-child'].indexOf(nth.name) == -1) { + return; + } + + if (nth.type == 'PseudoClass') { + if (!(normalizedArgs = classMap[nth.name])) { + throw new Error('Something awful was happened...'); + } + } else if (nth.type == 'FunctionalPseudo') { + var args = nth.arguments.first().sequence.toArray(); + + if (args.length == 1) { + normalizedArgs = argMap[args[0].value] || { + a: args[0].value, + b: 0 + } + } else if (args.length == 3) { + normalizedArgs = { + a: args[0].value, + b: args[2].value + } + } else { + throw new Error('Something awful was happened...'); + } + } + + return { + change: function(amount) { + normalizedArgs.b += amount; + }, + apply: function() { + var args = new List([{ + type: 'Argument', + sequence: new List([ + {type: 'Nth', value: normalizedArgs.a}, + {type: 'Operator', value: '+'}, + {type: 'Nth', value: normalizedArgs.b} + ]) + }]); + + nth.name = 'nth-child'; + nth.type = 'FunctionalPseudo'; + nth.arguments = args; + } + } +}; diff --git a/src/utils/emulators/nthHandler.js b/src/utils/emulators/nthHandler.js new file mode 100644 index 0000000..3fb6e3a --- /dev/null +++ b/src/utils/emulators/nthHandler.js @@ -0,0 +1,23 @@ +var List = require('csso:utils/list.js'); + +module.exports = { + getStates: function() { + return []; + }, + handleToken: function(token, parent, root, sourceMap) { + if (token.type == 'PseudoClass' && token.name == 'only-child') { + var sourceToken = sourceMap.get(token); + var newToken = List.createItem({type: 'PseudoClass', name: 'last-child'}); + + token.name = 'first-child'; + + sourceMap.delete(token); + sourceMap.set(newToken.data, sourceToken); + + parent.data.sequence.insert(newToken); + } + }, + emulate: function(token, parent, root, sourceMap, mapper, value) { + // nothing to do... + } +}; diff --git a/src/utils/emulators/pseudoClassFactory.js b/src/utils/emulators/pseudoClassFactory.js index e0b0729..1879d80 100644 --- a/src/utils/emulators/pseudoClassFactory.js +++ b/src/utils/emulators/pseudoClassFactory.js @@ -1,18 +1,33 @@ +var walk = require('utils.walker').all; + module.exports = function pseudoClassFactory(type) { var TYPE_NAME = 'pseudo-class-' + type + '__' + basis.genUID(); return { - getStates: function() { - var states = []; - var state = {}; + getStates: function(AST) { + var allow = false; + + //todo сделать оптимально - построить индекс по таким местам или не обходить оставшиеся узлы если стало понятно, что эмуляция нужна + walk(AST, { + PseudoClass: function(token) { + if (token.type == 'PseudoClass' && token.name == type) { + allow = true; + } + } + }); + + if (allow) { + var states = []; + var state = {}; - state[type] = false; - states.push(state); - state = {}; - state[type] = true; - states.push(state); + state[type] = false; + states.push(state); + state = {}; + state[type] = true; + states.push(state); - return states; + return states; + } }, handleToken: function(token, parent, root, sourceMap) { if (token.type == 'PseudoClass' && token.name == type) { diff --git a/src/utils/emulators/pseudoElementFactory.js b/src/utils/emulators/pseudoElementFactory.js index a25a9a0..a0e3e75 100644 --- a/src/utils/emulators/pseudoElementFactory.js +++ b/src/utils/emulators/pseudoElementFactory.js @@ -2,39 +2,65 @@ var List = require('csso:utils/list.js'); var walk = require('utils.walker').rules; var translate = require('csso:utils/translate.js'); var pseudoContentFactory = require('./contentFactory'); +var nthFactory = require('./nthFactory'); module.exports = function pseudoElementFactory(type, before) { var TYPE_NAME = 'pseudo-element-' + type + '__' + basis.genUID(); return { - getStates: function() { - return []; + getStates: function(AST) { + // no states }, handleToken: function(token, parent, root, sourceMap) { if (token.type == 'PseudoElement' && token.name == type) { + var sourceToken = sourceMap.get(token); + var newToken = List.createItem({type: 'Identifier', name: TYPE_NAME}); + token.type = 'Combinator'; token.name = '>'; - parent.data.sequence.insert(List.createItem({type: 'Identifier', name: TYPE_NAME})); + + sourceMap.delete(token); + sourceMap.set(newToken.data, sourceToken); + + parent.data.sequence.insert(newToken); + + /* + basis.array.from(element.children).forEach(function(child) { + var selectors = mapper.byElement(child); + + if (selectors) { + selectors.forEach(function(token) { + var handleNth = nthFactory(token, child); + + if (handleNth) { + //console.log(selectors); + console.log(token, handleNth); + handleNth.change(before ? 1 : -1); + handleNth.apply(); + needToRetranslate = true; + } + }); + } + }); + */ } }, emulate: function(token, parent, root, sourceMap, mapper, value) { var needToRetranslate = false; var sourceToken = sourceMap.get(token); + //console.log(token, '=>', sourceToken) if (sourceToken && sourceToken.type == 'PseudoElement' && sourceToken.name == type) { var elementHandler; - var allowToEmulate = []; var mappedElements = mapper.bySelector(token); - - if (mappedElements) { - allowToEmulate = mappedElements.filter(function(element) { + //console.log(token, mappedElements) + var allowToEmulate = mappedElements && mappedElements.filter(function(element) { var list = new List(parent.data.sequence.toArray().slice(0, -2)); return element.matches(translate({type: 'SimpleSelector', sequence: list})); }); - } - if (!allowToEmulate.length) { + if (!allowToEmulate || !allowToEmulate.length) { return; } @@ -59,7 +85,9 @@ module.exports = function pseudoElementFactory(type, before) { mapper.removeSelector(token); allowToEmulate.forEach(function(element) { - var existingEmulator = element.querySelector(TYPE_NAME); + var existingEmulator = basis.array.from(element.children).filter(function(child) { + return child.tagName.toLowerCase() == TYPE_NAME.toLowerCase(); + })[0]; if (existingEmulator) { if (elementHandler) { diff --git a/src/utils/styleDomMapper.js b/src/utils/styleDomMapper.js index e46ed39..db1f0a2 100644 --- a/src/utils/styleDomMapper.js +++ b/src/utils/styleDomMapper.js @@ -75,12 +75,14 @@ StyleDOMMapper.prototype.map = function(root, processedAST, sourceMap) { var selector = translate(token); var stack = []; - // console.group('full selector', selector, token); + //console.group('full selector', selector, token); token.sequence.each(function(part) { var sourcePart = sourceMap.get(part) || part; + //console.log('SP', part, '=>', sourcePart) + // пропускаем псевдо классы/элементы, иначе querySelectorAll не найдет узлы - if (sourcePart.type != 'PseudoClass' && sourcePart.type != 'PseudoElement') { + if ((sourcePart.type != 'PseudoClass' || sourcePart.type == 'PseudoClass' && sourcePart.name == 'first-element') && sourcePart.type != 'PseudoElement') { stack.push(sourcePart); } @@ -89,6 +91,10 @@ StyleDOMMapper.prototype.map = function(root, processedAST, sourceMap) { return; } + if (stack.length && stack[stack.length - 1].type == 'Combinator') { + stack.pop(); + } + var partSelector = translate({type: 'SimpleSelector', sequence: new List(stack)}); var elements = root.querySelectorAll(partSelector); @@ -97,7 +103,7 @@ StyleDOMMapper.prototype.map = function(root, processedAST, sourceMap) { this.linkElements(part, elements); // console.groupEnd(); }, this); - // console.groupEnd(); + //console.groupEnd(); } }, this); }; From 5219ebf29c80110ef025ec8414d86d5fda7c0ec0 Mon Sep 17 00:00:00 2001 From: Sergey Melyukov Date: Mon, 12 Sep 2016 17:33:47 +0300 Subject: [PATCH 2/3] isolate variants refactoring --- src/app/lib/builder.js | 122 +++++++ .../lib}/emulators/contentFactory.js | 0 src/{utils => app/lib}/emulators/index.js | 4 +- .../lib}/emulators/pseudoClassFactory.js | 1 + .../lib}/emulators/pseudoElementFactory.js | 31 +- src/app/lib/generator/basis.js | 61 ++++ src/app/lib/resource.js | 53 +++ src/{utils => app/lib}/styleDomMapper.js | 0 src/app/lib/template.js | 22 ++ src/app/lib/variant.js | 128 +++++++ src/module/stage/index.js | 48 +-- src/module/stage/template/variant.tmpl | 2 +- src/utils/builder.js | 324 ------------------ src/utils/dom.js | 40 +++ src/utils/emulators/nthFactory.js | 71 ---- src/utils/emulators/nthHandler.js | 23 -- src/utils/index.js | 2 - src/utils/style.js | 41 +++ 18 files changed, 500 insertions(+), 473 deletions(-) create mode 100644 src/app/lib/builder.js rename src/{utils => app/lib}/emulators/contentFactory.js (100%) rename src/{utils => app/lib}/emulators/index.js (72%) rename src/{utils => app/lib}/emulators/pseudoClassFactory.js (97%) rename src/{utils => app/lib}/emulators/pseudoElementFactory.js (79%) create mode 100644 src/app/lib/generator/basis.js create mode 100644 src/app/lib/resource.js rename src/{utils => app/lib}/styleDomMapper.js (100%) create mode 100644 src/app/lib/template.js create mode 100644 src/app/lib/variant.js delete mode 100644 src/utils/builder.js create mode 100644 src/utils/dom.js delete mode 100644 src/utils/emulators/nthFactory.js delete mode 100644 src/utils/emulators/nthHandler.js create mode 100644 src/utils/style.js diff --git a/src/app/lib/builder.js b/src/app/lib/builder.js new file mode 100644 index 0000000..5210004 --- /dev/null +++ b/src/app/lib/builder.js @@ -0,0 +1,122 @@ +var walk = require('utils.walker').all; +var sortObject = require('utils.index').sortObject; +var domUtils = require('utils.dom'); +var Variant = require('./variant'); + +/** + * @property {Array} emulators + * @property {Object} generator + */ +module.exports = basis.Class(null, { + className: 'dp.Builder', + extendConstructor_: true, + init: function() { + var emulators = Array.isArray(this.emulators) ? this.emulators : basis.array.from(this.emulators); + + this.emulators = { + '*': [] + }; + this.digest = {}; + this.variants = []; + this.ignoredVariantsCount = 0; + this.states = this.generator.getStates(); + this.states = this.generator.getStyles().reduce(function(states, style) { + return states.concat(this.handleStyle(style, emulators)); + }.bind(this), this.states); + + if (this.states.length) { + this.states = this.combine(this.states); + } else { + this.states.push({hasStates: false}); + } + + this.states.forEach(function(state) { + var newVariant = this.buildVariant(state); + + if (!this.addVariant(newVariant)) { + newVariant.destroy(); + } + }, this); + + console.log('BUILDER', this); + console.log('STATES', this.states); + console.log('EMULATORS', this.emulators); + console.log('VARIANTS', this.variants); + console.log('======================='); + + }, + handleStyle: function(style, emulators) { + var states = {}; + + emulators.forEach(function(emulator) { + var newStates = emulator.getStates(style.AST); + + if (Array.isArray(newStates)) { + newStates.forEach(function(state) { + var stateString = JSON.stringify(state); + + if (!states[stateString]) { + for (var stateName in state) { + if (state.hasOwnProperty(stateName)) { + this.emulators[stateName] = this.emulators[stateName] || []; + basis.array.add(this.emulators[stateName], emulator); + } + } + + states[stateString] = state; + } + }, this); + } else { + basis.array.add(this.emulators['*'], emulator); + } + }, this); + style.apply(); + + return basis.object.values(states); + }, + buildVariant: function(states) { + return new Variant({generator: this.generator, emulators: this.emulators, states: states}); + }, + addVariant: function(candidate) { + var digest; + + if (this.variants.indexOf(candidate) > -1) { + return; + } + + digest = domUtils.getDigest(candidate.wrapper); + + if (!this.digest.hasOwnProperty(digest)) { + this.digest[digest] = true; + this.variants.push(candidate); + + return true; + } else { + this.ignoredVariantsCount++; + + return false; + } + }, + combine: function(states) { + var newStates = {}; + + states.forEach(function(state) { + Object.keys(newStates).forEach(function(stateName) { + var stateToAdd = basis.object.merge(state, newStates[stateName]); + var objectString = JSON.stringify(sortObject(stateToAdd)); + + if (!newStates[objectString]) { + newStates[objectString] = stateToAdd; + } + }); + + var objectString = JSON.stringify(sortObject(state)); + + if (!newStates[objectString]) { + newStates[objectString] = state; + } + }); + + return basis.object.values(newStates); + } +}); diff --git a/src/utils/emulators/contentFactory.js b/src/app/lib/emulators/contentFactory.js similarity index 100% rename from src/utils/emulators/contentFactory.js rename to src/app/lib/emulators/contentFactory.js diff --git a/src/utils/emulators/index.js b/src/app/lib/emulators/index.js similarity index 72% rename from src/utils/emulators/index.js rename to src/app/lib/emulators/index.js index d86b9df..56a1eef 100644 --- a/src/utils/emulators/index.js +++ b/src/app/lib/emulators/index.js @@ -1,12 +1,10 @@ var pseudoClassFactory = require('./pseudoClassFactory'); var pseudoElementFactory = require('./pseudoElementFactory'); -var hthHandler = require('./nthHandler'); module.exports = { active: pseudoClassFactory('active'), hover: pseudoClassFactory('hover'), focus: pseudoClassFactory('focus'), after: pseudoElementFactory('after'), - before: pseudoElementFactory('before', true), - hthHandler: hthHandler + before: pseudoElementFactory('before', true) }; diff --git a/src/utils/emulators/pseudoClassFactory.js b/src/app/lib/emulators/pseudoClassFactory.js similarity index 97% rename from src/utils/emulators/pseudoClassFactory.js rename to src/app/lib/emulators/pseudoClassFactory.js index 1879d80..fb235e6 100644 --- a/src/utils/emulators/pseudoClassFactory.js +++ b/src/app/lib/emulators/pseudoClassFactory.js @@ -4,6 +4,7 @@ module.exports = function pseudoClassFactory(type) { var TYPE_NAME = 'pseudo-class-' + type + '__' + basis.genUID(); return { + /**@cut*/__debugName: TYPE_NAME, getStates: function(AST) { var allow = false; diff --git a/src/utils/emulators/pseudoElementFactory.js b/src/app/lib/emulators/pseudoElementFactory.js similarity index 79% rename from src/utils/emulators/pseudoElementFactory.js rename to src/app/lib/emulators/pseudoElementFactory.js index a0e3e75..7e400df 100644 --- a/src/utils/emulators/pseudoElementFactory.js +++ b/src/app/lib/emulators/pseudoElementFactory.js @@ -2,47 +2,23 @@ var List = require('csso:utils/list.js'); var walk = require('utils.walker').rules; var translate = require('csso:utils/translate.js'); var pseudoContentFactory = require('./contentFactory'); -var nthFactory = require('./nthFactory'); module.exports = function pseudoElementFactory(type, before) { var TYPE_NAME = 'pseudo-element-' + type + '__' + basis.genUID(); return { - getStates: function(AST) { - // no states + /**@cut*/__debugName: TYPE_NAME, + getStates: function() { + return false; }, handleToken: function(token, parent, root, sourceMap) { if (token.type == 'PseudoElement' && token.name == type) { - var sourceToken = sourceMap.get(token); var newToken = List.createItem({type: 'Identifier', name: TYPE_NAME}); token.type = 'Combinator'; token.name = '>'; - sourceMap.delete(token); - sourceMap.set(newToken.data, sourceToken); - parent.data.sequence.insert(newToken); - - /* - basis.array.from(element.children).forEach(function(child) { - var selectors = mapper.byElement(child); - - if (selectors) { - selectors.forEach(function(token) { - var handleNth = nthFactory(token, child); - - if (handleNth) { - //console.log(selectors); - console.log(token, handleNth); - handleNth.change(before ? 1 : -1); - handleNth.apply(); - needToRetranslate = true; - } - }); - } - }); - */ } }, emulate: function(token, parent, root, sourceMap, mapper, value) { @@ -53,7 +29,6 @@ module.exports = function pseudoElementFactory(type, before) { if (sourceToken && sourceToken.type == 'PseudoElement' && sourceToken.name == type) { var elementHandler; var mappedElements = mapper.bySelector(token); - //console.log(token, mappedElements) var allowToEmulate = mappedElements && mappedElements.filter(function(element) { var list = new List(parent.data.sequence.toArray().slice(0, -2)); diff --git a/src/app/lib/generator/basis.js b/src/app/lib/generator/basis.js new file mode 100644 index 0000000..4974c02 --- /dev/null +++ b/src/app/lib/generator/basis.js @@ -0,0 +1,61 @@ +var makeDeclaration = require('basis.template.declaration').makeDeclaration; +var templateBuilder = require('basis.template.html').Template.prototype.builder; +var Template = require('../template'); +var StyleResource = require('../resource').StyleResource; + +/** + * @property {String} url + */ +module.exports = basis.Class(null, { + className: 'dp.generator.BasisGenerator', + extendConstructor_: true, + init: function() { + this.resource = basis.resource(this.url); + this.decl = makeDeclaration(this.resource.fetch(), basis.path.dirname(this.resource.url), null, this.resource.url); + this.decl.instances = {}; + this.templateBuilder = templateBuilder.call({source: this.resource}, this.decl.tokens, this.decl.instances); + this.states = []; + + for (var name in this.decl.states) { + if (this.decl.states.hasOwnProperty(name)) { + var values = []; + + if (this.decl.states[name].bool) { + values.push(false, true); + } + + if (this.decl.states[name].enum) { + values.push.apply(values, this.decl.states[name].enum); + } + + values.forEach(function(value) { + var state = {}; + + state[name] = value; + this.states.push(state); + }, this); + } + } + + this.styles = this.decl.styles.map(function(style) { + return new StyleResource({url: style.resource}) + }); + }, + generate: function(states) { + return new Template({template: this.templateBuilder.createInstance(), states: states}); + }, + getStates: function() { + return this.states; + }, + getStyles: function() { + return this.styles; + }, + destroy: function() { + this.templateBuilder.destroy(); + this.resource.destroy(); + this.styles.forEach(function(style) { + style.destroy(); + }); + basis.Class.prototype.destroy.call(this); + } +}); diff --git a/src/app/lib/resource.js b/src/app/lib/resource.js new file mode 100644 index 0000000..fb8232a --- /dev/null +++ b/src/app/lib/resource.js @@ -0,0 +1,53 @@ +var styleUtils = require('utils.style'); +var parse = require('csso:parser/index'); +var translate = require('csso:utils/translate.js'); + +var Resource = basis.Class(null, { + extendConstructor_: true, + className: 'dp.Resource', + startUse: function() { + this.resource.startUse(); + }, + stopUse: function() { + this.resource.stopUse(); + }, + apply: function() { + this.resource.updateCssText(translate(this.AST)); + }, + destroy: function() { + this.stopUse(); + //TODO очищать ресурсы + //this.resource.destroy(); + //this.physResource.destroy(); + basis.Class.prototype.destroy.call(this); + }, + postInit: function() { + if (this.useImmediate) { + this.startUse(); + } + } +}); + +var StyleResource = basis.Class(Resource, { + className: 'dp.StyleResource', + init: function() { + this.physResource = basis.resource(this.url); + this.resource = this.physResource.fetch(); + this.AST = parse(this.resource.cssText); + } +}); + +var ProcessableStyleResource = Resource.subclass({ + className: 'dp.ProcessableStyleResource', + init: function() { + this.physResource = this.source.physResource; + this.resource = basis.resource.virtual('css', this.source.resource.cssText, this.url).fetch(); + this.AST = parse(this.resource.cssText); + this.sourceMap = styleUtils.sourceMap(this.source.AST, this.AST) + } +}); + +module.exports = { + StyleResource: StyleResource, + ProcessableStyleResource: ProcessableStyleResource +}; diff --git a/src/utils/styleDomMapper.js b/src/app/lib/styleDomMapper.js similarity index 100% rename from src/utils/styleDomMapper.js rename to src/app/lib/styleDomMapper.js diff --git a/src/app/lib/template.js b/src/app/lib/template.js new file mode 100644 index 0000000..e8c7822 --- /dev/null +++ b/src/app/lib/template.js @@ -0,0 +1,22 @@ +/** + * @property {Object} states + * @property {Object} template + */ +module.exports = basis.Class(null, { + className: 'dp.Template', + extendConstructor_: true, + init: function() { + if (this.states) { + for (var stateName in this.states) { + if (this.states.hasOwnProperty(stateName)) { + this.template.set(stateName, this.states[stateName]); + } + } + } else { + this.states = {}; + } + }, + getDOM: function() { + return /*this.template.element.parentNode || */this.template.element; + } +}); diff --git a/src/app/lib/variant.js b/src/app/lib/variant.js new file mode 100644 index 0000000..b462706 --- /dev/null +++ b/src/app/lib/variant.js @@ -0,0 +1,128 @@ +var StyleDOMMapper = require('./styleDomMapper'); +var walk = require('utils.walker').all; +var List = require('csso:utils/list.js'); +var ProcessableStyleResource = require('./resource').ProcessableStyleResource; + +function getAllEmulators(emuList) { + var emulators = Object.keys(emuList).reduce(function(emulators, stateName) { + return stateName == '*' ? emulators : emulators.concat(emuList[stateName]); + }, []); + + return emulators.concat(emuList['*'] || []); +} + +function getStateEmulators(state, emuList) { + var emulators = emuList.hasOwnProperty(state) && Array.isArray(emuList[state]) ? emuList[state] : []; + + return emulators.concat(emuList['*'] || []); +} + +/** + * @property {Object} generator + * @property {Object} emulators + * @property {Object} states + * @property {Array} generator + */ +module.exports = basis.Class(null, { + className: 'dp.Variant', + extendConstructor_: true, + init: function() { + this.emulators = this.emulators || {}; + this.resources = this.generator.getStyles(); + this.template = this.generator.generate(this.states); + this.wrapperClass = 'dp-variant-wrapper-' + this.basisObjectId; + this.resources = this.resources.map(function(resource) { + return new ProcessableStyleResource({source: resource, useImmediate: true}); + }, this); + this.emulators = this.emulators || []; + + this.wrapper = document.createElement('div'); + this.wrapper.appendChild(this.template.getDOM()); + this.isolateStyles(); + + this.styleDOMMapper = new StyleDOMMapper(); + this.resources.forEach(function(resource) { + this.styleDOMMapper.map(this.wrapper, resource.AST, resource.sourceMap); + }, this); + + this.resources.forEach(function(resource) { + walk(resource.AST, { + SimpleSelector: function(token, parent) { + token.sequence.each(function(part) { + var emulators = getAllEmulators(this.emulators); + + emulators.forEach(function(emulator) { + emulator.handleToken(part, parent, resource.AST, resource.sourceMap); + }); + }, this); + } + }, this); + }, this); + + this.resources.forEach(function(resource) { + walk(resource.AST, { + SimpleSelector: function(token, parent) { + token.sequence.each(function(part) { + for (var stateName in this.states) { + if (!this.states.hasOwnProperty(stateName)) { + continue; + } + + var emulators = getStateEmulators(stateName, this.emulators); + + emulators.forEach(function(emulator) { + emulator.emulate(part, parent, resource.AST, resource.sourceMap, this.styleDOMMapper, this.states[stateName]); + }, this); + } + }, this); + } + }, this); + + resource.apply(); + }, this); + }, + isolateStyles: function() { + this.wrapper.classList.add(this.wrapperClass); + + this.resources.forEach(function(resource) { + walk(resource.AST, { + SimpleSelector: function(token) { + var firstToken = token.sequence.head; + + if (firstToken.data.type == 'Class' && firstToken.data.name != this.wrapperClass || firstToken.data.type == 'Id') { + var className = List.createItem({type: 'Class', name: this.wrapperClass}); + var combinator = List.createItem({type: 'Combinator', name: ' '}); + + token.sequence.insert(combinator, firstToken); + token.sequence.insert(className, combinator); + } + } + }, this); + resource.apply(); + }, this); + }, + unIsolateStyles: function() { + this.wrapper.classList.remove(this.wrapperClass); + + this.resources.forEach(function(resource) { + walk(resource.AST, { + SimpleSelector: function(token) { + var firstToken = token.sequence.head; + + if (firstToken.data.type == 'Class' && firstToken.data.name == this.wrapperClass) { + token.sequence.remove(token.sequence.first()); + token.sequence.remove(token.sequence.first()); + } + } + }, this); + resource.apply(); + }, this); + }, + destroy: function() { + this.resources.forEach(function(resource) { + resource.destroy(); + }); + this.wrapper.removeChild(this.template.getDOM()); + basis.Class.prototype.destroy.call(this); + } +}); diff --git a/src/module/stage/index.js b/src/module/stage/index.js index 593546f..df96fd1 100644 --- a/src/module/stage/index.js +++ b/src/module/stage/index.js @@ -1,8 +1,9 @@ var Value = require('basis.data').Value; var Node = require('basis.ui').Node; var router = require('basis.router'); -var VariantBuilder = require('utils.builder'); -var emulators = require('utils.emulators.index'); +var Builder = require('app.lib.builder'); +var Generator = require('app.lib.generator.basis'); +var emulators = require('app.lib.emulators.index'); var sortObject = require('utils.index').sortObject; require('basis.l10n').setCultureList('ru-RU'); @@ -17,7 +18,7 @@ var view = new Node({ }, sorting: function(node) { - return Object.keys(node.condition).length + JSON.stringify(node.condition); + return Object.keys(node.states).length + JSON.stringify(node.states); }, childClass: { template: resource('./template/variant.tmpl'), @@ -25,18 +26,18 @@ var view = new Node({ Node.prototype.init.call(this); // копируем объект и сортируем его свойства - this.condition = sortObject(basis.object.merge(this.condition)); + this.states = sortObject(basis.object.merge(this.states)); // удаляем false-состояния из комбинации - for (var stateName in this.condition) { - if (this.condition.hasOwnProperty(stateName) && this.condition[stateName] === false) { - delete this.condition[stateName]; + for (var stateName in this.states) { + if (this.states.hasOwnProperty(stateName) && this.states[stateName] === false) { + delete this.states[stateName]; } } }, binding: { - condition: function(node) { - return JSON.stringify(node.condition); + states: function(node) { + return JSON.stringify(node.states); }, html: 'html', isBlock: 'isBlock' @@ -45,6 +46,7 @@ var view = new Node({ }); var vBuilder; +var tGenerator; function rebuildStage() { var url = selectedTemplate.value; @@ -55,24 +57,28 @@ function rebuildStage() { if (url) { if (vBuilder) { - vBuilder.destroy(); - vBuilder.styles.forEach(function(style) { - style.original.detach(rebuildStage); + tGenerator.getStyles().forEach(function(style) { + style.physResource.detach(rebuildStage); }); + vBuilder.destroy(); + tGenerator.destroy(); } - var resource = basis.resource(url); + tGenerator = new Generator({url: url}); + vBuilder = new Builder({generator: tGenerator, emulators: basis.object.values(emulators)}); - vBuilder = new VariantBuilder(resource, Object.keys(emulators).map(function(name) { - return emulators[name]; - })); - - vBuilder.styles.forEach(function(style) { - style.original.attach(rebuildStage); + tGenerator.getStyles().forEach(function(style) { + style.physResource.attach(rebuildStage); }); - ignoreCount.set(vBuilder.getIgnoredCount()); - view.setChildNodes(vBuilder.getAcceptedVariants()); + ignoreCount.set(vBuilder.ignoredVariantsCount); + view.setChildNodes(vBuilder.variants.map(function(variant) { + return { + states: variant.states, + html: variant.wrapper, + isBlock: variant.isBlock + }; + })); } } diff --git a/src/module/stage/template/variant.tmpl b/src/module/stage/template/variant.tmpl index c59fce4..d8d17f7 100644 --- a/src/module/stage/template/variant.tmpl +++ b/src/module/stage/template/variant.tmpl @@ -5,7 +5,7 @@
- {condition} + {states}
diff --git a/src/utils/builder.js b/src/utils/builder.js deleted file mode 100644 index a002973..0000000 --- a/src/utils/builder.js +++ /dev/null @@ -1,324 +0,0 @@ -var domUtils = require('basis.dom'); -var parse = require('csso:parser/index'); -var walk = require('utils.walker').all; -var sortObject = require('utils.index').sortObject; -var translate = require('csso:utils/translate.js'); -var StyleDOMMapper = require('utils.styleDomMapper'); -var makeDeclaration = require('basis.template.declaration').makeDeclaration; -var templateBuilder = require('basis.template.html').Template.prototype.builder; - -function Builder(resource, emulators) { - var bindings; - - this.resource = resource; - this.decl = makeDeclaration(resource.fetch(), basis.path.dirname(resource.url), null, resource.url); - this.decl.instances = {}; - this.templateBuilder = templateBuilder.call({source: resource}, this.decl.tokens, this.decl.instances); - this.emulators = { - '*': [] - }; - this.variants = []; - this.ignoredVariantsCount = 0; - this.allVariants = []; - this.digest = {}; - this.styles = []; - this.states = []; - - this.decl.styles.forEach(function(style) { - var resource; - var sourceAST; - var processedAST; - var processedStyle; - - if (!style.resource) { - return; - } - - resource = basis.resource(style.resource); - sourceAST = parse(resource.fetch().cssText); - processedAST = parse(resource.fetch().cssText); - processedStyle = basis.resource.virtual('css', resource.fetch().cssText, style.resource).fetch(); - - processedStyle.url = style.resource; - processedStyle.baseURI = basis.path.dirname(style.sourceUrl) + '/'; - processedStyle.startUse(); - - this.styles.push({ - isOwnStyle: !style.includeToken, - original: resource, - sourceAST: sourceAST, - processed: processedStyle, - processedAST: processedAST, - sourceMap: this.generateSourceMap(sourceAST, processedAST) - }); - }, this); - - bindings = this.decl.states; - - for (var name in bindings) { - if (bindings.hasOwnProperty(name)) { - var values = []; - - if (bindings[name].bool) { - values.push(false, true); - } - - if (bindings[name].enum) { - values.push.apply(values, bindings[name].enum); - } - - values.forEach(function(value) { - var state = {}; - - state[name] = value; - this.states.push(state); - }, this); - } - } - - this.styles.forEach(function(style) { - this.states = this.states.concat(this.handleStyle(style, basis.array.from(emulators))); - }, this); - - this.states = this.combine(this.states); - - if (!this.states.length) { - this.states.push({hasState: false}); - } - - this.states.forEach(function(state) { - var newVariant = this.buildVariant(state); - - for (var stateName in state) { - var emulators; - - if (!state.hasOwnProperty(stateName)) { - continue; - } - - emulators = this.emulators[stateName] || []; - emulators = emulators.concat(this.emulators['*']); - - if (emulators.length) { - this.styles.forEach(function(style) { - var needToRetranslate = false; - - walk(style.processedAST, { - SimpleSelector: function(token, parent) { - token.sequence.each(function(part) { - emulators.forEach(function(emulator) { - needToRetranslate = emulator.emulate(part, parent, style.processedAST, style.sourceMap, newVariant.mapper, state[stateName]) || needToRetranslate; - }); - }); - } - }); - - if (needToRetranslate) { - style.processed.updateCssText(translate(style.processedAST)); - } - }); - } - } - - this.addVariant(newVariant); - }, this); - - console.log('STATES', this.states); - console.log('EMULATORS', this.emulators); - - console.log('======================='); - console.log(this); -} - -Builder.prototype.destroy = function() { - this.styles.forEach(function(style) { - style.processed.stopUse(); - }); -}; - -Builder.prototype.generateSourceMap = function(sourceAst, processedAST) { - var sourceTokens = []; - var processedTokens = []; - var sourceMap = new WeakMap(); - - // todo возможно есть более простой способ сфлэтить все селекторы из ast - walk(sourceAst, { - SimpleSelector: function(token) { - token.sequence.each(function(part) { - sourceTokens.push(part) - }); - } - }); - - walk(processedAST, { - SimpleSelector: function(token) { - token.sequence.each(function(part) { - processedTokens.push(part) - }); - } - }); - - processedTokens.forEach(function(pToken, key) { - sourceMap.set(pToken, sourceTokens[key]); - }); - - return sourceMap; -}; - -Builder.prototype.handleStyle = function(style, emulators) { - var states = {}; - - walk(style.processedAST, { - SimpleSelector: function(token, parent) { - token.sequence.each(function(part) { - emulators.forEach(function(emulator) { - var newStates; - - emulator.handleToken(part, parent, style.processedAST, style.sourceMap); - newStates = emulator.getStates(style.sourceAST) || []; - - if (!newStates.length) { - if (this.emulators['*'].indexOf(emulator) < 0) { - this.emulators['*'].push(emulator) - } - } - - newStates.forEach(function(state) { - var stateString = JSON.stringify(state); - - if (!states[stateString]) { - for (var stateName in state) { - if (state.hasOwnProperty(stateName)) { - this.emulators[stateName] = this.emulators[stateName] || []; - - if (this.emulators[stateName].indexOf(emulator) < 0) { - this.emulators[stateName].push(emulator) - } - } - } - - states[stateString] = state; - } - }, this); - }, this); - }, this); - } - }, this); - style.processed.updateCssText(translate(style.processedAST)); - - return basis.object.values(states); -}; - -Builder.prototype.getDigest = function(node) { - document.body.appendChild(node); - var result = domUtils.axis(node, domUtils.AXIS_DESCENDANT).map(function(node) { - if (node.nodeType == 1) { - return getComputedStyle(node).cssText; - } - - if (node.nodeType == 3) { - return node.nodeValue; - } - - return ''; - }).join(''); - - document.body.removeChild(node); - - return result; -}; - -Builder.prototype.getFirstNodeStyle = function(html) { - var node = html.firstChild; - var result = {}; - - document.body.appendChild(html); - - if (node.nodeType == 1) { - result = basis.object.slice(getComputedStyle(node)); - } - - document.body.removeChild(html); - - return result; -}; - -Builder.prototype.buildVariant = function(state) { - var tmpl = this.templateBuilder.createInstance(); - var buffer = document.createElement('div'); - var styleDOMMapper = new StyleDOMMapper(); - - for (var name in state) { - if (state.hasOwnProperty(name)) { - tmpl.set(name, state[name]); - } - } - - buffer.appendChild(tmpl.element.parentNode || tmpl.element); - - this.styles.forEach(function(style) { - styleDOMMapper.map(buffer, style.processedAST, style.sourceMap); - }, this); - - return { - mapper: styleDOMMapper, - condition: state, - html: buffer - }; -}; - -Builder.prototype.addVariant = function(candidate) { - var digest; - - if (this.allVariants.indexOf(candidate) > -1) { - return; - } - - this.allVariants.push(candidate); - - digest = this.getDigest(candidate.html); - - if (!this.digest.hasOwnProperty(digest)) { - var firstNodeStyle = this.getFirstNodeStyle(candidate.html); - - candidate.isBlock = firstNodeStyle.display == 'block'; - this.digest[digest] = true; - this.variants.push(candidate); - } else { - this.ignoredVariantsCount++; - } -}; - -Builder.prototype.getAcceptedVariants = function() { - return this.variants; -}; - -Builder.prototype.getIgnoredCount = function() { - return this.ignoredVariantsCount; -}; - -// fixme подумать над тем, нужно ли выбирать все комбинации, при большом количестве комбинаций получаем тормоза при формировании комбинаций -Builder.prototype.combine = function(states) { - var newStates = {}; - - states.forEach(function(state) { - Object.keys(newStates).forEach(function(stateName) { - var stateToAdd = basis.object.merge(state, newStates[stateName]); - var objectString = JSON.stringify(sortObject(stateToAdd)); - - if (!newStates[objectString]) { - newStates[objectString] = stateToAdd; - } - }); - - var objectString = JSON.stringify(sortObject(state)); - - if (!newStates[objectString]) { - newStates[objectString] = state; - } - }); - - return basis.object.values(newStates); -}; - -module.exports = Builder; diff --git a/src/utils/dom.js b/src/utils/dom.js new file mode 100644 index 0000000..b37c3f1 --- /dev/null +++ b/src/utils/dom.js @@ -0,0 +1,40 @@ +var domUtils = require('basis.dom'); + +function getDigest(node) { + document.body.appendChild(node); + var result = domUtils.axis(node, domUtils.AXIS_DESCENDANT).map(function(node) { + if (node.nodeType == 1) { + return getComputedStyle(node).cssText; + } + + if (node.nodeType == 3) { + return node.nodeValue; + } + + return ''; + }).join(''); + + document.body.removeChild(node); + + return result; +} + +function getFirstNodeStyle(html) { + var node = html.firstChild; + var result = {}; + + document.body.appendChild(html); + + if (node.nodeType == 1) { + result = basis.object.slice(getComputedStyle(node)); + } + + document.body.removeChild(html); + + return result; +} + +module.exports = { + getDigest: getDigest, + getFirstNodeStyle: getFirstNodeStyle +}; diff --git a/src/utils/emulators/nthFactory.js b/src/utils/emulators/nthFactory.js deleted file mode 100644 index b64180e..0000000 --- a/src/utils/emulators/nthFactory.js +++ /dev/null @@ -1,71 +0,0 @@ -var List = require('csso:utils/list.js'); - -module.exports = function nthFactory(nth, element) { - var normalizedArgs; - var classMap = { - 'first-child': { - a: '0n', - b: 1 - }, - 'last-child': { - a: '0n', - b: element.parentNode.children.length - } - }; - var argMap = { - even: { - a: '2n', - b: 0 - }, - odd: { - a: '2n', - b: 1 - } - }; - - if (['first-child', 'last-child', 'nth-child', 'nth-child'].indexOf(nth.name) == -1) { - return; - } - - if (nth.type == 'PseudoClass') { - if (!(normalizedArgs = classMap[nth.name])) { - throw new Error('Something awful was happened...'); - } - } else if (nth.type == 'FunctionalPseudo') { - var args = nth.arguments.first().sequence.toArray(); - - if (args.length == 1) { - normalizedArgs = argMap[args[0].value] || { - a: args[0].value, - b: 0 - } - } else if (args.length == 3) { - normalizedArgs = { - a: args[0].value, - b: args[2].value - } - } else { - throw new Error('Something awful was happened...'); - } - } - - return { - change: function(amount) { - normalizedArgs.b += amount; - }, - apply: function() { - var args = new List([{ - type: 'Argument', - sequence: new List([ - {type: 'Nth', value: normalizedArgs.a}, - {type: 'Operator', value: '+'}, - {type: 'Nth', value: normalizedArgs.b} - ]) - }]); - - nth.name = 'nth-child'; - nth.type = 'FunctionalPseudo'; - nth.arguments = args; - } - } -}; diff --git a/src/utils/emulators/nthHandler.js b/src/utils/emulators/nthHandler.js deleted file mode 100644 index 3fb6e3a..0000000 --- a/src/utils/emulators/nthHandler.js +++ /dev/null @@ -1,23 +0,0 @@ -var List = require('csso:utils/list.js'); - -module.exports = { - getStates: function() { - return []; - }, - handleToken: function(token, parent, root, sourceMap) { - if (token.type == 'PseudoClass' && token.name == 'only-child') { - var sourceToken = sourceMap.get(token); - var newToken = List.createItem({type: 'PseudoClass', name: 'last-child'}); - - token.name = 'first-child'; - - sourceMap.delete(token); - sourceMap.set(newToken.data, sourceToken); - - parent.data.sequence.insert(newToken); - } - }, - emulate: function(token, parent, root, sourceMap, mapper, value) { - // nothing to do... - } -}; diff --git a/src/utils/index.js b/src/utils/index.js index 2d57405..fb029d8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -42,8 +42,6 @@ function sortObject(obj) { } module.exports = { - walk: require('./walker'), - selectorMapper: require('./styleDomMapper'), simpleEqual: simpleEqual, uniqueObjects: uniqueObjects, sortObject: sortObject diff --git a/src/utils/style.js b/src/utils/style.js new file mode 100644 index 0000000..1e213e0 --- /dev/null +++ b/src/utils/style.js @@ -0,0 +1,41 @@ +var parse = require('csso:parser/index'); +var walk = require('utils.walker').all; +var translate = require('csso:utils/translate.js'); + +function sourceMap(sourceAst, processedAST) { + var sourceTokens = []; + var processedTokens = []; + var sourceMap = new WeakMap(); + + // todo возможно есть более простой способ сфлэтить все селекторы из ast + walk(sourceAst, { + SimpleSelector: function(token) { + token.sequence.each(function(part) { + sourceTokens.push(part) + }); + } + }); + + walk(processedAST, { + SimpleSelector: function(token) { + token.sequence.each(function(part) { + processedTokens.push(part) + }); + } + }); + + processedTokens.forEach(function(pToken, key) { + sourceMap.set(pToken, sourceTokens[key]); + }); + + return sourceMap; +} + +function clone(sourceAST) { + return parse(translate(sourceAST)); +} + +module.exports = { + sourceMap: sourceMap, + clone: clone +}; From 988c599f3c60a816e3e5be14378e7e8b3290722f Mon Sep 17 00:00:00 2001 From: Sergey Melyukov Date: Wed, 14 Sep 2016 12:20:10 +0300 Subject: [PATCH 3/3] first/last/nth-child emulation --- data/box.css | 4 +- data/complex/child.css | 4 +- src/app/lib/emulators/nthFactory.js | 79 +++++++++++++++++++ src/app/lib/emulators/nthHandler.js | 23 ++++++ src/app/lib/emulators/pseudoClassFactory.js | 2 +- src/app/lib/emulators/pseudoElementFactory.js | 36 +++++++-- src/app/lib/styleDomMapper.js | 2 +- src/app/lib/variant.js | 4 +- 8 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/app/lib/emulators/nthFactory.js create mode 100644 src/app/lib/emulators/nthHandler.js diff --git a/data/box.css b/data/box.css index e500a82..307a009 100644 --- a/data/box.css +++ b/data/box.css @@ -9,13 +9,13 @@ background: palegoldenrod; } -.test:before { +.test::before { content: 'before'; display: block; box-shadow: -10px -10px 10px red; } -.test:after { +.test::after { content: 'after'; /*content: 222;*/ /*content: "("attr(class)")";*/ diff --git a/data/complex/child.css b/data/complex/child.css index cc7116a..97d1f60 100644 --- a/data/complex/child.css +++ b/data/complex/child.css @@ -1,4 +1,4 @@ -.container::before1 +.container::before { content: 'before'; display: block; @@ -12,7 +12,7 @@ color: red; } -.block:first-child +.container .block:nth-child(odd) { color: blueviolet; font-size: large; diff --git a/src/app/lib/emulators/nthFactory.js b/src/app/lib/emulators/nthFactory.js new file mode 100644 index 0000000..a3d516a --- /dev/null +++ b/src/app/lib/emulators/nthFactory.js @@ -0,0 +1,79 @@ +var List = require('csso:utils/list.js'); + +module.exports = function nthFactory(nth, element, sourceMap) { + var sourceNth = sourceMap.get(nth) || {}; + var normalizedArgs; + var classMap = { + 'first-child': { + a: '0n', + b: 1 + }, + 'last-child': { + a: '0n', + b: element.parentNode.children.length + } + }; + var argMap = { + even: { + a: '2n', + b: 0 + }, + odd: { + a: '2n', + b: 1 + } + }; + + console.log(nth); + + if (['first-child', 'last-child', 'nth-child'].indexOf(nth.name) == -1) { + return; + } + + if (nth.type == 'PseudoClass') { + if (!(normalizedArgs = classMap[nth.name])) { + throw new Error('Something awful was happened...'); + } + } else if (nth.type == 'FunctionalPseudo') { + var args = nth.arguments.first().sequence.toArray(); + + if (args.length == 1) { + normalizedArgs = argMap[args[0].value] || { + a: args[0].value, + b: 0 + } + } else if (args.length == 3) { + normalizedArgs = { + a: args[0].value, + b: args[2].value + } + } else { + throw new Error('Something awful was happened...'); + } + } + + return { + change: function(amount) { + console.log(nth.name, amount); + if (amount < 0 || sourceNth.name == 'last-child' && amount > 0) { + return; + } + + normalizedArgs.b += amount; + }, + apply: function() { + var args = new List([{ + type: 'Argument', + sequence: new List([ + {type: 'Nth', value: normalizedArgs.a}, + {type: 'Operator', value: '+'}, + {type: 'Nth', value: normalizedArgs.b} + ]) + }]); + + nth.name = 'nth-child'; + nth.type = 'FunctionalPseudo'; + nth.arguments = args; + } + } +}; diff --git a/src/app/lib/emulators/nthHandler.js b/src/app/lib/emulators/nthHandler.js new file mode 100644 index 0000000..3fb6e3a --- /dev/null +++ b/src/app/lib/emulators/nthHandler.js @@ -0,0 +1,23 @@ +var List = require('csso:utils/list.js'); + +module.exports = { + getStates: function() { + return []; + }, + handleToken: function(token, parent, root, sourceMap) { + if (token.type == 'PseudoClass' && token.name == 'only-child') { + var sourceToken = sourceMap.get(token); + var newToken = List.createItem({type: 'PseudoClass', name: 'last-child'}); + + token.name = 'first-child'; + + sourceMap.delete(token); + sourceMap.set(newToken.data, sourceToken); + + parent.data.sequence.insert(newToken); + } + }, + emulate: function(token, parent, root, sourceMap, mapper, value) { + // nothing to do... + } +}; diff --git a/src/app/lib/emulators/pseudoClassFactory.js b/src/app/lib/emulators/pseudoClassFactory.js index fb235e6..7fb552c 100644 --- a/src/app/lib/emulators/pseudoClassFactory.js +++ b/src/app/lib/emulators/pseudoClassFactory.js @@ -30,7 +30,7 @@ module.exports = function pseudoClassFactory(type) { return states; } }, - handleToken: function(token, parent, root, sourceMap) { + handleToken: function(token, parent, root, sourceMap, mapper) { if (token.type == 'PseudoClass' && token.name == type) { token.type = 'Class'; token.name = TYPE_NAME; diff --git a/src/app/lib/emulators/pseudoElementFactory.js b/src/app/lib/emulators/pseudoElementFactory.js index 7e400df..adaeb0c 100644 --- a/src/app/lib/emulators/pseudoElementFactory.js +++ b/src/app/lib/emulators/pseudoElementFactory.js @@ -2,6 +2,7 @@ var List = require('csso:utils/list.js'); var walk = require('utils.walker').rules; var translate = require('csso:utils/translate.js'); var pseudoContentFactory = require('./contentFactory'); +var nthFactory = require('./nthFactory'); module.exports = function pseudoElementFactory(type, before) { var TYPE_NAME = 'pseudo-element-' + type + '__' + basis.genUID(); @@ -11,24 +12,36 @@ module.exports = function pseudoElementFactory(type, before) { getStates: function() { return false; }, - handleToken: function(token, parent, root, sourceMap) { + handleToken: function(token, parent, root, sourceMap, mapper) { if (token.type == 'PseudoElement' && token.name == type) { + var sourceToken = sourceMap.get(token); var newToken = List.createItem({type: 'Identifier', name: TYPE_NAME}); + var oldTokenElement; token.type = 'Combinator'; token.name = '>'; + sourceMap.delete(token); + sourceMap.set(newToken.data, sourceToken); + + oldTokenElement = mapper.bySelector(token); + + if (oldTokenElement) { + mapper.linkElements(newToken.data, oldTokenElement); + } + parent.data.sequence.insert(newToken); } }, emulate: function(token, parent, root, sourceMap, mapper, value) { - var needToRetranslate = false; var sourceToken = sourceMap.get(token); //console.log(token, '=>', sourceToken) if (sourceToken && sourceToken.type == 'PseudoElement' && sourceToken.name == type) { + //debugger; var elementHandler; var mappedElements = mapper.bySelector(token); + //console.log(token, mappedElements) var allowToEmulate = mappedElements && mappedElements.filter(function(element) { var list = new List(parent.data.sequence.toArray().slice(0, -2)); @@ -49,7 +62,6 @@ module.exports = function pseudoElementFactory(type, before) { if (declaration.property.name == 'content') { declaration.property.name = '--dp-disabled-content'; - needToRetranslate = true; } } }); @@ -77,11 +89,25 @@ module.exports = function pseudoElementFactory(type, before) { elementHandler(emulator); mapper.linkElements(parent.data.sequence.last(), emulator); } + + basis.array.from(element.children).forEach(function(child) { + var selectors = mapper.byElement(child); + + if (selectors) { + selectors.forEach(function(token) { + var handleNth = nthFactory(token, child, sourceMap); + + if (handleNth) { + //console.log(selectors); + handleNth.change(before ? 1 : -1); + handleNth.apply(); + } + }); + } + }); } }); } - - return needToRetranslate; } } }; diff --git a/src/app/lib/styleDomMapper.js b/src/app/lib/styleDomMapper.js index db1f0a2..bf6dc08 100644 --- a/src/app/lib/styleDomMapper.js +++ b/src/app/lib/styleDomMapper.js @@ -51,7 +51,7 @@ StyleDOMMapper.prototype.linkElements = function(token, elements) { var selector = translate(token); var mappedElements = this.selectorElementMap.get(token) || []; - elements = Array.isArray(elements) ? element : basis.array.from(elements) + elements = Array.isArray(elements) ? elements : basis.array.from(elements); elements.forEach(function(element) { var mappedSelectors = this.elementSelectorMap.get(element) || []; diff --git a/src/app/lib/variant.js b/src/app/lib/variant.js index b462706..ab12a3f 100644 --- a/src/app/lib/variant.js +++ b/src/app/lib/variant.js @@ -52,8 +52,8 @@ module.exports = basis.Class(null, { var emulators = getAllEmulators(this.emulators); emulators.forEach(function(emulator) { - emulator.handleToken(part, parent, resource.AST, resource.sourceMap); - }); + emulator.handleToken(part, parent, resource.AST, resource.sourceMap, this.styleDOMMapper); + }, this); }, this); } }, this);