Skip to content

Commit 08d4a37

Browse files
committed
feat(selector): initial version of the selector
1 parent d0c870f commit 08d4a37

File tree

7 files changed

+498
-12
lines changed

7 files changed

+498
-12
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
System.import('benchmarks/compiler/selector_benchmark').then(function (bm) {
2+
bm.setup();
3+
4+
window.benchmarkSteps.push({name: 'CssSelector.parse', fn: bm.runParse});
5+
}, console.log.bind(console));
6+
7+
System.import('benchmarks/compiler/selector_benchmark').then(function (bm) {
8+
bm.setup();
9+
10+
window.benchmarkSteps.push({name: 'SelectorMatcher.addSelectable', fn: bm.runAdd});
11+
}, console.log.bind(console));
12+
13+
System.import('benchmarks/compiler/selector_benchmark').then(function (bm) {
14+
bm.setup();
15+
16+
window.benchmarkSteps.push({name: 'SelectorMatcher.match', fn: bm.runMatch});
17+
}, console.log.bind(console));
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = function(config) {
2+
config.set({
3+
scripts: [
4+
{src: '/js/traceur-runtime.js'},
5+
{src: '/js/es6-module-loader-sans-promises.src.js'},
6+
{src: '/js/extension-register.js'},
7+
{src: 'register_system.js'},
8+
{src: 'benchmark.js'}
9+
]
10+
});
11+
};

modules/benchmarks/src/compiler/main.html

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
System.paths = {
2+
'core/*': '/js/core/lib/*.js',
3+
'change_detection/*': '/js/change_detection/lib/*.js',
4+
'facade/*': '/js/facade/lib/*.js',
5+
'di/*': '/js/di/lib/*.js',
6+
'rtts_assert/*': '/js/rtts_assert/lib/*.js',
7+
'test_lib/*': '/js/test_lib/lib/*.js',
8+
'benchmarks/*': '/js/benchmarks/lib/*.js'
9+
};
10+
register(System);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {SelectorMatcher, CssSelector} from "core/compiler/selector";
2+
import {StringWrapper, Math} from 'facade/lang';
3+
import {ListWrapper} from 'facade/collection';
4+
5+
var fixedMatcher;
6+
var fixedSelectorStrings = [];
7+
var fixedSelectors = [];
8+
9+
var COUNT = 1000;
10+
11+
export function setup() {
12+
for (var i=0; i<COUNT; i++) {
13+
ListWrapper.push(fixedSelectorStrings, randomSelector());
14+
}
15+
for (var i=0; i<COUNT; i++) {
16+
ListWrapper.push(fixedSelectors, CssSelector.parse(fixedSelectorStrings[i]));
17+
}
18+
fixedMatcher = new SelectorMatcher();
19+
for (var i=0; i<COUNT; i++) {
20+
fixedMatcher.addSelectable(fixedSelectors[i], i);
21+
}
22+
}
23+
24+
export function runParse() {
25+
var result = [];
26+
for (var i=0; i<COUNT; i++) {
27+
ListWrapper.push(result, CssSelector.parse(fixedSelectorStrings[i]));
28+
}
29+
return result;
30+
}
31+
32+
export function runAdd() {
33+
// The sum is used to prevent Chrome from optimizing the loop away...
34+
var matcher = new SelectorMatcher();
35+
var count = 0;
36+
for (var i=0; i<COUNT; i++) {
37+
count += matcher.addSelectable(fixedSelectors[i], i);
38+
}
39+
return count;
40+
}
41+
42+
export function runMatch() {
43+
// The sum is used to prevent Chrome from optimizing the loop away...
44+
var count = 0;
45+
for (var i=0; i<COUNT; i++) {
46+
fixedMatcher.match(fixedSelectors[i], (selected) => {
47+
count += selected;
48+
});
49+
}
50+
return count;
51+
}
52+
53+
function randomSelector() {
54+
var res = randomStr(5);
55+
for (var i=0; i<3; i++) {
56+
res += '.'+randomStr(5);
57+
}
58+
for (var i=0; i<3; i++) {
59+
res += '['+randomStr(3)+'='+randomStr(6)+']';
60+
}
61+
return res;
62+
}
63+
64+
function randomStr(len){
65+
var s = '';
66+
while (s.length < len) {
67+
s += randomChar();
68+
}
69+
return s;
70+
}
71+
72+
function randomChar(){
73+
var n = randomNum(62);
74+
if(n<10) return n; //1-10
75+
if(n<36) return StringWrapper.fromCharCode(n+55); //A-Z
76+
return StringWrapper.fromCharCode(n+61); //a-z
77+
}
78+
79+
function randomNum(max) {
80+
return Math.floor(Math.random() * max);
81+
}
Lines changed: 225 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,232 @@
1-
import {Set} from 'facade/lang';
2-
//import {AnnotatedType} from './annotated_type';
1+
import {List, ListWrapper, StringMapWrapper} from 'facade/collection';
2+
import {RegExpWrapper, RegExpMatcherWrapper, CONST, isPresent, isBlank} from 'facade/lang';
33

