From 57da5b9bbec12cc14ff35d9b447f54ca32fdb3c3 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Tue, 24 May 2016 23:35:57 +0200 Subject: [PATCH] Start CSS parser --- src/parser/model/ParserCss.js | 92 ++++++++++++++++++++++- src/parser/model/ParserHtml.js | 53 ++++++++++--- test/specs/parser/main.js | 7 +- test/specs/parser/model/ParserCss.js | 102 ++++++++++++++++++++++++++ test/specs/parser/model/ParserHtml.js | 75 ++++++++++++++++++- 5 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 test/specs/parser/model/ParserCss.js diff --git a/src/parser/model/ParserCss.js b/src/parser/model/ParserCss.js index 6cfd90132..715b70cd9 100644 --- a/src/parser/model/ParserCss.js +++ b/src/parser/model/ParserCss.js @@ -4,8 +4,98 @@ define(function(require) { return { + /** + * Parse selector string to array. + * Only concatenated classes are valid as CSS rules inside editor. + * It's ok with the last part of the string as state (:hover, :active) + * @param {string} str Selectors string + * @return {Array} + * @example + * var res = ParserCss.parseSelector('.test1, .test1.test2, .test2.test3'); + * console.log(res); + * // [['test1'], ['test1', 'test2'], ['test2', 'test3']] + */ + parseSelector: function(str){ + var result = []; + var sels = str.split(','); + for (var i = 0, len = sels.length; i < len; i++) { + var sel = sels[i].trim(); + // Will accept only concatenated classes and last + // class might be with state (eg. :hover), nothing else. + if (/^(\.{1}[\w\-]+)+(:{1}[\w\-]+)?$/ig.test(sel)) { + var cls = sel.split('.').filter(Boolean); + result.push(cls); + } + } + return result; + }, + + /** + * Fetch data from node + * @param {StyleSheet|CSSMediaRule} el + * @return {Array} + */ + parseNode: function(el){ + var result = []; + var nodes = el.cssRules; + + for (var i = 0, len = nodes.length; i < len; i++) { + var node = nodes[i]; + var sels = node.selectorText; // CSSMediaRule has conditionText (screen and (min-width: 480px)) + + //if(node.cssRules) + // it's a CSSMediaRule, need to go deeper + + if(!sels) + continue; + + sels = this.parseSelector(sels); + + // Create style object from the big one + var stl = node.style; + var style = {}; + for(var j = 0, len2 = stl.length; j < len2; j++){ + style[stl[j]] = stl[stl[j]]; + } + + // For each group of selectors + for (var k = 0, len3 = sels.length; k < len3; k++) { + var selArr = sels[k]; + var model = {}; + model.selectors = selArr; + model.style = style; + result.push(model); + } + + } + + return result; + }, + + /** + * Parse CSS string to a desired model object + * @param {string} str HTML string + * @return {Object|Array} + */ parse: function(str){ - return {parsed: 'CSS '+str}; + var el = document.createElement('style'); + /* + el.innerHTML = ".cssClass {border: 2px solid black; background-color: blue;} " + + ".red, .red2 {color:red; padding:5px} .test1.red {color:black} .red:hover{color: blue} " + + "@media screen and (min-width: 480px){ .red{color:white} }"; + */ + el.innerHTML = str; + + // There is no .sheet without adding it to the + document.head.appendChild(el); + var sheet = el.sheet; + document.head.removeChild(el); + var result = this.parseNode(sheet); + + if(result.length == 1) + result = result[0]; + + return result; }, }; diff --git a/src/parser/model/ParserHtml.js b/src/parser/model/ParserHtml.js index 2d91eab8f..6009fb2be 100644 --- a/src/parser/model/ParserHtml.js +++ b/src/parser/model/ParserHtml.js @@ -48,23 +48,20 @@ define(function(require) { }, /** - * Parse HTML string to a desired model object - * @param {string} str HTML string - * @return {Object} + * Fetch data from node + * @param {HTMLElement} el DOM + * @return {Array} */ - parse: function(str){ - var el = document.createElement('div'); - el.innerHTML = str; - var nodes = el.childNodes; + parseNode: function(el){ var result = []; + var nodes = el.childNodes; - // Iterate all nodes for (var i = 0, len = nodes.length; i < len; i++) { var node = nodes[i]; var model = {}; - var attrs = node.attributes; + var attrs = node.attributes || []; var attrsLen = attrs.length; - model.tagName = node.tagName.toLowerCase(); + model.tagName = node.tagName ? node.tagName.toLowerCase() : ''; if(attrsLen) model.attributes = {}; @@ -74,18 +71,50 @@ define(function(require) { var nodeName = attrs[j].nodeName; var nodeValue = attrs[j].nodeValue; - //Isolate style and class attributes + //Isolate style, class and src attributes if(nodeName === 'style') model.style = this.parseStyle(nodeValue); else if(nodeName === 'class') model.classes = this.parseClass(nodeValue); - else + else if(nodeName === 'src' && model.tagName === 'img'){ + model.type = 'image'; + model.src = nodeValue; + }else model.attributes[nodeName] = nodeValue; } + // Check for nested elements + if(node.childNodes.length) + model.components = this.parseNode(node); + + // Find text nodes + if(!model.tagName && node.nodeType === 3 && node.nodeValue.trim()){ + model.type = 'text'; + model.tagName = 'span'; + model.content = node.nodeValue; + } + + // If tagName is still empty do not push it + if(!model.tagName) + continue; + result.push(model); } + return result; + }, + + /** + * Parse HTML string to a desired model object + * @param {string} str HTML string + * @return {Object} + */ + parse: function(str){ + var el = document.createElement('div'); + el.innerHTML = str; + var nodes = el.childNodes; + var result = this.parseNode(el); + if(result.length == 1) result = result[0]; diff --git a/test/specs/parser/main.js b/test/specs/parser/main.js index 298e07cc0..612d6986d 100644 --- a/test/specs/parser/main.js +++ b/test/specs/parser/main.js @@ -2,16 +2,19 @@ var modulePath = './../../../test/specs/parser'; define([ 'Parser', - modulePath + '/model/ParserHtml' + modulePath + '/model/ParserHtml', + modulePath + '/model/ParserCss' ], function( Parser, - ParserHtml + ParserHtml, + ParserCss ) { describe('Parser', function() { ParserHtml.run(); + ParserCss.run(); }); }); \ No newline at end of file diff --git a/test/specs/parser/model/ParserCss.js b/test/specs/parser/model/ParserCss.js new file mode 100644 index 000000000..6a0083674 --- /dev/null +++ b/test/specs/parser/model/ParserCss.js @@ -0,0 +1,102 @@ +var path = 'Parser/'; +define([path + 'model/ParserCss',], + function(ParserCss) { + + return { + run : function(){ + + describe('ParserCss', function() { + var obj; + + beforeEach(function () { + obj = new ParserCss(); + }); + + afterEach(function () { + delete obj; + }); + + it('Parse selector', function() { + var str = '.test'; + var result = [['test']]; + obj.parseSelector(str).should.deep.equal(result); + }); + + it('Parse selectors', function() { + var str = '.test1, .test1.test2, .test2.test3'; + var result = [['test1'], ['test1', 'test2'], ['test2', 'test3']]; + obj.parseSelector(str).should.deep.equal(result); + }); + + it('Ignore not valid selectors', function() { + var str = '.test1.test2, .test2 .test3, div > .test4, #test.test5, .test6'; + var result = [['test1', 'test2'], ['test6']]; + obj.parseSelector(str).should.deep.equal(result); + }); + + it('Parse selectors with state', function() { + var str = '.test1. test2, .test2>test3, .test4.test5:hover'; + var result = [['test4', 'test5:hover']]; + obj.parseSelector(str).should.deep.equal(result); + }); + + it('Parse simple rule', function() { + var str = ' .test1 {color:red; width: 50px }'; + var result = { + selectors: ['test1'], + style: { + color: 'red', + width: '50px', + } + }; + obj.parse(str).should.deep.equal(result); + }); + + it('Parse rule with more selectors', function() { + var str = ' .test1.test2 {color:red; test: value}'; + var result = { + selectors: ['test1', 'test2'], + style: { color: 'red'} + }; + obj.parse(str).should.deep.equal(result); + }); + + it('Parse same rule with more selectors', function() { + var str = ' .test1.test2, .test3{ color:red }'; + var result = [{ + selectors: ['test1', 'test2'], + style: { color: 'red'} + },{ + selectors: ['test3'], + style: { color: 'red'} + }]; + obj.parse(str).should.deep.equal(result); + }); + + it('Parse more rules', function() { + var str = ' .test1.test2, .test3{ color:red } .test4, .test5.test6{ width:10px }'; + var result = [{ + selectors: ['test1', 'test2'], + style: { color: 'red'} + },{ + selectors: ['test3'], + style: { color: 'red'} + },{ + selectors: ['test4'], + style: { width: '10px'} + },{ + selectors: ['test5', 'test6'], + style: { width: '10px'} + }]; + obj.parse(str).should.deep.equal(result); + }); + + it.skip('Parse rule with state', function() { + }); + + }); + + } + }; + +}); \ No newline at end of file diff --git a/test/specs/parser/model/ParserHtml.js b/test/specs/parser/model/ParserHtml.js index 229334d68..a4c128764 100644 --- a/test/specs/parser/model/ParserHtml.js +++ b/test/specs/parser/model/ParserHtml.js @@ -82,16 +82,83 @@ define([path + 'model/ParserHtml',], obj.parse(str).should.deep.equal(result); }); - it.skip('Parse nested nodes', function() { - + it('Parse images nodes', function() { + var str = ''; + var result = { + tagName: 'img', + type: 'image', + src: './index.html', + attributes: { id: 'test1'}, + }; + obj.parse(str).should.deep.equal(result); }); - it.skip('Parse images nodes', function() { + it('Parse text nodes', function() { + var str = '
test2
'; + var result = { + tagName: 'div', + attributes: { id: 'test1'}, + components: [{ + tagName: 'span', + type: 'text', + content: 'test2 ', + }], + }; + obj.parse(str).should.deep.equal(result); + }); + it('Parse nested nodes', function() { + var str = '
Text mid
'; + var result = { + tagName: 'article', + attributes: {id: 'test1'}, + components: [ + { + tagName: 'div' + },{ + tagName: 'footer', + attributes: { id: 'test2'}, + },{ + tagName: 'span', + type: 'text', + content: ' Text mid ', + },{ + tagName: 'div', + attributes: { id: 'last'}, + }, + ] + }; + obj.parse(str).should.deep.equal(result); }); - it.skip('Parse text nodes', function() { + it('Parse nested text nodes', function() { + var str = '
content1
nested
content2
'; + var result = { + tagName: 'div', + components: [{ + tagName: 'span', + type: 'text', + content: 'content1 ', + },{ + tagName: 'div', + components: [{ + tagName: 'span', + type: 'text', + content: 'nested', + }] + },{ + tagName: 'span', + type: 'text', + content: ' content2', + }], + }; + obj.parse(str).should.deep.equal(result); + }); + it('Parse multiple nodes', function() { + var str = '
'; + var result = [{ tagName: 'div'},{ tagName: 'div'}]; + obj.parse(str).should.deep.equal(result); }); });