From 08d4a37c069bf11971666bb2aac874bc573c9154 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 28 Oct 2014 14:46:55 -0700 Subject: [PATCH] feat(selector): initial version of the selector --- modules/benchmarks/src/compiler/benchmark.es5 | 17 ++ modules/benchmarks/src/compiler/bp.conf.es5 | 11 + modules/benchmarks/src/compiler/main.html | 0 .../src/compiler/register_system.es5 | 10 + .../src/compiler/selector_benchmark.js | 81 ++++++ modules/core/src/compiler/selector.js | 237 +++++++++++++++++- modules/core/test/compiler/selector_spec.js | 154 ++++++++++++ 7 files changed, 498 insertions(+), 12 deletions(-) create mode 100644 modules/benchmarks/src/compiler/benchmark.es5 create mode 100644 modules/benchmarks/src/compiler/bp.conf.es5 create mode 100644 modules/benchmarks/src/compiler/main.html create mode 100644 modules/benchmarks/src/compiler/register_system.es5 create mode 100644 modules/benchmarks/src/compiler/selector_benchmark.js create mode 100644 modules/core/test/compiler/selector_spec.js diff --git a/modules/benchmarks/src/compiler/benchmark.es5 b/modules/benchmarks/src/compiler/benchmark.es5 new file mode 100644 index 0000000000..9e007b302c --- /dev/null +++ b/modules/benchmarks/src/compiler/benchmark.es5 @@ -0,0 +1,17 @@ +System.import('benchmarks/compiler/selector_benchmark').then(function (bm) { + bm.setup(); + + window.benchmarkSteps.push({name: 'CssSelector.parse', fn: bm.runParse}); +}, console.log.bind(console)); + +System.import('benchmarks/compiler/selector_benchmark').then(function (bm) { + bm.setup(); + + window.benchmarkSteps.push({name: 'SelectorMatcher.addSelectable', fn: bm.runAdd}); +}, console.log.bind(console)); + +System.import('benchmarks/compiler/selector_benchmark').then(function (bm) { + bm.setup(); + + window.benchmarkSteps.push({name: 'SelectorMatcher.match', fn: bm.runMatch}); +}, console.log.bind(console)); diff --git a/modules/benchmarks/src/compiler/bp.conf.es5 b/modules/benchmarks/src/compiler/bp.conf.es5 new file mode 100644 index 0000000000..b9ff8a4015 --- /dev/null +++ b/modules/benchmarks/src/compiler/bp.conf.es5 @@ -0,0 +1,11 @@ +module.exports = function(config) { + config.set({ + scripts: [ + {src: '/js/traceur-runtime.js'}, + {src: '/js/es6-module-loader-sans-promises.src.js'}, + {src: '/js/extension-register.js'}, + {src: 'register_system.js'}, + {src: 'benchmark.js'} + ] + }); +}; diff --git a/modules/benchmarks/src/compiler/main.html b/modules/benchmarks/src/compiler/main.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/benchmarks/src/compiler/register_system.es5 b/modules/benchmarks/src/compiler/register_system.es5 new file mode 100644 index 0000000000..388ec08a16 --- /dev/null +++ b/modules/benchmarks/src/compiler/register_system.es5 @@ -0,0 +1,10 @@ +System.paths = { + 'core/*': '/js/core/lib/*.js', + 'change_detection/*': '/js/change_detection/lib/*.js', + 'facade/*': '/js/facade/lib/*.js', + 'di/*': '/js/di/lib/*.js', + 'rtts_assert/*': '/js/rtts_assert/lib/*.js', + 'test_lib/*': '/js/test_lib/lib/*.js', + 'benchmarks/*': '/js/benchmarks/lib/*.js' +}; +register(System); diff --git a/modules/benchmarks/src/compiler/selector_benchmark.js b/modules/benchmarks/src/compiler/selector_benchmark.js new file mode 100644 index 0000000000..991cd981da --- /dev/null +++ b/modules/benchmarks/src/compiler/selector_benchmark.js @@ -0,0 +1,81 @@ +import {SelectorMatcher, CssSelector} from "core/compiler/selector"; +import {StringWrapper, Math} from 'facade/lang'; +import {ListWrapper} from 'facade/collection'; + +var fixedMatcher; +var fixedSelectorStrings = []; +var fixedSelectors = []; + +var COUNT = 1000; + +export function setup() { + for (var i=0; i { + count += selected; + }); + } + return count; +} + +function randomSelector() { + var res = randomStr(5); + for (var i=0; i<3; i++) { + res += '.'+randomStr(5); + } + for (var i=0; i<3; i++) { + res += '['+randomStr(3)+'='+randomStr(6)+']'; + } + return res; +} + +function randomStr(len){ + var s = ''; + while (s.length < len) { + s += randomChar(); + } + return s; +} + +function randomChar(){ + var n = randomNum(62); + if(n<10) return n; //1-10 + if(n<36) return StringWrapper.fromCharCode(n+55); //A-Z + return StringWrapper.fromCharCode(n+61); //a-z +} + +function randomNum(max) { + return Math.floor(Math.random() * max); +} diff --git a/modules/core/src/compiler/selector.js b/modules/core/src/compiler/selector.js index aed4c2ac41..c08c7ef438 100644 --- a/modules/core/src/compiler/selector.js +++ b/modules/core/src/compiler/selector.js @@ -1,19 +1,232 @@ -import {Set} from 'facade/lang'; -//import {AnnotatedType} from './annotated_type'; +import {List, ListWrapper, StringMapWrapper} from 'facade/collection'; +import {RegExpWrapper, RegExpMatcherWrapper, CONST, isPresent, isBlank} from 'facade/lang'; -export class Selector { - constructor(directives:Set) { - this.directives = directives; +const _EMPTY_ATTR_VALUE = ''; + +export class SelectorMatcher { + /* TODO: Add these fields when the transpiler supports fields + _elementMap:Map; + _elementPartialMap:Map; + + _classMap:Map; + _classPartialMap:Map; + + _attrValueMap:Map>; + _attrValuePartialMap:Map>; + */ + constructor() { + this._selectables = ListWrapper.create(); + + this._elementMap = StringMapWrapper.create(); + this._elementPartialMap = StringMapWrapper.create(); + + this._classMap = StringMapWrapper.create(); + this._classPartialMap = StringMapWrapper.create(); + + this._attrValueMap = StringMapWrapper.create(); + this._attrValuePartialMap = StringMapWrapper.create(); } /** - * When presented with an element description it will return the current set of - * directives which are present on the element. - * - * @param elementName Name of the element - * @param attributes Attributes on the Element. + * Add an object that can be found later on by calling `match`. + * @param cssSelector A css selector + * @param selectable An opaque object that will be given to the callback of the `match` function */ - visitElement(elementName:string, attributes:Map):List { - return null; + addSelectable(cssSelector:CssSelector, selectable) { + var matcher = this; + var element = cssSelector.element; + var classNames = cssSelector.classNames; + var attrs = cssSelector.attrs; + + if (isPresent(element)) { + var isTerminal = attrs.length === 0 && classNames.length === 0; + if (isTerminal) { + this._addTerminal(matcher._elementMap, element, selectable); + } else { + matcher = this._addPartial(matcher._elementPartialMap, element); + } + } + + if (isPresent(classNames)) { + for (var index = 0; index, attrs:List) { + this.element = element; + this.classNames = classNames; + this.attrs = attrs; + } + + toString():string { + var res = ''; + if (isPresent(this.element)) { + res += this.element; + } + if (isPresent(this.classNames)) { + for (var i=0; i { + var matcher, matched, selectableCollector; + + function reset() { + matched = ListWrapper.create(); + } + + beforeEach(() => { + reset(); + selectableCollector = (selectable) => { + ListWrapper.push(matched, selectable); + } + matcher = new SelectorMatcher(); + }); + + it('should select by element name case insensitive', () => { + matcher.addSelectable(CssSelector.parse('someTag'), 1); + + matcher.match(CssSelector.parse('SOMEOTHERTAG'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('SOMETAG'), selectableCollector); + expect(matched).toEqual([1]); + }); + + it('should select by class name case insensitive', () => { + matcher.addSelectable(CssSelector.parse('.someClass'), 1); + matcher.addSelectable(CssSelector.parse('.someClass.class2'), 2); + + matcher.match(CssSelector.parse('.SOMEOTHERCLASS'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('.SOMECLASS'), selectableCollector); + expect(matched).toEqual([1]); + + reset(); + matcher.match(CssSelector.parse('.someClass.class2'), selectableCollector); + expect(matched).toEqual([1,2]); + }); + + it('should select by attr name case insensitive', () => { + matcher.addSelectable(CssSelector.parse('[someAttr]'), 1); + matcher.addSelectable(CssSelector.parse('[someAttr][someAttr2]'), 2); + + matcher.match(CssSelector.parse('[SOMEOTHERATTR]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('[SOMEATTR]'), selectableCollector); + expect(matched).toEqual([1]); + + reset(); + matcher.match(CssSelector.parse('[someAttr][someAttr2]'), selectableCollector); + expect(matched).toEqual([1,2]); + }); + + it('should select by attr name and value case insensitive', () => { + matcher.addSelectable(CssSelector.parse('[someAttr=someValue]'), 1); + + matcher.match(CssSelector.parse('[SOMEATTR=SOMEOTHERATTR]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('[SOMEATTR=SOMEVALUE]'), selectableCollector); + expect(matched).toEqual([1]); + }); + + it('should select by element name, class name and attribute name with value', () => { + matcher.addSelectable(CssSelector.parse('someTag.someClass[someAttr=someValue]'), 1); + + matcher.match(CssSelector.parse('someOtherTag.someOtherClass[someOtherAttr]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('someTag.someOtherClass[someOtherAttr]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('someTag.someClass[someOtherAttr]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('someTag.someClass[someAttr]'), selectableCollector); + expect(matched).toEqual([]); + + matcher.match(CssSelector.parse('someTag.someClass[someAttr=someValue]'), selectableCollector); + expect(matched).toEqual([1]); + }); + + it('should select independent of the order in the css selector', () => { + matcher.addSelectable(CssSelector.parse('[someAttr].someClass'), 1); + matcher.addSelectable(CssSelector.parse('.someClass[someAttr]'), 2); + matcher.addSelectable(CssSelector.parse('.class1.class2'), 3); + matcher.addSelectable(CssSelector.parse('.class2.class1'), 4); + + matcher.match(CssSelector.parse('[someAttr].someClass'), selectableCollector); + expect(matched).toEqual([1,2]); + + reset(); + matcher.match(CssSelector.parse('.someClass[someAttr]'), selectableCollector); + expect(matched).toEqual([1,2]); + + reset(); + matcher.match(CssSelector.parse('.class1.class2'), selectableCollector); + expect(matched).toEqual([3,4]); + + reset(); + matcher.match(CssSelector.parse('.class2.class1'), selectableCollector); + expect(matched).toEqual([4,3]); + }); + }); + + describe('CssSelector.parse', () => { + it('should detect element names', () => { + var cssSelector = CssSelector.parse('sometag'); + expect(cssSelector.element).toEqual('sometag'); + expect(cssSelector.toString()).toEqual('sometag'); + }); + + it('should detect class names', () => { + var cssSelector = CssSelector.parse('.someClass'); + expect(cssSelector.classNames).toEqual(['someclass']); + + expect(cssSelector.toString()).toEqual('.someclass'); + }); + + it('should detect attr names', () => { + var cssSelector = CssSelector.parse('[attrname]'); + var attr = ListWrapper.get(cssSelector.attrs, 0); + expect(attr.name).toEqual('attrname'); + expect(isPresent(attr.value)).toBe(false); + }); + + it('should detect attr values', () => { + var cssSelector = CssSelector.parse('[attrname=attrvalue]'); + var attr = ListWrapper.get(cssSelector.attrs, 0); + expect(attr.name).toEqual('attrname'); + expect(attr.value).toEqual('attrvalue'); + expect(cssSelector.toString()).toEqual('[attrname=attrvalue]'); + }); + + it('should detect multiple parts', () => { + var cssSelector = CssSelector.parse('sometag[attrname=attrvalue].someclass'); + expect(cssSelector.element).toEqual('sometag'); + var attr = ListWrapper.get(cssSelector.attrs, 0); + expect(attr.name).toEqual('attrname'); + expect(attr.value).toEqual('attrvalue'); + expect(cssSelector.classNames).toEqual(['someclass']); + + expect(cssSelector.toString()).toEqual('sometag.someclass[attrname=attrvalue]'); + }); + }); +} \ No newline at end of file