4-
export class Selector {
5-
constructor(directives:Set<AnnotatedType>) {
6-
this.directives = directives;
4+
const _EMPTY_ATTR_VALUE = '';
5+
6+
export class SelectorMatcher {
7+
/* TODO: Add these fields when the transpiler supports fields
8+
_elementMap:Map<String, List>;
9+
_elementPartialMap:Map<String, Selector>;
10+
11+
_classMap:Map<String, List>;
12+
_classPartialMap:Map<String, Selector>;
13+
14+
_attrValueMap:Map<String, Map<String, List>>;
15+
_attrValuePartialMap:Map<String, Map<String, Selector>>;
16+
*/
17+
constructor() {
18+
this._selectables = ListWrapper.create();
19+
20+
this._elementMap = StringMapWrapper.create();
21+
this._elementPartialMap = StringMapWrapper.create();
22+
23+
this._classMap = StringMapWrapper.create();
24+
this._classPartialMap = StringMapWrapper.create();
25+
26+
this._attrValueMap = StringMapWrapper.create();
27+
this._attrValuePartialMap = StringMapWrapper.create();
728
}
829

930
/**
10-
* When presented with an element description it will return the current set of
11-
* directives which are present on the element.
12-
*
13-
* @param elementName Name of the element
14-
* @param attributes Attributes on the Element.
31+
* Add an object that can be found later on by calling `match`.
32+
* @param cssSelector A css selector
33+
* @param selectable An opaque object that will be given to the callback of the `match` function
1534
*/
16-
visitElement(elementName:string, attributes:Map<string, string>):List<AnnotatedType> {
17-
return null;
35+
addSelectable(cssSelector:CssSelector, selectable) {
36+
var matcher = this;
37+
var element = cssSelector.element;
38+
var classNames = cssSelector.classNames;
39+
var attrs = cssSelector.attrs;
40+
41+
if (isPresent(element)) {
42+
var isTerminal = attrs.length === 0 && classNames.length === 0;
43+
if (isTerminal) {
44+
this._addTerminal(matcher._elementMap, element, selectable);
45+
} else {
46+
matcher = this._addPartial(matcher._elementPartialMap, element);
47+
}
48+
}
49+
50+
if (isPresent(classNames)) {
51+
for (var index = 0; index<classNames.length; index++) {
52+
var isTerminal = attrs.length === 0 && index === classNames.length - 1;
53+
var className = classNames[index];
54+
if (isTerminal) {
55+
this._addTerminal(matcher._classMap, className, selectable);
56+
} else {
57+
matcher = this._addPartial(matcher._classPartialMap, className);
58+
}
59+
}
60+
}
61+
62+
if (isPresent(attrs)) {
63+
for (var index = 0; index<attrs.length; index++) {
64+
var isTerminal = index === attrs.length - 1;
65+
var attr = attrs[index];
66+
var attrName = attr.name;
67+
var attrValue = isPresent(attr.value) ? attr.value : _EMPTY_ATTR_VALUE;
68+
var map = isTerminal ? matcher._attrValueMap : matcher._attrValuePartialMap;
69+
var valuesMap = StringMapWrapper.get(map, attrName)
70+
if (isBlank(valuesMap)) {
71+
valuesMap = StringMapWrapper.create();
72+
StringMapWrapper.set(map, attrName, valuesMap);
73+
}
74+
if (isTerminal) {
75+
this._addTerminal(valuesMap, attrValue, selectable);
76+
} else {
77+
matcher = this._addPartial(valuesMap, attrValue);
78+
}
79+
}
80+
}
81+
}
82+
// TODO: map:StringMap when we have a StringMap type...
83+
_addTerminal(map, name:string, selectable) {
84+
var terminalList = StringMapWrapper.get(map, name)
85+
if (isBlank(terminalList)) {
86+
terminalList = ListWrapper.create();
87+
StringMapWrapper.set(map, name, terminalList);
88+
}
89+
ListWrapper.push(terminalList, selectable);
90+
}
91+
// TODO: map:StringMap when we have a StringMap type...
92+
_addPartial(map, name:string) {
93+
var matcher = StringMapWrapper.get(map, name)
94+
if (isBlank(matcher)) {
95+
matcher = new SelectorMatcher();
96+
StringMapWrapper.set(map, name, matcher);
97+
}
98+
return matcher;
99+
}
100+
101+
/**
102+
* Find the objects that have been added via `addSelectable`
103+
* whose css selector is contained in the given css selector.
104+
* @param cssSelector A css selector
105+
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
106+
*/
107+
match(cssSelector:CssSelector, matchedCallback:Function) {
108+
var element = cssSelector.element;
109+
var classNames = cssSelector.classNames;
110+
var attrs = cssSelector.attrs;
111+
112+
this._matchTerminal(this._elementMap, element, matchedCallback);
113+
this._matchPartial(this._elementPartialMap, element, cssSelector, matchedCallback);
114+
115+
if (isPresent(classNames)) {
116+
for (var index = 0; index<classNames.length; index++) {
117+
var className = classNames[index];
118+
this._matchTerminal(this._classMap, className, matchedCallback);
119+
this._matchPartial(this._classPartialMap, className, cssSelector, matchedCallback);
120+
}
121+
}
122+
123+
if (isPresent(attrs)) {
124+
for (var index = 0; index<attrs.length; index++) {
125+
var attr = attrs[index];
126+
var attrName = attr.name;
127+
var attrValue = isPresent(attr.value) ? attr.value : _EMPTY_ATTR_VALUE;
128+
129+
var valuesMap = StringMapWrapper.get(this._attrValueMap, attrName)
130+
this._matchTerminal(valuesMap, attrValue, matchedCallback);
131+
132+
valuesMap = StringMapWrapper.get(this._attrValuePartialMap, attrName)
133+
this._matchPartial(valuesMap, attrValue, cssSelector, matchedCallback);
134+
}
135+
}
136+
}
137+
// TODO: map:StringMap when we have a StringMap type...
138+
_matchTerminal(map, name, matchedCallback) {
139+
if (isBlank(map) || isBlank(name)) {
140+
return;
141+
}
142+
var selectables = StringMapWrapper.get(map, name)
143+
if (isBlank(selectables)) {
144+
return;
145+
}
146+
for (var index=0; index<selectables.length; index++) {
147+
matchedCallback(selectables[index]);
148+
}
149+
}
150+
// TODO: map:StringMap when we have a StringMap type...
151+
_matchPartial(map, name, cssSelector, matchedCallback) {
152+
if (isBlank(map) || isBlank(name)) {
153+
return;
154+
}
155+
var nestedSelector = StringMapWrapper.get(map, name)
156+
if (isBlank(nestedSelector)) {
157+
return;
158+
}
159+
// TODO(perf): get rid of recursion and measure again
160+
// TODO(perf): don't pass the whole selector into the recursion,
161+
// but only the not processed parts
162+
nestedSelector.match(cssSelector, matchedCallback);
18163
}
19164
}
165+
166+
export class Attr {
167+
@CONST()
168+
constructor(name:string, value:string = null) {
169+
this.name = name;
170+
this.value = value;
171+
}
172+
}
173+
174+
// TODO: Can't use `const` here as
175+
// in Dart this is not transpiled into `final` yet...
176+
var _SELECTOR_REGEXP =
177+
RegExpWrapper.create('^([-\\w]+)|' + // "tag"
178+
'(?:\\.([-\\w]+))|' + // ".class"
179+
'(?:\\[([-\\w*]+)(?:=([^\\]]*))?\\])'); // "[name]", "[name=value]" or "[name*=value]"
180+
181+
export class CssSelector {
182+
static parse(selector:string):CssSelector {
183+
var element = null;
184+
var classNames = ListWrapper.create();
185+
var attrs = ListWrapper.create();
186+
selector = selector.toLowerCase();
187+
var matcher = RegExpWrapper.matcher(_SELECTOR_REGEXP, selector);
188+
var match;
189+
while (isPresent(match = RegExpMatcherWrapper.next(matcher))) {
190+
if (isPresent(match[1])) {
191+
element = match[1];
192+
}
193+
if (isPresent(match[2])) {
194+
ListWrapper.push(classNames, match[2]);
195+
}
196+
if (isPresent(match[3])) {
197+
ListWrapper.push(attrs, new Attr(match[3], match[4]));
198+
}
199+
}
200+
return new CssSelector(element, classNames, attrs);
201+
}
202+
// TODO: do a toLowerCase() for all arguments
203+
@CONST()
204+
constructor(element:string, classNames:List<string>, attrs:List<Attr>) {
205+
this.element = element;
206+
this.classNames = classNames;
207+
this.attrs = attrs;
208+
}
209+
210+
toString():string {
211+
var res = '';
212+
if (isPresent(this.element)) {
213+
res += this.element;
214+
}
215+
if (isPresent(this.classNames)) {
216+
for (var i=0; i<this.classNames.length; i++) {
217+
res += '.' + this.classNames[i];
218+
}
219+
}
220+
if (isPresent(this.attrs)) {
221+
for (var i=0; i<this.attrs.length; i++) {
222+
var attr = this.attrs[i];
223+
res += '[' + attr.name;
224+
if (isPresent(attr.value)) {
225+
res += '=' + attr.value;
226+
}
227+
res += ']';
228+
}
229+
}
230+
return res;
231+
}
232+
}

0 commit comments

Comments
 (0)