Skip to content

Commit 4cc7bb9

Browse files
committed
Index style rules by their rightmost simple selector
computeStyle tested every selector alternative of every rule against every element. RuleIndex buckets each alternative by the id, class, or element type its rightmost compound requires; alternatives without such a key (universal, attribute-only or pseudo-only compounds) go into a remainder bucket consulted for every element. A lookup gathers the buckets the element satisfies and hands the candidates to the matcher in cascade order, so match positions and cascade results are unchanged. RuleIndexTest locks the completeness invariant: over a stylesheet covering every selector shape, the index never drops an alternative the matcher would accept.
1 parent 86e17df commit 4cc7bb9

4 files changed

Lines changed: 382 additions & 47 deletions

File tree

bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl;
6262
import org.eclipse.e4.ui.css.core.impl.dom.CssRule;
6363
import org.eclipse.e4.ui.css.core.impl.dom.StyleWrapper;
64+
import org.eclipse.e4.ui.css.core.impl.engine.selector.RuleIndex;
6465
import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher;
6566
import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors;
6667
import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry;
@@ -102,8 +103,8 @@ public abstract class CSSEngineImpl implements CSSEngine {
102103
/** The stylesheets added to this engine, in registration order. */
103104
private final List<CSSStyleSheetImpl> styleSheets = new ArrayList<>();
104105

105-
/** Cached flat list of style rules over all stylesheets. */
106-
private List<CSSStyleRuleImpl> combinedRules;
106+
/** Cached rule index over all stylesheets. */
107+
private RuleIndex ruleIndex;
107108

108109
/**
109110
* {@link IElementProvider} used to retrieve w3c Element linked to the
@@ -224,7 +225,7 @@ private CSSStyleSheetImpl parseStyleSheet(String content, String uri) throws IOE
224225
/** Register a parsed stylesheet with this engine's cascade. */
225226
public void addStyleSheet(CSSStyleSheetImpl styleSheet) {
226227
styleSheets.add(styleSheet);
227-
combinedRules = null;
228+
ruleIndex = null;
228229
}
229230

230231
/** The stylesheets registered with this engine, in registration order. */
@@ -974,22 +975,20 @@ public CSSStyleDeclaration computeStyle(Element elt, String pseudoElt) {
974975
hierarchy[idx++] = (Element) n;
975976
}
976977

977-
for (CSSStyleRuleImpl rule : getCombinedRules()) {
978-
for (Selectors.Selector selector : rule.getSelectorList().alternatives()) {
979-
if (SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0)) {
980-
int specificity = selector.specificity();
981-
StyleWrapper wrapper = new StyleWrapper(rule.getStyle(), specificity, position++);
982-
if (firstStyleDeclaration == null) {
983-
firstStyleDeclaration = wrapper;
984-
} else {
985-
// There are several style declarations which match
986-
// the current element
987-
if (styleDeclarations == null) {
988-
styleDeclarations = new ArrayList<>();
989-
styleDeclarations.add(firstStyleDeclaration);
990-
}
991-
styleDeclarations.add(wrapper);
978+
for (RuleIndex.Candidate candidate : getRuleIndex().candidatesFor(elt)) {
979+
if (SelectorMatcher.matches(candidate.selector(), elt, pseudoElt, hierarchy, 0)) {
980+
int specificity = candidate.selector().specificity();
981+
StyleWrapper wrapper = new StyleWrapper(candidate.rule().getStyle(), specificity, position++);
982+
if (firstStyleDeclaration == null) {
983+
firstStyleDeclaration = wrapper;
984+
} else {
985+
// There are several style declarations which match
986+
// the current element
987+
if (styleDeclarations == null) {
988+
styleDeclarations = new ArrayList<>();
989+
styleDeclarations.add(firstStyleDeclaration);
992990
}
991+
styleDeclarations.add(wrapper);
993992
}
994993
}
995994
}
@@ -1004,19 +1003,11 @@ public CSSStyleDeclaration computeStyle(Element elt, String pseudoElt) {
10041003
return null;
10051004
}
10061005

1007-
private List<CSSStyleRuleImpl> getCombinedRules() {
1008-
if (combinedRules == null) {
1009-
List<CSSStyleRuleImpl> rules = new ArrayList<>();
1010-
for (CSSStyleSheetImpl styleSheet : styleSheets) {
1011-
for (CssRule rule : styleSheet.getRules()) {
1012-
if (rule instanceof CSSStyleRuleImpl styleRule) {
1013-
rules.add(styleRule);
1014-
}
1015-
}
1016-
}
1017-
combinedRules = rules;
1006+
private RuleIndex getRuleIndex() {
1007+
if (ruleIndex == null) {
1008+
ruleIndex = RuleIndex.of(styleSheets);
10181009
}
1019-
return combinedRules;
1010+
return ruleIndex;
10201011
}
10211012

10221013
@Override
@@ -1041,7 +1032,7 @@ public void dispose() {
10411032
@Override
10421033
public void reset() {
10431034
styleSheets.clear();
1044-
combinedRules = null;
1035+
ruleIndex = null;
10451036
}
10461037

10471038
/*--------------- Resources Registry -----------------*/
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Lars Vogel and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.e4.ui.css.core.impl.engine.selector;
15+
16+
import java.util.ArrayList;
17+
import java.util.Collections;
18+
import java.util.Comparator;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.eclipse.e4.ui.css.core.dom.CSSStylableElement;
24+
import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleRuleImpl;
25+
import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl;
26+
import org.eclipse.e4.ui.css.core.impl.dom.CssRule;
27+
import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector;
28+
import org.w3c.dom.Element;
29+
30+
/**
31+
* Buckets style rules by the rightmost simple selector of each selector
32+
* alternative, so the cascade only runs the matcher against rules that can
33+
* possibly match a given element.
34+
*/
35+
public final class RuleIndex {
36+
37+
/** One selector alternative of a style rule, with its cascade position. */
38+
public record Candidate(Selector selector, CSSStyleRuleImpl rule, int order) {
39+
}
40+
41+
private static final Comparator<Candidate> BY_ORDER = Comparator.comparingInt(Candidate::order);
42+
43+
private final Map<String, List<Candidate>> byId = new HashMap<>();
44+
private final Map<String, List<Candidate>> byClass = new HashMap<>();
45+
private final Map<String, List<Candidate>> byTag = new HashMap<>();
46+
private final List<Candidate> remainder = new ArrayList<>();
47+
private final List<Candidate> all = new ArrayList<>();
48+
49+
private RuleIndex() {
50+
}
51+
52+
/** Indexes every style rule of the given stylesheets in cascade order. */
53+
public static RuleIndex of(List<CSSStyleSheetImpl> styleSheets) {
54+
RuleIndex index = new RuleIndex();
55+
for (CSSStyleSheetImpl styleSheet : styleSheets) {
56+
for (CssRule rule : styleSheet.getRules()) {
57+
if (rule instanceof CSSStyleRuleImpl styleRule) {
58+
for (Selector alternative : styleRule.getSelectorList().alternatives()) {
59+
index.add(new Candidate(alternative, styleRule, index.all.size()));
60+
}
61+
}
62+
}
63+
}
64+
return index;
65+
}
66+
67+
private void add(Candidate candidate) {
68+
all.add(candidate);
69+
Selector key = rightmostKey(candidate.selector());
70+
if (key instanceof Selectors.IdSelector id) {
71+
byId.computeIfAbsent(id.id(), k -> new ArrayList<>()).add(candidate);
72+
} else if (key instanceof Selectors.ClassSelector cls) {
73+
byClass.computeIfAbsent(cls.className(), k -> new ArrayList<>()).add(candidate);
74+
} else if (key instanceof Selectors.ElementType type) {
75+
byTag.computeIfAbsent(type.localName(), k -> new ArrayList<>()).add(candidate);
76+
} else {
77+
remainder.add(candidate);
78+
}
79+
}
80+
81+
/**
82+
* The most selective simple selector the rightmost compound requires of
83+
* the subject element: id over class over element type. {@code null}
84+
* when the compound only constrains the element by attributes or pseudo
85+
* classes, or not at all; such alternatives go into the remainder bucket
86+
* consulted for every element.
87+
*/
88+
private static Selector rightmostKey(Selector selector) {
89+
if (selector instanceof Selectors.Descendant d) {
90+
return rightmostKey(d.descendant());
91+
}
92+
if (selector instanceof Selectors.Child c) {
93+
return rightmostKey(c.child());
94+
}
95+
if (selector instanceof Selectors.Adjacent a) {
96+
return rightmostKey(a.second());
97+
}
98+
if (selector instanceof Selectors.And and) {
99+
Selector left = rightmostKey(and.left());
100+
Selector right = rightmostKey(and.right());
101+
return rank(left) >= rank(right) ? left : right;
102+
}
103+
if (selector instanceof Selectors.IdSelector || selector instanceof Selectors.ClassSelector) {
104+
return selector;
105+
}
106+
if (selector instanceof Selectors.ElementType type && type.localName() != null) {
107+
return selector;
108+
}
109+
return null;
110+
}
111+
112+
private static int rank(Selector key) {
113+
if (key instanceof Selectors.IdSelector) {
114+
return 3;
115+
}
116+
if (key instanceof Selectors.ClassSelector) {
117+
return 2;
118+
}
119+
if (key instanceof Selectors.ElementType) {
120+
return 1;
121+
}
122+
return 0;
123+
}
124+
125+
/**
126+
* The candidates whose bucket key the element satisfies, in cascade
127+
* order. A superset of the actually matching alternatives; callers still
128+
* run the matcher on each candidate.
129+
*/
130+
public List<Candidate> candidatesFor(Element element) {
131+
List<Candidate> result = new ArrayList<>(remainder);
132+
String elementName = element.getPrefix() == null ? element.getNodeName() : element.getLocalName();
133+
addBucket(byTag, elementName, result);
134+
if (element instanceof CSSStylableElement stylable) {
135+
addClassBuckets(stylable.getCSSClass(), result);
136+
addBucket(byId, stylable.getCSSId(), result);
137+
}
138+
result.sort(BY_ORDER);
139+
return result;
140+
}
141+
142+
/** Every indexed candidate, in cascade order. */
143+
public List<Candidate> allCandidates() {
144+
return Collections.unmodifiableList(all);
145+
}
146+
147+
private static void addBucket(Map<String, List<Candidate>> buckets, String key, List<Candidate> result) {
148+
if (key == null || buckets.isEmpty()) {
149+
return;
150+
}
151+
List<Candidate> bucket = buckets.get(key);
152+
if (bucket != null) {
153+
result.addAll(bucket);
154+
}
155+
}
156+
157+
private void addClassBuckets(String cssClass, List<Candidate> result) {
158+
if (cssClass == null || byClass.isEmpty()) {
159+
return;
160+
}
161+
// Same whitespace-token semantics as the matcher's class comparison;
162+
// a duplicate word must not add its bucket twice.
163+
List<String> words = null;
164+
int length = cssClass.length();
165+
int i = 0;
166+
while (i < length) {
167+
while (i < length && Character.isWhitespace(cssClass.charAt(i))) {
168+
i++;
169+
}
170+
int start = i;
171+
while (i < length && !Character.isWhitespace(cssClass.charAt(i))) {
172+
i++;
173+
}
174+
if (i > start) {
175+
String word = cssClass.substring(start, i);
176+
if (words == null) {
177+
words = new ArrayList<>(2);
178+
}
179+
if (!words.contains(word)) {
180+
words.add(word);
181+
addBucket(byClass, word, result);
182+
}
183+
}
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)