diff --git a/haxe/ui/backend/ComponentBase.hx b/haxe/ui/backend/ComponentBase.hx index a0de8659d..8db8fe778 100644 --- a/haxe/ui/backend/ComponentBase.hx +++ b/haxe/ui/backend/ComponentBase.hx @@ -1378,6 +1378,10 @@ class ComponentBase extends ComponentSurface implements IClonable return; //it should be added into the queue later } + if (isComponentValidationPaused) { + return; + } + var isAlreadyInvalid:Bool = isComponentInvalid(flag); var isAlreadyDelayedInvalid:Bool = false; if (_isValidating == true) { @@ -1568,7 +1572,35 @@ class ComponentBase extends ComponentSurface implements IClonable } } + private var _validationPaused:Bool = false; + public function pauseComponentValidation() { + _validationPaused = true; + } + + private var isComponentValidationPaused(get, never):Bool; + private function get_isComponentValidationPaused():Bool { + var ref = this; + while (ref != null) { + if (ref._validationPaused) { + return true; + } + ref = ref.parentComponent; + } + return false; + } + + public function resumeComponentValidation() { + Toolkit.callLater(() -> { + _validationPaused = false; + invalidateComponent(true); + }); + } + private function validateComponentInternal(nextFrame:Bool = true) { + if (isComponentValidationPaused) { + return; + } + var dataInvalid = isComponentInvalid(InvalidationFlags.DATA); var styleInvalid = isComponentInvalid(InvalidationFlags.STYLE); var textDisplayInvalid = isComponentInvalid(InvalidationFlags.TEXT_DISPLAY) && hasTextDisplay(); diff --git a/haxe/ui/containers/Collapsible.hx b/haxe/ui/containers/Collapsible.hx index 1a73c2b01..0bf2edc84 100644 --- a/haxe/ui/containers/Collapsible.hx +++ b/haxe/ui/containers/Collapsible.hx @@ -151,8 +151,6 @@ class CollapsibleBuilder extends CompositeBuilder { super(collapsible); _collapsible = collapsible; // we'll start off as non-animatable so things dont animate at the start of the component creation - _originalAnimatable = _collapsible.animatable; - _collapsible.animatable = false; _component.recursivePointerEvents = false; _header = new HBox(); _header.percentWidth = 100; @@ -182,6 +180,11 @@ class CollapsibleBuilder extends CompositeBuilder { _collapsible.registerInternalEvents(true); } + public override function onInitialize() { + _originalAnimatable = _collapsible.animatable; + _collapsible.animatable = false; + } + public override function onReady() { super.onReady(); _collapsible.animatable = _originalAnimatable; diff --git a/haxe/ui/core/Component.hx b/haxe/ui/core/Component.hx index d687a9cf9..a75da35d9 100644 --- a/haxe/ui/core/Component.hx +++ b/haxe/ui/core/Component.hx @@ -1263,6 +1263,7 @@ class Component extends ComponentImpl private function set_customStyle(value:Style):Style { if (value != _customStyle) { invalidateComponentStyle(); + _updateStyleCacheKey(); } _customStyle = value; return value; @@ -1285,6 +1286,7 @@ class Component extends ComponentImpl classes.push(name); if (invalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } } @@ -1316,6 +1318,7 @@ class Component extends ComponentImpl if (needsInvalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } if (recursive == true) { @@ -1338,6 +1341,7 @@ class Component extends ComponentImpl classes.remove(name); if (invalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } } @@ -1369,6 +1373,7 @@ class Component extends ComponentImpl if (needsInvalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } if (recursive == true) { @@ -1411,6 +1416,7 @@ class Component extends ComponentImpl if (invalidate == true && needsInvalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } if (recursive == true) { @@ -1442,6 +1448,7 @@ class Component extends ComponentImpl if (invalidate == true && needsInvalidate == true) { invalidateComponentStyle(); + _updateStyleCacheKey(); } if (recursive == true) { @@ -1552,9 +1559,23 @@ class Component extends ComponentImpl _styleString = value; invalidateComponentStyle(); + _updateStyleCacheKey(); return value; } + private var _styleCacheKey:Array = []; + function _updateStyleCacheKey() { + _styleCacheKey = []; + var ref = this; + while (ref != null) { + _styleCacheKey.push(ref.className); + // _styleCacheKey.push(ref.id); + _styleCacheKey = _styleCacheKey.concat(ref.classes); + ref = ref.parentComponent; + } + _styleCacheKey.reverse(); + } + // were going to cache the ref (which may be null) so we dont have to // perform a parent based lookup each for a performance tweak @:noCompletion private var _useCachedStyleSheetRef:Bool = false; @@ -1708,6 +1729,7 @@ class Component extends ComponentImpl Toolkit.callLater(function() { invalidateComponentData(); invalidateComponentStyle(); + _updateStyleCacheKey(); if (_compositeBuilder != null) { _compositeBuilder.onReady(); diff --git a/haxe/ui/styles/StyleSheet.hx b/haxe/ui/styles/StyleSheet.hx index aa2ec2856..a91f6efc1 100644 --- a/haxe/ui/styles/StyleSheet.hx +++ b/haxe/ui/styles/StyleSheet.hx @@ -6,12 +6,24 @@ import haxe.ui.styles.elements.AnimationKeyFrames; import haxe.ui.styles.elements.ImportElement; import haxe.ui.styles.elements.MediaQuery; import haxe.ui.styles.elements.RuleElement; +import haxe.ui.styles.elements.Directive; +#if new_selectors +import haxe.ui.styles.selector.SelectorData; +import haxe.ui.styles.selector.SelectorSpecificity; +#end using StringTools; class StyleSheet { public var name:String; + private var _styleCacheTree = new haxe.utils.trie.Trie>>(); + +#if new_selectors + // private var _directives = new Map(); + // private var _selectedSelectors = new Map(); +#end + private var _imports:Array = []; private var _rules:Array = []; @@ -146,12 +158,49 @@ class StyleSheet { if (style == null) { style = {}; } - for (r in rules) { - if (!r.match(c)) { - continue; + + final key = @:privateAccess c._styleCacheKey; + var cachedDirectives = _styleCacheTree.get(key); + if (cachedDirectives != null) { + for (d in cachedDirectives) + style.mergeDirectives(d); + } else + { + + cachedDirectives = []; + + #if new_selectors + // _directives.clear(); + // _selectedSelectors.clear(); + #end + + for (r in rules) { + if (!r.match(c)) { + continue; + } + #if new_selectors + // this was some code for specifity but this is broken with the caching now. ignore it + // for (k=>v in r.directives) { + // if (!_directives.exists(k)) { + // _directives.set(k, v); + // _selectedSelectors.set(k, r.selector); + // } else { + // if (SelectorSpecificity.get(r.selector) >= SelectorSpecificity.get(_selectedSelectors.get(k))) { + // _directives.set(k, v); + // _selectedSelectors.set(k, r.selector); + // } + // } + // } + // cachedDirectives.push(_directives); + cachedDirectives.push(r.directives); + #else + cachedDirectives.push(r.directives); + #end } - style.mergeDirectives(r.directives); + for (d in cachedDirectives) + style.mergeDirectives(d); + _styleCacheTree.set(key, cachedDirectives); } return style; diff --git a/haxe/ui/styles/elements/RuleElement.hx b/haxe/ui/styles/elements/RuleElement.hx index eb0be2e1f..90d22c46b 100644 --- a/haxe/ui/styles/elements/RuleElement.hx +++ b/haxe/ui/styles/elements/RuleElement.hx @@ -2,15 +2,29 @@ package haxe.ui.styles.elements; import haxe.ui.core.Component; import haxe.ui.styles.Value; +#if new_selectors +import haxe.ui.styles.selector.SelectorMatcher; +import haxe.ui.styles.selector.SelectorParser; +import haxe.ui.styles.selector.SelectorData; +#end @:access(haxe.ui.core.Component) class RuleElement { +#if new_selectors + public var selector:SelectorVO; + static var matchedPseudoClasses = new MatchedPseudoClassesVO(false, false, false, false, false, false, false, false, false, false, null, null, null); +#else public var selector:Selector; +#end public var directives:Map = new Map(); public var directiveCount:Int = 0; public function new(selector:String, directives:Array) { +#if new_selectors + this.selector = SelectorParser.parse(selector); +#else this.selector = new Selector(selector); +#end //this.directives = directives; for (d in directives) { @@ -25,7 +39,122 @@ class RuleElement { } public function match(d:Component):Bool { + +#if new_selectors + matchedPseudoClasses.hover = false; + matchedPseudoClasses.focus = false; + matchedPseudoClasses.active = false; + matchedPseudoClasses.link = false; + matchedPseudoClasses.enabled = false; + matchedPseudoClasses.disabled = false; + matchedPseudoClasses.checked = false; + matchedPseudoClasses.fullscreen = false; + + matchedPseudoClasses.hasClasses = d.classes.length > 0; + matchedPseudoClasses.nodeClassList = d.classes; + + if (matchedPseudoClasses.hasClasses) { + for (c in (d.classes:Array)) { + if (c == ':hover') matchedPseudoClasses.hover = true; + else if (c == ':focus') matchedPseudoClasses.focus = true; + else if (c == ':active') matchedPseudoClasses.active = true; + else if (c == ':link') matchedPseudoClasses.link = true; + else if (c == ':enabled') matchedPseudoClasses.enabled = true; + else if (c == ':disabled') matchedPseudoClasses.disabled = true; + else if (c == ':checked') matchedPseudoClasses.checked = true; + else if (c == ':fullscreen') matchedPseudoClasses.fullscreen = true; + } + } + + matchedPseudoClasses.hasId = true; + matchedPseudoClasses.nodeId = d.id; + matchedPseudoClasses.nodeType = d.className; + + // naive version, full match every rule: + // final res = SelectorMatcher.match(d, selector, matchedPseudoClasses); + // trace('$res: ${selector.toString()} == <${@:privateAccess d.className} id=${d.id} class=${d.classes}>'); + // return res; + + var match:Bool = false; + + //to optimise speed the matchSelector method must be called + //the least time possible + + //if the selector begins with a class, + //then only match if the node has at least one class, + //and contains the first class of the selector + if (selector.beginsWithClass) { + if (matchedPseudoClasses.hasClasses) { + var classListLength:Int = matchedPseudoClasses.nodeClassList.length; + for (cls in matchedPseudoClasses.nodeClassList) { + if (cls == selector.firstClass) { + // in this case, the selector only has a single + // class selector, so it is a match + if (selector.isSimpleClassSelector == true) { + match = true; + break; + } + //else need to perform a full match + else { + match = SelectorMatcher.match(d, selector, matchedPseudoClasses) == true; + break; + } + } + } + } + } + //if the selector begins with an id selector, only match node if + //it has an id + else if (selector.beginsWithId == true) { + if (matchedPseudoClasses.hasId == true) { + if (matchedPseudoClasses.nodeId == selector.firstId) { + //if the selector consists of only an Id, it is a match + if (selector.isSimpleIdSelector == true) + match = true; + //else need to perform a full match + else + match = SelectorMatcher.match(d, selector, matchedPseudoClasses) == true; + } + } + } + //if the selector begins with a type, only match node wih the + //same type + else if (selector.beginsWithType == true) { + if (matchedPseudoClasses.nodeType == selector.firstType) { + //if the selector is only a type selector, then it matches + if (selector.isSimpleTypeSelector == true) + match = true; + //else a full match is needed + else + match = SelectorMatcher.match(d, selector, matchedPseudoClasses) == true; + } + } + //in other cases, full match + else + match = SelectorMatcher.match(d, selector, matchedPseudoClasses) == true; + + // if (match == true) + // { + // //if the selector is matched, store the coresponding style declaration + // //along with the matching selector + // var matchingStyleDeclaration:StyleDeclarationVO = new StyleDeclarationVO(); + // matchingStyleDeclaration.style = styleRule.style; + // matchingStyleDeclaration.selector = selectors[k]; + // _matchingStyleDeclaration.push(matchingStyleDeclaration); + + // //break to prevent from adding a style declaration + // //multiplt time if more than one selector + // //matches + // break; + // } + + // if (matchedPseudoClasses.hover) + // trace('$match: ${selector.toString()} == <${@:privateAccess d.tagName} id=${d.id} class=${d.classList}> {$directives}'); + + return match; +#else return ruleMatch(selector.parts[selector.parts.length - 1], d); +#end } private static function ruleMatch( c : SelectorPart, d : Component ):Bool { @@ -212,4 +341,4 @@ class RuleElement { trace("unknown value type", d.value); } } -} +} \ No newline at end of file diff --git a/haxe/ui/styles/selector/SelectorData.hx b/haxe/ui/styles/selector/SelectorData.hx new file mode 100644 index 000000000..ee94fd7fb --- /dev/null +++ b/haxe/ui/styles/selector/SelectorData.hx @@ -0,0 +1,511 @@ +/* + * Cocktail, HTML rendering engine + * http://haxe.org/com/libs/cocktail + * + * Copyright (c) Silex Labs + * Cocktail is available under the MIT license + * http://www.silexlabs.org/labs/cocktail-licensing/ +*/ +package haxe.ui.styles.selector; + +import haxe.ui.styles.selector.SelectorSerializer; + +/** + * @author Yannick DOMINGUEZ + */ + +////////////////////////////////////////////////////////////////////////////////////////// +// SELECTOR STRUCTURES +////////////////////////////////////////////////////////////////////////////////////////// + +/** + * For a given element, when retrieving + * its styles, stores which pseudo-classes + * the element currently matches. + * + * Also store some additional data about + * the node, such as wether it has an ID, + * used to optimise cascading + */ +class MatchedPseudoClassesVO { + + public var hover:Bool; + public var focus:Bool; + public var active:Bool; + public var link:Bool; + public var enabled:Bool; + public var disabled:Bool; + public var checked:Bool; + public var fullscreen:Bool; + + public var hasId:Bool; + public var nodeId:String; + public var hasClasses:Bool; + public var nodeClassList:Array; + public var nodeType:String; + + public function new(hover:Bool, focus:Bool, active:Bool, link:Bool, enabled:Bool, + disabled:Bool, checked:Bool, fullscreen:Bool, + hasId:Bool, hasClasses:Bool, nodeId:String, nodeClassList:Array, nodeType:String) + { + this.hover = hover; + this.focus = focus; + this.active = active; + this.link = link; + this.enabled = enabled; + this.disabled = disabled; + this.checked = checked; + this.fullscreen = fullscreen; + this.hasId = hasId; + this.hasClasses = hasClasses; + this.nodeId = nodeId; + this.nodeClassList = nodeClassList; + this.nodeType = nodeType; + } +} + +/** + * Holds the data used to determine a selector specificity (priority). + * Selector specificity is used to determine which styles to use when + * a particular style is defined in more than one CSS rule. The + * style with the more specific selector is used. + * + * Specificity is defined by 3 categories whose value are + * then concatenated into an integer value + */ +class SelectorSpecificityVO { + + /** + * Incremented for each ID simple selector + * in the selector + */ + public var idSelectorsNumber:Int; + + /** + * Incremented for each class and pseudo class + * simple selector in the selector + */ + public var classAttributesAndPseudoClassesNumber:Int; + + /** + * Incremented for each type and pseudo element + * simple selector in the selector + */ + public var typeAndPseudoElementsNumber:Int; + + public function new() + { + idSelectorsNumber = 0; + classAttributesAndPseudoClassesNumber = 0; + typeAndPseudoElementsNumber = 0; + } +} + +/** + * Contains all the data of one selector + */ +class SelectorVO { + + /** + * an array of any combination of selector + * components + */ + public var components:Array; + + /** + * a selector can only have one pseudo element, + * always specified at the end of the selector + */ + public var pseudoElement:PseudoElementSelectorValue; + + /** + * Store wether the first component (starting from the right) + * of this selector is a class selector. Used for optimisations + * during cascade + */ + public var beginsWithClass:Bool; + + /** + * If the selector begins with a class, it is stored + * here, else it is null + */ + public var firstClass:String; + + /** + * same as beginsWithClass for Id selector + */ + public var beginsWithId:Bool; + + /** + * same as firstClass for Id + */ + public var firstId:String; + + /** + * same as beginsWithClass for type selector + */ + public var beginsWithType:Bool; + + /** + * same as firstClass for type selector + */ + public var firstType:String; + + /** + * Wether this selector only contains a single + * class selector + */ + public var isSimpleClassSelector:Bool; + + /** + * same as above for id selector + */ + public var isSimpleIdSelector:Bool; + + /** + * same as above for type selector + */ + public var isSimpleTypeSelector:Bool; + + public function new(components:Array, pseudoElement:PseudoElementSelectorValue, + beginsWithClass:Bool, firstClass:String, beginsWithId:Bool, firstId:String, beginsWithType:Bool, firstType:String + , isSimpleClassSelector:Bool, isSimpleIdSelector:Bool, isSimpleTypeSelector:Bool) + { + this.components = components; + this.pseudoElement = pseudoElement; + this.beginsWithClass = beginsWithClass; + this.firstClass = firstClass; + this.beginsWithId = beginsWithId; + this.firstId = firstId; + this.beginsWithType = beginsWithType; + this.firstType = firstType; + this.isSimpleClassSelector = isSimpleClassSelector; + this.isSimpleIdSelector = isSimpleIdSelector; + this.isSimpleTypeSelector = isSimpleTypeSelector; + } + + public function toString() + return CSSSelectorSerializer.serialize(this); +} + +/** + * Represent a simple selector sequence. + * A sequence always begin with a type or + * universal selector and only has one of + * those two in the whole sequence. Then it can + * have any combination of the remaining simple + * selectors + */ +class SimpleSelectorSequenceVO { + + /** + * Only one sequence start selector for a selector + * sequence + */ + public var startValue:SimpleSelectorSequenceStartValue; + + /** + * any number of the remaining simple selectors + */ + public var simpleSelectors:Array; + + public function new(startValue:SimpleSelectorSequenceStartValue, simpleSelectors:Array) + { + this.startValue = startValue; + this.simpleSelectors = simpleSelectors; + } +} + +////////////////////////////////////////////////////////////////////////////////////////// +// SELECTOR ENUMS +////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A selector contains either simple selector + * or combinator between 2 simple selector + */ +enum SelectorComponentValue { + SIMPLE_SELECTOR_SEQUENCE(value:SimpleSelectorSequenceVO); + COMBINATOR(value:CombinatorValue); +} + +/** + * Lists all the simple selectors besides the type and + * universal selector, reserved for the start of a + * simple selector sequence + */ +enum SimpleSelectorSequenceItemValue { + ATTRIBUTE(value:AttributeSelectorValue); + PSEUDO_CLASS(value:PseudoClassSelectorValue); + CSS_CLASS(value:String); + ID(value:String); +} + +/** + * Matches an element's type (tag name) or any element (universal, symbolised by "*"). + * A simple selector sequence always begin with + * one of those 2 values. Universal may be implied. + * For instance ".myclass" is the same as "*.myClass" + */ +enum SimpleSelectorSequenceStartValue { + + /** + * any element + */ + UNIVERSAL; + + /** + * an element of type value + */ + TYPE(value:String); +} + +/** + * Matches an element's attribute + * presence and value + */ +enum AttributeSelectorValue { + + /** + * an element with a "value" attribute + */ + ATTRIBUTE(value:String); + + /** + * an element with a "name" attribute + * whose value is exactly "value" + */ + ATTRIBUTE_VALUE(name:String, value:String); + + /** + * an element whose "name" attribute + * value is a list of whitespace-separated values, + * one of which is exactly equal to "value" + */ + ATTRIBUTE_LIST(name:String, value:String); + + /** + * an element whose "name" attribute value begins + * exactly with the string "value" + */ + ATTRIBUTE_VALUE_BEGINS(name:String, value:String); + + /** + * an element whose "name" attribute + * value ends exactly with the string "value" + */ + ATTRIBUTE_VALUE_ENDS(name:String, value:String); + + /** + * an element whose "name" attribute value + * contains the substring "value" + */ + ATTRIBUTE_VALUE_CONTAINS(name:String, value:String); + + /** + * an element whose "name" attribute has a hyphen-separated + * list of values beginning (from the left) with "value" + */ + ATTRIBUTE_VALUE_BEGINS_HYPHEN_LIST(name:String, value:String); +} + +/** + * List the pseuso class selector types + */ +enum PseudoClassSelectorValue { + STRUCTURAL(value:StructuralPseudoClassSelectorValue); + LINK(value:LinkPseudoClassValue); + TARGET; + FULLSCREEN; + LANG(value:Array); + USER_ACTION(value:UserActionPseudoClassValue); + UI_ELEMENT_STATES(value:UIElementStatesValue); + + CUSTOM(value:String); + + //TODO 2 : should actually be SelectorVO ? + NOT(value:SimpleSelectorSequenceVO); +} + +/** + * List the structural pseudo class, which + * are based on the DOM structure + */ +enum StructuralPseudoClassSelectorValue { + + /** + * The :root pseudo-class represents an element + * that is the root of the document. In HTML 4, this + * is always the HTML element. + */ + ROOT; + + /** + * The :first-child pseudo-class represents an element + * that is the first child of some other element. + */ + FIRST_CHILD; + + /** + * The :last-child pseudo-class represents + * an element that is the last child of + * some other element. + */ + LAST_CHILD; + + /** + * The :first-of-type pseudo-class represents + * an element that is the first sibling of its + * type in the list of children of its parent element. + */ + FIRST_OF_TYPE; + + /** + * he :last-of-type pseudo-class represents an element + * that is the last sibling of its type in the list + * of children of its parent element. + */ + LAST_OF_TYPE; + + /** + * Represents an element that has a parent element and whose + * parent element has no other element children. + * Same as :first-child:last-child + */ + ONLY_CHILD; + + /** + * Represents an element that has a parent element and + * whose parent element has no other element children + * with the same expanded element name + */ + ONLY_OF_TYPE; + + /** + * The :empty pseudo-class represents an element + * that has no children at all. + */ + EMPTY; + + //TODO 2 : doc + implementation + NTH_CHILD(value:StructuralPseudoClassArgumentValue); + NTH_LAST_CHILD(value:StructuralPseudoClassArgumentValue); + NTH_LAST_OF_TYPE(value:StructuralPseudoClassArgumentValue); + NTH_OF_TYPE(value:StructuralPseudoClassArgumentValue); +} + +//TODO 2 : missing values +enum StructuralPseudoClassArgumentValue { + INDEX(idx:Int); + ODD; + EVEN; +} + +/** + * pseudo class applying to anchor + */ +enum LinkPseudoClassValue { + + /** + * The :link pseudo-class applies + * to links that have not yet been visited. + */ + LINK; + + /** + * The :visited pseudo-class applies once + * the link has been visited by the user. + */ + VISITED; +} + +/** + * Pseudo classes caused by user actions + */ +enum UserActionPseudoClassValue { + + /** + * The :active pseudo-class applies while an element is being + * activated by the user. For example, between + * the times the user presses the mouse + * button and releases it. + */ + ACTIVE; + + /** + * The :hover pseudo-class applies while the user + * designates an element with a pointing device, + * but does not necessarily activate it. + */ + HOVER; + + /** + * The :focus pseudo-class applies while an element + * has the focus (accepts keyboard or mouse events, + * or other forms of input). + */ + FOCUS; +} + +enum UIElementStatesValue { + ENABLED; + DISABLED; + CHECKED; +} + +enum PseudoElementSelectorValue { + NONE; + FIRST_LINE; + FIRST_LETTER; + BEFORE; + AFTER; +} + +enum CombinatorValue { + DESCENDANT; + CHILD; + ADJACENT_SIBLING; + GENERAL_SIBLING; +} + +/** + * states enums for state parsers + * + * @author Yannick DOMINGUEZ + */ + +enum SelectorParserState { + IGNORE_SPACES; + BEGIN_SIMPLE_SELECTOR; + END_SIMPLE_SELECTOR; + SIMPLE_SELECTOR; + END_TYPE_SELECTOR; + END_CLASS_SELECTOR; + END_ID_SELECTOR; + BEGIN_COMBINATOR; + COMBINATOR; + BEGIN_PSEUDO_SELECTOR; + END_UNIVERSAL_SELECTOR; + PSEUDO_ELEMENT_SELECTOR; + BEGIN_ATTRIBUTE_SELECTOR; + INVALID_SELECTOR; +} + +enum SelectorsParserState { + IGNORE_SPACES; + BEGIN_SELECTOR; + END_SELECTOR; + SELECTOR; +} + +enum AttributeSelectorParserState { + IGNORE_SPACES; + END_OPERATOR; + ATTRIBUTE; + BEGIN_OPERATOR; + OPERATOR; + IDENTIFIER_VALUE; + STRING_VALUE; + END_SELECTOR; + INVALID_SELECTOR; +} diff --git a/haxe/ui/styles/selector/SelectorMatcher.hx b/haxe/ui/styles/selector/SelectorMatcher.hx new file mode 100644 index 000000000..21f75a1c5 --- /dev/null +++ b/haxe/ui/styles/selector/SelectorMatcher.hx @@ -0,0 +1,327 @@ +/* + * Cocktail, HTML rendering engine + * http://haxe.org/com/libs/cocktail + * + * Copyright (c) Silex Labs + * Cocktail is available under the MIT license + * http://www.silexlabs.org/labs/cocktail-licensing/ +*/ +package haxe.ui.styles.selector; + +import haxe.ui.core.Component; +import haxe.ui.styles.selector.SelectorData; +import haxe.ui.styles.selector.matchers.Attributes; +import haxe.ui.styles.selector.matchers.PseudoClass; + +/** + * The selector matcher has 2 purposes : + * - For a given element and selector, it returns wether the element + * matches the selector + * - For a given selector, it can return its specificity (its priority) + * + * @author Yannick DOMINGUEZ + */ +class SelectorMatcher +{ + /** + * class constructor + */ + private function new() + { + + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC SELECTOR MATCHING METHODS + ////////////////////////////////////////////////////////////////////////////////////////// + + /** + * For a given element and selector, return wether + * the element matches all of the components of the selector + */ + inline public static function match(element:Component, selector:SelectorVO, ?matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + var res = true; + + //if null, assumes that the document is non-interactive and can't match any of this + if (matchedPseudoClasses == null) + { + matchedPseudoClasses = new MatchedPseudoClassesVO( + false, false, false, false, false, false, false, false, false, false, + null, null, null); + } + + var components:Array = selector.components; + + //a flag set to true when the last item in the components array + //was a combinator. + //This flag is a shortcut to prevent matching again selector + //sequence that were matched by the combinator + var lastWasCombinator:Bool = false; + + //loop in all the components of the selector + var length:Int = components.length; + for (i in 0...length) + { + var component:SelectorComponentValue = components[i]; + + //wether the current selector component match the element + var matched:Bool = false; + + switch(component) + { + case SelectorComponentValue.COMBINATOR(value): + matched = matchCombinator(element, value, components[i + 1], matchedPseudoClasses); + lastWasCombinator = true; + + //if the combinator is a child combinator, the relevant + //element becomes the parentComponent element as any subsequent would + //apply to it instead of the current element + if (value == CHILD) + { + element = element.parentComponent; + } + + case SelectorComponentValue.SIMPLE_SELECTOR_SEQUENCE(value): + //if the previous item was a combinator, then + //this simple selector sequence was already + //successfuly matched, else the method would have + //returned + if (lastWasCombinator == true) + { + matched = true; + lastWasCombinator = false; + } + else + { + matched = matchSimpleSelectorSequence(element, value, matchedPseudoClasses); + } + } + + //if the component is not + //matched, then the selector is not matched + if (matched == false) + { + res = false; + break; + } + } + + return res; + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE SELECTOR MATCHING METHODS + ////////////////////////////////////////////////////////////////////////////////////////// + + // COMBINATORS + ////////////////////////////////////////////////////////////////////////////////////////// + + /** + * return wether a combinator is matched + */ + private static function matchCombinator(element:Component, combinator:CombinatorValue, nextSelectorComponent:SelectorComponentValue, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + //if the element has no parentComponent, it can't match + //any combinator + if (element.parentComponent == null) + { + return false; + } + + var nextSelectorSequence:SimpleSelectorSequenceVO = null; + //the next component at this point is always a simple + //selector sequence, there can't be 2 combinators in a row + //in a selector, it makes the selector invalid + switch(nextSelectorComponent) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + nextSelectorSequence = value; + + case COMBINATOR(value): + return false; + } + + switch(combinator) + { + case CombinatorValue.ADJACENT_SIBLING: + return matchAdjacentSiblingCombinator(element, nextSelectorSequence, matchedPseudoClasses); + + case CombinatorValue.GENERAL_SIBLING: + return matchGeneralSiblingCombinator(element, nextSelectorSequence, matchedPseudoClasses); + + case CombinatorValue.CHILD: + return matchChildCombinator(element, nextSelectorSequence, matchedPseudoClasses); + + case CombinatorValue.DESCENDANT: + return matchDescendantCombinator(element, nextSelectorSequence, matchedPseudoClasses); + } + } + + /** + * Return wether a general sibling combinator is + * matched. + * + * It is matched if the element has a sibling matching + * the preious selector sequence which precedes in + * the DOM tree + */ + private static function matchGeneralSiblingCombinator(element:Component, nextSelectorSequence:SimpleSelectorSequenceVO, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + // var previousComponentSibling = element.previousComponentSibling; + + // while (previousComponentSibling != null) + // { + // if (matchSimpleSelectorSequence(previousComponentSibling, nextSelectorSequence, matchedPseudoClasses) == true) + // { + // return true; + // } + + // previousComponentSibling = previousComponentSibling.previousComponentSibling; + // } + + return false; + } + + /** + * Same as general sibling combinator, but + * only matched if the first previous + * element sibling of the element matches + * the previous selector + */ + private static function matchAdjacentSiblingCombinator(element:Component, nextSelectorSequence:SimpleSelectorSequenceVO, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + // var previousComponentSibling = element.previousComponentSibling; + + // if (previousComponentSibling == null) + // { + // return false; + // } + + // return matchSimpleSelectorSequence(previousComponentSibling, nextSelectorSequence, matchedPseudoClasses); + return false; + } + + /** + * Return wether a descendant combinator is matched. + * It is matched when an ancestor of the element + * matches the next selector sequence + */ + private static function matchDescendantCombinator(element:Component, nextSelectorSequence:SimpleSelectorSequenceVO, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + var parentComponent = element.parentComponent; + + //check that at least one ancestor matches + //the parentComponent selector + while (parentComponent != null) + { + if (matchSimpleSelectorSequence(parentComponent, nextSelectorSequence, matchedPseudoClasses) == true) + { + return true; + } + + parentComponent = parentComponent.parentComponent; + } + + //here no parentComponent matched, so the + //combinator is not matched + return false; + } + + /** + * Same as matchDescendantCombinator, but the + * next selector sequence must be matched by the + * direct parentComponent of the element and not just any ancestor + */ + private static function matchChildCombinator(element:Component, nextSelectorSequence:SimpleSelectorSequenceVO, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + return matchSimpleSelectorSequence(element.parentComponent, nextSelectorSequence, matchedPseudoClasses); + } + + // SIMPLE SELECTORS + ////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Return wether a element match a simple selector sequence starter. + * + * A simple selector sequence is a list of simple selector, + * for instance in : div.myclass , div is a simple selector, .myclass is too + * and together they are a simple selector sequence + * + * A simple selector sequence always start with either a type (like 'div') or a universal ('*') + * selector + */ + private static function matchSimpleSelectorSequenceStart(element:Component, simpleSelectorSequenceStart:SimpleSelectorSequenceStartValue):Bool + { + switch(simpleSelectorSequenceStart) + { + case SimpleSelectorSequenceStartValue.TYPE(value): + return element.className == value; + + case SimpleSelectorSequenceStartValue.UNIVERSAL: + return true; + } + } + + /** + * Return weher a element match an item of a simple selector sequence. + * The possible items of a simple selector are all simple selectors + * (class, ID...) but type or universal which are always at the + * begining of a simple selector sequence + */ + private static function matchSimpleSelectorSequenceItem(element:Component, simpleSelectorSequenceItem:SimpleSelectorSequenceItemValue, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + switch(simpleSelectorSequenceItem) + { + //for this check the list of class of the element + case CSS_CLASS(value): + var classList = @:privateAccess element.classes; + + //here the element has no classes + if (classList == null) + { + return false; + } + + return classList.contains(value); + + //for this check the id attribute of the element + case ID(value): + return element.id == value; + + case PSEUDO_CLASS(value): + return PseudoClass.match(element, value, matchedPseudoClasses); + + case ATTRIBUTE(value): + // return Attributes.match(element.getAttribute, value); + return false; + } + } + + /** + * Return wether all items in a simple selector + * sequence are matched + */ + private static function matchSimpleSelectorSequence(element:Component, simpleSelectorSequence:SimpleSelectorSequenceVO, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + //check if sequence start matches + if (matchSimpleSelectorSequenceStart(element, simpleSelectorSequence.startValue) == false) + { + return false; + } + + //check all items + var simpleSelectors:Array = simpleSelectorSequence.simpleSelectors; + var length:Int = simpleSelectors.length; + for (i in 0...length) + { + var simpleSelectorSequence:SimpleSelectorSequenceItemValue = simpleSelectors[i]; + if (matchSimpleSelectorSequenceItem(element, simpleSelectorSequence, matchedPseudoClasses) == false) + { + return false; + } + } + + return true; + } +} diff --git a/haxe/ui/styles/selector/SelectorMetadata.hx b/haxe/ui/styles/selector/SelectorMetadata.hx new file mode 100644 index 000000000..2ffb4e53f --- /dev/null +++ b/haxe/ui/styles/selector/SelectorMetadata.hx @@ -0,0 +1,197 @@ +package haxe.ui.styles.selector; + +import haxe.ui.styles.selector.SelectorData; + +/** + * This class allows to attach metadata to a selector + * to optimize matching + */ +class SelectorMetadata +{ + /** + * if the selector begins with a class selector, return it, + * else return null + */ + public static function getFirstClass(components:Array):String + { + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + //check that don't start with type selector + if (value.startValue == UNIVERSAL) + { + //check that has at least 1 simple selector + if (value.simpleSelectors.length != 0) + { + //check that the first simple selector is a class selector + switch(value.simpleSelectors[0]) + { + case CSS_CLASS(value): + return value; + + default: + } + } + } + + //won't happen, selector always begins with selector sequence + case COMBINATOR(value): + } + return null; + } + + /** + * Returns wether this selector contains only one clss selector + */ + public static function getIsSimpleClassSelector(components:Array):Bool + { + // > 1 means that it has combinators + if (components.length > 1) + { + return false; + } + + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + //must start with universal selector + if (value.startValue == UNIVERSAL) + { + //check that has only 1 simple selector + if (value.simpleSelectors.length == 1) + { + //check that that this simple selector is a class selector + switch(value.simpleSelectors[0]) + { + case CSS_CLASS(value): + return true; + + default: + } + } + } + + case COMBINATOR(value): + } + return false; + } + + /** + * Same as above for id selector + */ + public static function getIsSimpleIdSelector(components:Array):Bool + { + if (components.length > 1) + { + return false; + } + + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + + if (value.startValue == UNIVERSAL) + { + if (value.simpleSelectors.length == 1) + { + switch(value.simpleSelectors[0]) + { + case ID(value): + return true; + + default: + } + } + } + + case COMBINATOR(value): + } + return false; + } + + /** + * Same as above for type selector + */ + public static function getIsSimpleTypeSelector(components:Array):Bool + { + if (components.length > 1) + { + return false; + } + + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + switch(value.startValue) + { + case TYPE(typeValue): + if (value.simpleSelectors.length == 0) + { + return true; + } + + default: + + } + + case COMBINATOR(value): + } + return false; + } + + /** + * if the selector begins with an Id selector, return it, + * else return null + */ + public static function getFirstId(components:Array):String + { + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + //check that don't start with type selector + if (value.startValue == UNIVERSAL) + { + //check that has at least 1 simple selector + if (value.simpleSelectors.length != 0) + { + //check that the first simple selector is an Id selector + switch(value.simpleSelectors[0]) + { + case ID(value): + return value; + + default: + } + } + } + + //won't happen, selector always begins with selector sequence + case COMBINATOR(value): + } + return null; + } + + /** + * if the selector begins with a type selector, return it, + * else return null + */ + public static function getFirstType(components:Array):String + { + switch(components[0]) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + switch(value.startValue) + { + case TYPE(value): + return value; + + default: + } + + //won't happen, selector always begins with selector sequence + case COMBINATOR(value): + } + return null; + } +} + diff --git a/haxe/ui/styles/selector/SelectorParser.hx b/haxe/ui/styles/selector/SelectorParser.hx new file mode 100644 index 000000000..ef37e12f6 --- /dev/null +++ b/haxe/ui/styles/selector/SelectorParser.hx @@ -0,0 +1,344 @@ +/* + * Cocktail, HTML rendering engine + * http://haxe.org/com/libs/cocktail + * + * Copyright (c) Silex Labs + * Cocktail is available under the MIT license + * http://www.silexlabs.org/labs/cocktail-licensing/ +*/ +package haxe.ui.styles.selector; + +using StringTools; +import haxe.ui.styles.selector.SelectorData; +import haxe.ui.styles.selector.SelectorMetadata; +import haxe.ui.styles.selector.parsers.*; + +/** + * This class is a parser whose role + * is to parse a single CSS selector string, + * and parse it into typed selector data. + * + * Trying to parse multiple selectors (example: "div, p") + * will fail + * + * @author Yannick DOMINGUEZ + */ +class SelectorParser +{ + /** + * Parse the selector string into a typed selector object + * + * @param selector the CSS selector string to parse + * @return the typed selector or null if the selector is invalid + */ + public static function parse(selector:String):SelectorVO + { + var state:SelectorParserState = IGNORE_SPACES; + var next:SelectorParserState = BEGIN_SIMPLE_SELECTOR; + var start:Int = 0; + var position:Int = 0; + var c:Int = selector.fastCodeAt(position); + + var simpleSelectorSequenceStartValue:SimpleSelectorSequenceStartValue = null; + var simpleSelectorSequenceItemValues:Array = []; + var components:Array = []; + + var selectorData:SelectorVO = new SelectorVO(components, PseudoElementSelectorValue.NONE, + false, null, false, null, false, null, false, false, false); + + while (!StringTools.isEof(c)) + { + switch (state) + { + case IGNORE_SPACES: + switch(c) + { + case + '\n'.code, + '\r'.code, + '\t'.code, + ' '.code: + default: + state = next; + continue; + } + + case BEGIN_SIMPLE_SELECTOR: + if (isSelectorChar(c)) + { + state = SIMPLE_SELECTOR; + next = END_TYPE_SELECTOR; + start = position; + } + else + { + switch(c) + { + + case '.'.code: + state = SIMPLE_SELECTOR; + next = END_CLASS_SELECTOR; + start = position + 1; + + case '#'.code: + state = SIMPLE_SELECTOR; + next = END_ID_SELECTOR; + start = position + 1; + + case '*'.code: + state = SIMPLE_SELECTOR; + next = END_UNIVERSAL_SELECTOR; + start = position; + + case ':'.code: + state = BEGIN_PSEUDO_SELECTOR; + start = position; + + case '['.code: + state = BEGIN_ATTRIBUTE_SELECTOR; + start = position; + continue; + + default: + state = INVALID_SELECTOR; + continue; + } + } + + + case BEGIN_ATTRIBUTE_SELECTOR: + position = Attribute.parse(selector, position, simpleSelectorSequenceItemValues); + state = END_SIMPLE_SELECTOR; + next = IGNORE_SPACES; + + case BEGIN_PSEUDO_SELECTOR: + if (isSelectorChar(c)) + { + position = PseudoClass.parse(selector, position, simpleSelectorSequenceItemValues); + state = END_SIMPLE_SELECTOR; + next = IGNORE_SPACES; + } + else + { + switch(c) + { + case ':'.code: + state = PSEUDO_ELEMENT_SELECTOR; + + default: + state = INVALID_SELECTOR; + continue; + } + } + + case PSEUDO_ELEMENT_SELECTOR: + position = PseudoElement.parse(selector, position, selectorData); + state = IGNORE_SPACES; + next = INVALID_SELECTOR; + + case END_SIMPLE_SELECTOR: + switch(c) + { + case ' '.code, '\n'.code, '\r'.code, '>'.code: + state = BEGIN_COMBINATOR; + continue; + + case ':'.code, '#'.code, '.'.code, '['.code: + state = BEGIN_SIMPLE_SELECTOR; + continue; + + default: + state = INVALID_SELECTOR; + continue; + } + + case SIMPLE_SELECTOR: + if (!isSelectorChar(c)) + { + switch(c) + { + case ' '.code, '\n'.code, '\r'.code, '>'.code, ':'.code, '#'.code, '.'.code, '['.code: + state = next; + continue; + + default: + state = INVALID_SELECTOR; + continue; + } + } + + case END_TYPE_SELECTOR: + var type:String = selector.substr(start, position - start); + simpleSelectorSequenceStartValue = SimpleSelectorSequenceStartValue.TYPE(type.toUpperCase()); + state = END_SIMPLE_SELECTOR; + continue; + + case END_CLASS_SELECTOR: + var className:String = selector.substr(start, position - start); + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.CSS_CLASS(className)); + state = END_SIMPLE_SELECTOR; + continue; + + case END_ID_SELECTOR: + var id:String = selector.substr(start, position - start); + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ID(id)); + state = END_SIMPLE_SELECTOR; + continue; + + case END_UNIVERSAL_SELECTOR: + simpleSelectorSequenceStartValue = SimpleSelectorSequenceStartValue.UNIVERSAL; + state = END_SIMPLE_SELECTOR; + continue; + + case BEGIN_COMBINATOR: + + flushSelectors(simpleSelectorSequenceStartValue, simpleSelectorSequenceItemValues, components); + + simpleSelectorSequenceStartValue = null; + simpleSelectorSequenceItemValues = []; + + state = IGNORE_SPACES; + next = COMBINATOR; + continue; + + case COMBINATOR: + + if (isSelectorChar(c)) + { + state = BEGIN_SIMPLE_SELECTOR; + components.push(SelectorComponentValue.COMBINATOR(CombinatorValue.DESCENDANT)); + continue; + } + else + { + switch(c) + { + case '>'.code: + state = IGNORE_SPACES; + next = BEGIN_SIMPLE_SELECTOR; + components.push(SelectorComponentValue.COMBINATOR(CombinatorValue.CHILD)); + + case '+'.code: + state = IGNORE_SPACES; + next = BEGIN_SIMPLE_SELECTOR; + components.push(SelectorComponentValue.COMBINATOR(CombinatorValue.ADJACENT_SIBLING)); + + case '~'.code: + state = IGNORE_SPACES; + next = BEGIN_SIMPLE_SELECTOR; + components.push(SelectorComponentValue.COMBINATOR(CombinatorValue.GENERAL_SIBLING)); + + case ':'.code, '#'.code, '.'.code, '['.code, '*'.code: + state = BEGIN_SIMPLE_SELECTOR; + components.push(SelectorComponentValue.COMBINATOR(CombinatorValue.DESCENDANT)); + continue; + } + } + + case INVALID_SELECTOR: + return null; + } + c = selector.fastCodeAt(++position); + } + + //TODO 2 : dusplaicate code, when reading ident, should + //read until end of file + switch(next) + { + case END_TYPE_SELECTOR: + var type = selector.substr(start, position - start); + //type stored internally as uppercase to match html tag name + simpleSelectorSequenceStartValue = SimpleSelectorSequenceStartValue.TYPE(type.toUpperCase()); + + case END_UNIVERSAL_SELECTOR: + simpleSelectorSequenceStartValue = SimpleSelectorSequenceStartValue.UNIVERSAL; + + case END_CLASS_SELECTOR: + var className:String = selector.substr(start, position - start); + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.CSS_CLASS(className)); + state = END_SIMPLE_SELECTOR; + + case END_ID_SELECTOR: + var id = selector.substr(start, position - start); + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ID(id)); + + default: + } + + flushSelectors(simpleSelectorSequenceStartValue, simpleSelectorSequenceItemValues, components); + + //if at this point, there are no components in + //this selector, it is invalid + if (selectorData.components.length == 0) + { + return null; + } + + //simple selectors and combinators are parsed from left to + //right but are matched from right to left to match + //combinators logic, so the array is reversed + selectorData.components.reverse(); + + //if the selector begins with a class return it, else return null + var firstClass:String = SelectorMetadata.getFirstClass(selectorData.components); + + //check wether the selector only contains a single class + var isSimpleClassSelector:Bool = false; + if (firstClass != null) + { + isSimpleClassSelector = SelectorMetadata.getIsSimpleClassSelector(selectorData.components); + } + + //same as above for Id + var firstId:String = SelectorMetadata.getFirstId(selectorData.components); + + var isSimpleIdSelector:Bool = false; + if (firstId != null) + { + isSimpleIdSelector = SelectorMetadata.getIsSimpleIdSelector(selectorData.components); + } + + //same as above for type + var firstType:String = SelectorMetadata.getFirstType(selectorData.components); + + var isSimpleTypeSelector:Bool = false; + if (firstType != null) + { + isSimpleTypeSelector = SelectorMetadata.getIsSimpleTypeSelector(selectorData.components); + } + + var typedSelector:SelectorVO = new SelectorVO(selectorData.components, selectorData.pseudoElement, + firstClass != null, firstClass, + firstId != null, firstId, + firstType != null, firstType + , isSimpleClassSelector, isSimpleIdSelector, isSimpleTypeSelector); + + return typedSelector; + } + + private static function flushSelectors(simpleSelectorSequenceStartValue:SimpleSelectorSequenceStartValue, simpleSelectorSequenceItemValues:Array, components:Array):Void + { + if (simpleSelectorSequenceStartValue == null && simpleSelectorSequenceItemValues.length == 0) + { + return; + } + + if (simpleSelectorSequenceStartValue == null) + { + simpleSelectorSequenceStartValue = SimpleSelectorSequenceStartValue.UNIVERSAL; + } + + var simpleSelectorSequence:SimpleSelectorSequenceVO = new SimpleSelectorSequenceVO(simpleSelectorSequenceStartValue, simpleSelectorSequenceItemValues); + components.push(SelectorComponentValue.SIMPLE_SELECTOR_SEQUENCE(simpleSelectorSequence)); + + } + + static inline function isAsciiChar(c) { + return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code); + } + + static inline function isSelectorChar(c) { + return isAsciiChar(c) || c == '-'.code || c == '_'.code; + } +} + diff --git a/haxe/ui/styles/selector/SelectorSerializer.hx b/haxe/ui/styles/selector/SelectorSerializer.hx new file mode 100644 index 000000000..bce20f98a --- /dev/null +++ b/haxe/ui/styles/selector/SelectorSerializer.hx @@ -0,0 +1,301 @@ +/* + * Cocktail, HTML rendering engine + * http://haxe.org/com/libs/cocktail + * + * Copyright (c) Silex Labs + * Cocktail is available under the MIT license + * http://www.silexlabs.org/labs/cocktail-licensing/ +*/ +package haxe.ui.styles.selector; + +import haxe.ui.styles.selector.SelectorData; + +/** + * This class serialize a CSS selector + * into a String + * + * @author Yannick DOMINGUEZ + */ +class CSSSelectorSerializer +{ + + /** + * Class constructor. Private as + * this is a static class + */ + private function new() + { + + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC SERIALIZATION METHOD + ////////////////////////////////////////////////////////////////////////////////////////// + + public static function serialize(selector:SelectorVO):String + { + var serializedSelector:String = ""; + + for (i in 0...selector.components.length) + { + var component:SelectorComponentValue = selector.components[i]; + + switch(component) + { + case SIMPLE_SELECTOR_SEQUENCE(value): + serializedSelector += serializeSimpleSelectorSequence(value); + + case COMBINATOR(value): + serializedSelector += serializeCombinator(value); + } + } + + serializedSelector += serializePseudoElement(selector.pseudoElement); + + + return serializedSelector; + } + + ////////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE SERIALIZATION METHODS + ////////////////////////////////////////////////////////////////////////////////////////// + + private static function serializePseudoElement(pseudoElement:PseudoElementSelectorValue):String + { + switch(pseudoElement) + { + case NONE: + return ""; + + case FIRST_LETTER: + return "::first-letter"; + + case FIRST_LINE: + return "::first-line"; + + case BEFORE: + return "::before"; + + case AFTER: + return "::after"; + } + } + + private static function serializeSimpleSelectorSequence(simpleSelectorSequence:SimpleSelectorSequenceVO):String + { + var serializedSimpleSelectorSequence:String = ""; + + serializedSimpleSelectorSequence += serializeStartValue(simpleSelectorSequence.startValue); + + for (i in 0...simpleSelectorSequence.simpleSelectors.length) + { + var simpleSelector:SimpleSelectorSequenceItemValue = simpleSelectorSequence.simpleSelectors[i]; + serializedSimpleSelectorSequence += serializeSimpleSelector(simpleSelector); + } + + return serializedSimpleSelectorSequence; + } + + private static function serializeCombinator(combinator:CombinatorValue):String + { + switch(combinator) + { + case DESCENDANT: + return " "; + + case CHILD: + return " > "; + + case ADJACENT_SIBLING: + return " + "; + + case GENERAL_SIBLING: + return " ~ "; + } + } + + private static function serializeStartValue(selectorStartValue:SimpleSelectorSequenceStartValue):String + { + switch(selectorStartValue) + { + case UNIVERSAL: + return "*"; + + case TYPE(value): + return value; + } + } + + private static function serializeSimpleSelector(simpleSelector:SimpleSelectorSequenceItemValue):String + { + switch (simpleSelector) + { + case ID(value): + return "#" + value; + + case CSS_CLASS(value): + return "." + value; + + case ATTRIBUTE(value): + return serializeAttributeSelector(value); + + case PSEUDO_CLASS(value): + return serializePseudoClassSelector(value); + } + } + + private static function serializeAttributeSelector(attributeSelector:AttributeSelectorValue):String + { + switch(attributeSelector) + { + case ATTRIBUTE(value): + return "[" + value + "]"; + + case ATTRIBUTE_VALUE(name, value): + return '[' + name + '="' + value + '"]'; + + case ATTRIBUTE_LIST(name, value): + return '[' + name + '~="' + value + '"]'; + + case ATTRIBUTE_VALUE_BEGINS(name, value): + return '[' + name + '^="' + value + '"]'; + + case ATTRIBUTE_VALUE_ENDS(name, value): + return '[' + name + '$="' + value + '"]'; + + case ATTRIBUTE_VALUE_CONTAINS(name, value): + return '[' + name + '*="' + value + '"]'; + + case ATTRIBUTE_VALUE_BEGINS_HYPHEN_LIST(name, value): + return '[' + name + '|="' + value + '"]'; + } + } + + private static function serializePseudoClassSelector(pseudoClassSelector:PseudoClassSelectorValue):String + { + switch(pseudoClassSelector) + { + case STRUCTURAL(value): + return serializeStructuralPseudoClassSelector(value); + + case LINK(value): + return serializeLinkPseudoClassSelector(value); + + case TARGET: + return ":target"; + + case FULLSCREEN: + return ":fullscreen"; + + case LANG(value): + return serializeLangPseudoClassSelector(value); + + case USER_ACTION(value): + return serializeUserActionPseudoClassSelector(value); + + case UI_ELEMENT_STATES(value): + return serializeUIElementStatePseudoClass(value); + + case NOT(value): + return ":not("+serializeSimpleSelectorSequence(value)+")"; + + case CUSTOM(value): + return ':$value'; + } + } + + private static function serializeUIElementStatePseudoClass(uiElementStateSelector:UIElementStatesValue):String + { + switch (uiElementStateSelector) + { + case ENABLED: + return ":enabled"; + + case DISABLED: + return ":disabled"; + + case CHECKED: + return ":checked"; + } + } + + private static function serializeLangPseudoClassSelector(langs:Array):String + { + var serializedLangSelector:String = ":lang("; + + for (i in 0...langs.length) + { + serializedLangSelector += langs[i]; + if (i < langs.length) + { + serializedLangSelector += "-"; + } + } + + serializedLangSelector += ")"; + return serializedLangSelector; + } + + private static function serializeLinkPseudoClassSelector(linkPseudoClassSelector:LinkPseudoClassValue):String + { + switch(linkPseudoClassSelector) + { + case VISITED: + return ":visited"; + + case LINK: + return ":link"; + } + } + + private static function serializeUserActionPseudoClassSelector(userActionPseudoClassSelector:UserActionPseudoClassValue):String + { + switch (userActionPseudoClassSelector) + { + case ACTIVE: + return ":active"; + + case HOVER: + return ":hover"; + + case FOCUS: + return ":focus"; + } + } + + private static function serializeStructuralPseudoClassSelector(structuralpseudoClassSelector:StructuralPseudoClassSelectorValue):String + { + switch(structuralpseudoClassSelector) + { + case ROOT: + return ":root"; + + case FIRST_CHILD: + return ":first-child"; + + case LAST_CHILD: + return ":last-child"; + + case FIRST_OF_TYPE: + return ":first-of-type"; + + case LAST_OF_TYPE: + return ":last-of-type"; + + case ONLY_CHILD: + return ":only-child"; + + case ONLY_OF_TYPE: + return ":only-of-type"; + + case EMPTY: + return ":empty"; + + //TODO 1 : values with arguments + default: + return ""; + } + } + +} + diff --git a/haxe/ui/styles/selector/SelectorSpecificity.hx b/haxe/ui/styles/selector/SelectorSpecificity.hx new file mode 100644 index 000000000..02f1e0e56 --- /dev/null +++ b/haxe/ui/styles/selector/SelectorSpecificity.hx @@ -0,0 +1,107 @@ +package haxe.ui.styles.selector; + +import haxe.ui.styles.selector.SelectorData; + +/** + * Returns the specificity of a selector which is + * 'weight', used during cascading. + * If multiple selectors matches the same elements, the most + * specific one is used + */ +class SelectorSpecificity +{ + /** + * Return the specifity of a selector, which is + * its priority next to other selector + */ + public static function get(selector:SelectorVO):Int + { + //holds the specicities values + var selectorSpecificityVO = new SelectorSpecificityVO(); + + //a pseudo element increment the specificity + switch (selector.pseudoElement) + { + case PseudoElementSelectorValue.FIRST_LETTER, + PseudoElementSelectorValue.FIRST_LINE, + PseudoElementSelectorValue.AFTER, + PseudoElementSelectorValue.BEFORE: + selectorSpecificityVO.typeAndPseudoElementsNumber++; + + case PseudoElementSelectorValue.NONE: + } + + var components:Array = selector.components; + var length:Int = components.length; + for (i in 0...length) + { + var component:SelectorComponentValue = components[i]; + + switch(component) + { + case SelectorComponentValue.COMBINATOR(value): + + case SelectorComponentValue.SIMPLE_SELECTOR_SEQUENCE(value): + getSimpleSelectorSequenceSpecificity(value, selectorSpecificityVO); + } + } + + //specificity has 3 categories, whose int values are concatenated + //for instance, if idSelectorsNumber is equal to 1, classAttributesAndPseudoClassesNumber to 0 + //and typeAndPseudoElementsNumber to 2, + //the specificity is 102 + return selectorSpecificityVO.idSelectorsNumber * 100 + selectorSpecificityVO.classAttributesAndPseudoClassesNumber * 10 + selectorSpecificityVO.typeAndPseudoElementsNumber; + } + + /** + * Increment the specificity of simple selector sequence + */ + private static function getSimpleSelectorSequenceSpecificity(simpleSelectorSequence:SimpleSelectorSequenceVO, selectorSpecificity:SelectorSpecificityVO):Void + { + getSimpleSelectorSequenceStartSpecificity(simpleSelectorSequence.startValue, selectorSpecificity); + + var simpleSelectors:Array = simpleSelectorSequence.simpleSelectors; + var length:Int = simpleSelectors.length; + for (i in 0...length) + { + var simpleSelectorSequenceItem:SimpleSelectorSequenceItemValue = simpleSelectors[i]; + getSimpleSelectorSequenceItemSpecificity(simpleSelectorSequenceItem, selectorSpecificity); + } + } + + /** + * Increment specificity according to a simple selector start item + */ + private static function getSimpleSelectorSequenceStartSpecificity(simpleSelectorSequenceStart:SimpleSelectorSequenceStartValue, selectorSpecificity:SelectorSpecificityVO):Void + { + switch(simpleSelectorSequenceStart) + { + case SimpleSelectorSequenceStartValue.TYPE(value): + selectorSpecificity.typeAndPseudoElementsNumber++; + + case SimpleSelectorSequenceStartValue.UNIVERSAL: + } + } + + /** + * Increment specificity according to a simple selector item + */ + private static function getSimpleSelectorSequenceItemSpecificity(simpleSelectorSequenceItem:SimpleSelectorSequenceItemValue, selectorSpecificity:SelectorSpecificityVO):Void + { + switch (simpleSelectorSequenceItem) + { + case ATTRIBUTE(value): + selectorSpecificity.classAttributesAndPseudoClassesNumber++; + + case PSEUDO_CLASS(value): + selectorSpecificity.classAttributesAndPseudoClassesNumber++; + + case CSS_CLASS(value): + selectorSpecificity.classAttributesAndPseudoClassesNumber++; + + case ID(value): + selectorSpecificity.idSelectorsNumber++; + } + } +} + diff --git a/haxe/ui/styles/selector/SelectorsParser.hx b/haxe/ui/styles/selector/SelectorsParser.hx new file mode 100644 index 000000000..b1e552da5 --- /dev/null +++ b/haxe/ui/styles/selector/SelectorsParser.hx @@ -0,0 +1,115 @@ +package haxe.ui.styles.selector; + +using StringTools; +using Lambda; +import haxe.ui.styles.selector.SelectorData; + +/** + * Parses one or many CSS selector(s) + * + * @author Yannick DOMINGUEZ + */ +class SelectorsParser +{ + /** + * Takes a string containing or multiple comma-separated CSS selectors + * and return an array of typed selectors, or null if at least one of the + * selector is invalid + * + * @param selectors the CSS selectors to parse + * @return an array of typed selectors or null if one or more selectors are + * invalid + */ + public static function parse(selectors:String):Array + { + var typedSelectors = new Array(); + + var state:SelectorsParserState = IGNORE_SPACES; + var next:SelectorsParserState = BEGIN_SELECTOR; + var start:Int = 0; + var position:Int = 0; + var c:Int = selectors.fastCodeAt(position); + + while (!StringTools.isEof(c)) + { + switch (state) + { + case IGNORE_SPACES: + switch(c) + { + case + '\n'.code, + '\r'.code, + '\t'.code, + ' '.code: + default: + state = next; + continue; + } + + case BEGIN_SELECTOR: + state = SELECTOR; + next = END_SELECTOR; + start = position; + continue; + + case SELECTOR: + if (!isSelectorChar(c)) + { + switch(c) + { + case ','.code: + state = END_SELECTOR; + next = BEGIN_SELECTOR; + continue; + } + } + + case END_SELECTOR: + var selector:String = selectors.substr(start, position - start); + typedSelectors.push(SelectorParser.parse(selector)); + state = next; + } + c = selectors.fastCodeAt(++position); + } + + //parse last selector if any + var selector:String = selectors.substr(start, position - start); + typedSelectors.push(SelectorParser.parse(selector)); + + //if one selector is invalid, the whole rule and selectors are + //invalid and won't be used during cascade + if (typedSelectors.has(null)) + { + return null; + } + + return typedSelectors; + } + + /** + * Parse one selector. Returns a typed selector or null if invalid + */ + private static function parseSelector(selector:String):SelectorVO + { + var typedSelector:SelectorVO = SelectorParser.parse(selector); + + //if one selector is invalid, the whole rule and selectors are + //invalid and won't be used during cascade + if (typedSelector == null) + { + return null; + } + + return typedSelector; + } + + static inline function isSelectorChar(c:Int):Bool { + return isAsciiChar(c) || c == ':'.code || c == '.'.code || c == '*'.code; + } + + static inline function isAsciiChar(c) { + return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code); + } +} + diff --git a/haxe/ui/styles/selector/matchers/Attributes.hx b/haxe/ui/styles/selector/matchers/Attributes.hx new file mode 100644 index 000000000..5e498f8ea --- /dev/null +++ b/haxe/ui/styles/selector/matchers/Attributes.hx @@ -0,0 +1,137 @@ +package haxe.ui.styles.selector.matchers; + +import haxe.ui.styles.selector.SelectorData; + +/** + * Functions to match attribute selectors + */ +class Attributes +{ + /** + * Return wether an attribute selector + * matches the element + */ + public static function match(getAttribute:String->String, attributeSelector:AttributeSelectorValue):Bool + { + return switch(attributeSelector) + { + case AttributeSelectorValue.ATTRIBUTE(value): + getAttribute(value) != null; + + case AttributeSelectorValue.ATTRIBUTE_VALUE(name, value): + getAttribute(name) == value; + + case AttributeSelectorValue.ATTRIBUTE_LIST(name, value): + matchAttributeList(getAttribute, name, value); + + case AttributeSelectorValue.ATTRIBUTE_VALUE_BEGINS(name, value): + matchAttributeBeginValue(getAttribute, name, value); + + case AttributeSelectorValue.ATTRIBUTE_VALUE_CONTAINS(name, value): + matchAttributeContainsValue(getAttribute, name, value); + + case AttributeSelectorValue.ATTRIBUTE_VALUE_ENDS(name, value): + matchAttributeEndValue(getAttribute, name, value); + + case AttributeSelectorValue.ATTRIBUTE_VALUE_BEGINS_HYPHEN_LIST(name, value): + matchAttributeBeginsHyphenList(getAttribute, name, value); + } + } + + /** + * return wether the value of the "name" attribute is a hyphen + * separated list whose first item is "value" + */ + private static function matchAttributeBeginsHyphenList(getAttribute:String->String, name:String, value:String):Bool + { + var attributeValue:String = getAttribute(name); + //early exit if the attribute doesn't exist on the element + if (attributeValue == null) + { + return false; + } + + //valid if value exactly matches the attribute + if (attributeValue == value) + { + return true; + } + + //else valid if begins with value + hyphen + var hyphenValue:String = value + "-"; + return attributeValue.substr(0, hyphenValue.length) == hyphenValue; + } + + /** + * Return wether the value of the "name" attribute ends with "value" + */ + private static function matchAttributeEndValue(getAttribute:String->String, name:String, value:String):Bool + { + var attributeValue:String = getAttribute(name); + //early exit if the attribute doesn't exist on the element + if (attributeValue == null) + { + return false; + } + + return attributeValue.lastIndexOf(value) == attributeValue.length - value.length; + } + + /** + * Return wether the value of the "name" attribute contains "value" + */ + private static function matchAttributeContainsValue(getAttribute:String->String, name:String, value:String):Bool + { + var attributeValue:String = getAttribute(name); + //early exit if the attribute doesn't exist on the element + if (attributeValue == null) + { + return false; + } + + return attributeValue.indexOf(value) != -1; + } + + /** + * Return wether the value of the "name" attribute + * on the element begins with "value" + */ + private static function matchAttributeBeginValue(getAttribute:String->String, name:String, value:String):Bool + { + var attributeValue:String = getAttribute(name); + //early exit if the attribute doesn't exist on the element + if (attributeValue == null) + { + return false; + } + + return attributeValue.indexOf(value) == 0; + } + + /** + * Return wether "value" is a part of the "name" attribute + * which is a white-space separated list of values + */ + private static function matchAttributeList(getAttribute:String->String, name:String, value:String):Bool + { + var attributeValue:String = getAttribute(name); + //early exit if the attribute doesn't exist on the element + if (attributeValue == null) + { + return false; + } + + trace(attributeValue); + var attributeValueAsList:Array = attributeValue.split(" "); + for (i in 0...attributeValueAsList.length) + { + if (attributeValueAsList[i] == value) + { + return true; + } + } + + return false; + } +} + diff --git a/haxe/ui/styles/selector/matchers/PseudoClass.hx b/haxe/ui/styles/selector/matchers/PseudoClass.hx new file mode 100644 index 000000000..ec0afd9ae --- /dev/null +++ b/haxe/ui/styles/selector/matchers/PseudoClass.hx @@ -0,0 +1,283 @@ +package haxe.ui.styles.selector.matchers; + +import haxe.ui.core.Component; +import haxe.ui.styles.selector.SelectorData; +typedef Element = Component; + +// import haxe.ui.styles.dom.Element; +// import haxe.ui.styles.dom.DOMConstants; + +/** + * Functions to match pseudo class selectors + */ +class PseudoClass +{ + /** + * Return wether a pseudo class matches + * the element + */ + public static function match(element:Element, pseudoClassSelector:PseudoClassSelectorValue, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + switch (pseudoClassSelector) + { + case PseudoClassSelectorValue.STRUCTURAL(value): + return matchStructuralPseudoClassSelector(element, value); + + case PseudoClassSelectorValue.LINK(value): + return matchLinkPseudoClassSelector(element, value, matchedPseudoClasses); + + case PseudoClassSelectorValue.USER_ACTION(value): + return matchUserActionPseudoClassSelector(element, value, matchedPseudoClasses); + + case PseudoClassSelectorValue.TARGET: + return matchTargetPseudoClassSelector(element); + + case PseudoClassSelectorValue.NOT(value): + return matchNegationPseudoClassSelector(element, value); + + case PseudoClassSelectorValue.LANG(value): + return matchLangPseudoClassSelector(element, value); + + case PseudoClassSelectorValue.UI_ELEMENT_STATES(value): + return matchUIElementStatesSelector(element, value, matchedPseudoClasses); + + case PseudoClassSelectorValue.FULLSCREEN: + return matchedPseudoClasses.fullscreen; + + case PseudoClassSelectorValue.CUSTOM(value): + return matchedPseudoClasses.nodeClassList.contains(value); + + default: + return false; + } + } + + /** + * Return wether a UI state selector + * matches the element + */ + private static function matchUIElementStatesSelector(element:Element, uiElementState:UIElementStatesValue, matchedPseudoClasses:MatchedPseudoClassesVO):Bool + { + switch(uiElementState) + { + case UIElementStatesValue.CHECKED: + return matchedPseudoClasses.checked; + + case UIElementStatesValue.DISABLED: + return matchedPseudoClasses.disabled; + + case UIElementStatesValue.ENABLED: + return matchedPseudoClasses.enabled; + } + } + + /** + * Return wether a negation pseudo-class selector + * matches the element + */ + private static function matchNegationPseudoClassSelector(element:Element, negationSimpleSelectorSequence:SimpleSelectorSequenceVO):Bool + { + return false; + } + + /** + * Return wether a lang pseudo-class selector + * matches the element + */ + private static function matchLangPseudoClassSelector(element:Element, lang:Array):Bool + { + return false; + } + + /** + * Return wether a structural pseudo-class selector + * matches the element + */ + private static function matchStructuralPseudoClassSelector(element:Element, structuralPseudoClassSelector:StructuralPseudoClassSelectorValue):Bool + { + switch(structuralPseudoClassSelector) + { + case StructuralPseudoClassSelectorValue.EMPTY: + return element.childComponents.length == 0; + + case StructuralPseudoClassSelectorValue.FIRST_CHILD: + + //HTML root element is not considered a first child + // + //TODO : parent of root element should actually be a document + // if (element.parentComponent == null) + // return false; + + // return element.previousSibling == null; + return false; + + case StructuralPseudoClassSelectorValue.LAST_CHILD: + + //HTML root element not considered last child + // if (element.parentComponent == null) + // return false; + + // return element.nextSibling == null; + return false; + + case StructuralPseudoClassSelectorValue.ONLY_CHILD: + + //HTML root element is not considered only child + if (element.parentComponent == null) + { + return false; + } + + return element.parentComponent.childComponents.length == 1; + + case StructuralPseudoClassSelectorValue.ROOT: + // return element.tagName == DOMConstants.HTML_HTML_TAG_NAME && element.parentComponent == null; + return false; + + case StructuralPseudoClassSelectorValue.ONLY_OF_TYPE: + return matchOnlyOfType(element); + + case StructuralPseudoClassSelectorValue.FIRST_OF_TYPE: + return matchFirstOfType(element); + + case StructuralPseudoClassSelectorValue.LAST_OF_TYPE: + return matchLastOfType(element); + + case StructuralPseudoClassSelectorValue.NTH_CHILD(value): + return matchNthChild(element, value); + + case StructuralPseudoClassSelectorValue.NTH_LAST_CHILD(value): + return matchNthLastChild(element, value); + + case StructuralPseudoClassSelectorValue.NTH_LAST_OF_TYPE(value): + return matchNthLastOfType(element, value); + + case StructuralPseudoClassSelectorValue.NTH_OF_TYPE(value): + return matchNthOfType(element, value); + } + } + + private static function matchNthChild(element:Element, value:StructuralPseudoClassArgumentValue):Bool + { + // if (element.parentComponent == null) return false; + // final idx = element.parentComponent.childComponents.indexOf(element)+1; + // switch(value) { + // case INDEX(_idx): return _idx == idx; + // case ODD: return (idx & 1) != 0; + // case EVEN: return (idx & 1) != 0; + // } + return false; + } + + private static function matchNthLastChild(element:Element, value:StructuralPseudoClassArgumentValue):Bool + { + // if (element.parentComponent == null) return false; + // final idx:Int = element.parentComponent.childComponents.length - (element.parentComponent.childComponents.indexOf(element) + 1); + // switch(value) { + // case INDEX(_idx): return _idx == idx; + // case ODD: return (idx & 1) != 0; + // case EVEN: return (idx & 1) != 0; + // } + return false; + } + + private static function matchNthLastOfType(element:Element, value:StructuralPseudoClassArgumentValue):Bool + { + return false; + } + + private static function matchNthOfType(element:Element, value:StructuralPseudoClassArgumentValue):Bool + { + return false; + } + + /** + * Return wether the element is the first + * element among its element siblings of + * its type (tag name) + */ + private static function matchFirstOfType(element:Element):Bool + { + // var type = element.tagName; + // var previousSibling:Element = element.previousSibling; + // while (previousSibling != null) { + // if (previousSibling.tagName == type) + // return false; + // previousSibling = previousSibling.previousSibling; + // } + // return true; + return false; + } + + /** + * Same as above but for last element + */ + private static function matchLastOfType(element:Element):Bool + { + // var type = element.tagName; + // var nextSibling:Element = element.nextSibling; + // while (nextSibling != null) { + // if (nextSibling.tagName == type) + // return false; + // nextSibling = nextSibling.nextSibling; + // } + // return true; + return false; + } + + /** + * Return wether this element is the only among + * its element sibling of its type (tag name) + */ + private static function matchOnlyOfType(element:Element):Bool + { + //to be the only of its type is the same as + //being the first and last of its type + return matchLastOfType(element) == true && matchFirstOfType(element) == true; + } + + /** + * Return wether a link pseudo-class selector + * matches the element + */ + private static function matchLinkPseudoClassSelector(element:Element, linkPseudoClassSelector:LinkPseudoClassValue, matchedPseudoClass:MatchedPseudoClassesVO):Bool + { + switch(linkPseudoClassSelector) + { + case LinkPseudoClassValue.LINK: + return matchedPseudoClass.link; + + case LinkPseudoClassValue.VISITED: + return false; + } + } + + /** + * Return wether a user pseudo-class selector + * matches the element + */ + private static function matchUserActionPseudoClassSelector(element:Element, userActionPseudoClassSelector:UserActionPseudoClassValue, matchedPseudoClass:MatchedPseudoClassesVO):Bool + { + switch(userActionPseudoClassSelector) + { + case UserActionPseudoClassValue.ACTIVE: + return matchedPseudoClass.active; + + case UserActionPseudoClassValue.HOVER: + return matchedPseudoClass.hover; + + case UserActionPseudoClassValue.FOCUS: + return matchedPseudoClass.focus; + } + } + + /** + * Return wether the target pseudo-class + * matches the element. + */ + private static function matchTargetPseudoClassSelector(element:Element):Bool + { + return false; + } +} + diff --git a/haxe/ui/styles/selector/parsers/Attribute.hx b/haxe/ui/styles/selector/parsers/Attribute.hx new file mode 100644 index 000000000..953847065 --- /dev/null +++ b/haxe/ui/styles/selector/parsers/Attribute.hx @@ -0,0 +1,193 @@ +package haxe.ui.styles.selector.parsers; + +import haxe.ui.styles.selector.SelectorData; +using StringTools; + +/** + * CSS Selector attribute parser + */ +class Attribute +{ + public static function parse(selector:String, position:Int, simpleSelectorSequenceItemValues:Array):Int + { + //consumes '[' token + position++; + + var c:Int = selector.fastCodeAt(position); + var start:Int = position; + + var attribute:String = null; + var op:String = null; // operator + var value:String = null; + + var state:AttributeSelectorParserState = IGNORE_SPACES; + var next:AttributeSelectorParserState = ATTRIBUTE; + + while (true) + { + switch(state) + { + case IGNORE_SPACES: + switch(c) + { + case + '\n'.code, + '\r'.code, + '\t'.code, + ' '.code: + default: + state = next; + continue; + } + + case ATTRIBUTE: + if (!isSelectorChar(c)) + { + attribute = selector.substr(start, position - start); + + if (c == ']'.code) + { + state = END_SELECTOR; + continue; + } + else + { + state = IGNORE_SPACES; + next = BEGIN_OPERATOR; + continue; + } + } + + case BEGIN_OPERATOR: + start = position; + state = OPERATOR; + + case OPERATOR: + if (!isOperatorChar(c)) + { + op = selector.substr(start, position - start); + state = IGNORE_SPACES; + next = END_OPERATOR; + continue; + } + + case END_OPERATOR: + switch(c) + { + case '"'.code, "'".code: + position++; + start = position; + state = STRING_VALUE; + + case ']'.code: + state = END_SELECTOR; + continue; + + default: + + if (isSelectorChar(c) == true) + { + start = position; + state = IDENTIFIER_VALUE; + } + else + { + state = INVALID_SELECTOR; + } + } + + case STRING_VALUE: + if (!isSelectorChar(c)) + { + switch (c) + { + case '"'.code, "'".code: + value = selector.substr(start, position - start); + state = END_SELECTOR; + + case ']'.code: + state = INVALID_SELECTOR; + + default: + state = INVALID_SELECTOR; + } + } + + case IDENTIFIER_VALUE: + if (!isSelectorChar(c)) + { + switch (c) + { + case ']'.code: + value = selector.substr(start, position - start); + state = END_SELECTOR; + continue; + + default: + state = INVALID_SELECTOR; + } + } + + case INVALID_SELECTOR: + attribute = null; + break; + + case END_SELECTOR: + break; + + } + c = selector.fastCodeAt(++position); + } + + //invalid selector + if (attribute == null) + { + return -1; + } + + if (op != null) + { + switch(op) + { + case '=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_VALUE(attribute, value))); + + case '^=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_VALUE_BEGINS(attribute, value))); + + case '~=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_LIST(attribute, value))); + + case '$=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_VALUE_ENDS(attribute, value))); + + case '*=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_VALUE_CONTAINS(attribute, value))); + + case '|=': + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE_VALUE_BEGINS_HYPHEN_LIST(attribute, value))); + } + } + else + { + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.ATTRIBUTE(AttributeSelectorValue.ATTRIBUTE(attribute))); + } + + return position; + } + + static inline function isSelectorChar(c) + { + return isAsciiChar(c) || c == '-'.code || c == '_'.code; + } + + static inline function isAsciiChar(c) + { + return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code); + } + + static inline function isOperatorChar(c:Int):Bool + { + return c == '='.code || c == '~'.code || c == '^'.code || c == '$'.code || c == '*'.code || c == '|'.code; + } +} diff --git a/haxe/ui/styles/selector/parsers/PseudoClass.hx b/haxe/ui/styles/selector/parsers/PseudoClass.hx new file mode 100644 index 000000000..2ce7d681e --- /dev/null +++ b/haxe/ui/styles/selector/parsers/PseudoClass.hx @@ -0,0 +1,124 @@ +package haxe.ui.styles.selector.parsers; + +import haxe.ui.styles.selector.SelectorData; +using StringTools; + +/** + * CSS Selector pseudo-class parser + */ +class PseudoClass +{ + //TODO : parse pseudo class with arguments + public static function parse(selector:String, position:Int, simpleSelectorSequenceItemValues:Array):Int + { + var c:Int = selector.fastCodeAt(position); + var start:Int = position; + + while (true) + { + if (!isPseudoClassChar(c)) + { + break; + } + c = selector.fastCodeAt(++position); + } + + var pseudoClass:String = selector.substr(start, position - start); + + var typedPseudoClass:PseudoClassSelectorValue = switch(pseudoClass) + { + case 'first-child': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.FIRST_CHILD); + + case 'last-child': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.LAST_CHILD); + + case 'empty': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.EMPTY); + + case 'root': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.ROOT); + + case 'first-of-type': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.FIRST_OF_TYPE); + + case 'last-of-type': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.LAST_OF_TYPE); + + case 'only-of-type': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.ONLY_OF_TYPE); + + case 'only-child': + PseudoClassSelectorValue.STRUCTURAL(StructuralPseudoClassSelectorValue.ONLY_CHILD); + + case 'link': + PseudoClassSelectorValue.LINK(LinkPseudoClassValue.LINK); + + case 'visited': + PseudoClassSelectorValue.LINK(LinkPseudoClassValue.VISITED); + + case 'active': + PseudoClassSelectorValue.USER_ACTION(UserActionPseudoClassValue.ACTIVE); + + case 'hover': + PseudoClassSelectorValue.USER_ACTION(UserActionPseudoClassValue.HOVER); + + case 'focus': + PseudoClassSelectorValue.USER_ACTION(UserActionPseudoClassValue.FOCUS); + + case 'target': + PseudoClassSelectorValue.TARGET; + + case 'fullscreen': + PseudoClassSelectorValue.FULLSCREEN; + + // case 'nth-child': + // //TODO + + // case 'nth-last-child': + // //TODO + + // case 'nth-of-type': + // //TODO + + // case 'nth-last-of-type': + // //TODO + + // case 'not': + // //TODO + + // case 'lang': + // //TODO + + case 'enabled': + PseudoClassSelectorValue.UI_ELEMENT_STATES(UIElementStatesValue.ENABLED); + + case 'disabled': + PseudoClassSelectorValue.UI_ELEMENT_STATES(UIElementStatesValue.DISABLED); + + case 'checked': + PseudoClassSelectorValue.UI_ELEMENT_STATES(UIElementStatesValue.CHECKED); + + default: + PseudoClassSelectorValue.CUSTOM(pseudoClass); + } + + //selector is invalid + if (typedPseudoClass == null) + { + return -1; + } + + simpleSelectorSequenceItemValues.push(SimpleSelectorSequenceItemValue.PSEUDO_CLASS(typedPseudoClass)); + + return --position; + } + + static inline function isPseudoClassChar(c) { + return isAsciiChar(c) || c == '-'.code; + } + + static inline function isAsciiChar(c) { + return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code); + } +} diff --git a/haxe/ui/styles/selector/parsers/PseudoElement.hx b/haxe/ui/styles/selector/parsers/PseudoElement.hx new file mode 100644 index 000000000..219155da2 --- /dev/null +++ b/haxe/ui/styles/selector/parsers/PseudoElement.hx @@ -0,0 +1,59 @@ +package haxe.ui.styles.selector.parsers; + +import haxe.ui.styles.selector.SelectorData; +using StringTools; + +/** + * CSS Selector pseudo-element parser + */ +class PseudoElement +{ + public static function parse(selector:String, position:Int, selectorData:SelectorVO):Int + { + var c:Int = selector.fastCodeAt(position); + var start:Int = position; + + while (true) + { + if (!isPseudoClassChar(c)) + { + break; + } + c = selector.fastCodeAt(++position); + } + + var pseudoElement:String = selector.substr(start, position - start); + var typedPseudoElement:PseudoElementSelectorValue = null; + + switch (pseudoElement) + { + case 'first-line': + typedPseudoElement = PseudoElementSelectorValue.FIRST_LINE; + + case 'first-letter': + typedPseudoElement = PseudoElementSelectorValue.FIRST_LETTER; + + case 'before': + typedPseudoElement = PseudoElementSelectorValue.BEFORE; + + case 'after': + typedPseudoElement = PseudoElementSelectorValue.AFTER; + + default: + typedPseudoElement = PseudoElementSelectorValue.NONE; + + } + + selectorData.pseudoElement = typedPseudoElement; + + return --position; + } + + static inline function isPseudoClassChar(c) { + return isAsciiChar(c) || c == '-'.code; + } + + static inline function isAsciiChar(c) { + return (c >= 'a'.code && c <= 'z'.code) || (c >= 'A'.code && c <= 'Z'.code) || (c >= '0'.code && c <= '9'.code); + } +} diff --git a/haxe/utils/enumExtractor/EnumExtractor.hx b/haxe/utils/enumExtractor/EnumExtractor.hx new file mode 100644 index 000000000..0a055f581 --- /dev/null +++ b/haxe/utils/enumExtractor/EnumExtractor.hx @@ -0,0 +1,73 @@ +package haxe.utils.enumExtractor; + +// MIT: https://github.com/ErikRikoo/Enum-Extractor/tree/master + +#if macro +import haxe.macro.ExprTools; +import haxe.macro.Context; +import haxe.macro.Expr; + +using haxe.utils.enumExtractor.Util; + +class EnumExtractor { + private static final metaName = "as"; + + public static macro function build():Array { + var fields:Array = Context.getBuildFields(); + + for(field in fields) { + switch(field.kind) { + case FFun(f): + f.expr = modifyExpr(f.expr); + default: + } + } + + return fields; + } + + private static function modifyExpr(e:Expr):Expr { + return switch(e.expr) { + case null: + null; + case EMeta(entry, block) if(entry.name == metaName): + buildExtractingExpression(entry.params, ExprTools.map(block, modifyExpr), e.pos); + default: + ExprTools.map(e, modifyExpr); + } + } + + private static function buildExtractingExpression(params:Array, block:Expr, metaPos:Position):Expr { + var conditionnal = params[1].removeIfMeta(); + + switch(params[0]) { + case macro $value => $pattern: + var ret = if(conditionnal == null) { + macro switch ($value) { + case $pattern: $block; + default: {} + }; + } else { + macro switch ($value) { + case $pattern if($conditionnal): $block; + default: {} + }; + } + // trace(ExprTools.toString(ret)); + return ret; + case null: + throw new Error("Invalid pattern: it must not be empty", metaPos); + default: + throw new Error("Invalid pattern: it must be expr => expr", params[0].pos); + } + } +} + +#else + +@:autoBuild(haxe.utils.enumExtractor.EnumExtractor.build()) +interface EnumExtractor { + +} + +#end \ No newline at end of file diff --git a/haxe/utils/enumExtractor/Util.hx b/haxe/utils/enumExtractor/Util.hx new file mode 100644 index 000000000..1e102aa74 --- /dev/null +++ b/haxe/utils/enumExtractor/Util.hx @@ -0,0 +1,16 @@ +package haxe.utils.enumExtractor; + +import haxe.macro.Expr; + +class Util { + public static function removeIfMeta(e:Expr):Expr { + return switch(e) { + case null: + null; + case macro @if $c: + return c; + default: + e; + } + } +} \ No newline at end of file diff --git a/haxe/utils/trie/NodeType.hx b/haxe/utils/trie/NodeType.hx new file mode 100644 index 000000000..aa694c07b --- /dev/null +++ b/haxe/utils/trie/NodeType.hx @@ -0,0 +1,7 @@ +package haxe.utils.trie; + +enum NodeType { + Terminal(value:V); + Node(key:K, children:Array>); + Root(children:Array>); +} \ No newline at end of file diff --git a/haxe/utils/trie/Trie.hx b/haxe/utils/trie/Trie.hx new file mode 100644 index 000000000..f38d0077e --- /dev/null +++ b/haxe/utils/trie/Trie.hx @@ -0,0 +1,143 @@ +package haxe.utils.trie; + +// MIT: https://github.com/ErikRikoo/TreeMap + +import haxe.utils.enumExtractor.EnumExtractor; +import haxe.utils.trie.NodeType; + +using haxe.utils.trie.Util; + +private typedef Matcher = (v1:T, v2:T) -> Bool; + +class Trie implements EnumExtractor { + private var root:NodeType = Root([]); + private var current:NodeType = null; + private var matcher:Matcher; + + public function new(?m:Matcher) { + matcher = if(m != null) m else (v1:K, v2:K) -> v1 == v2; + } + + public function has(keys:Array):Bool { + return get(keys) != null; + } + + public function get(keys:Array):V { + var index:Int = followPath(keys); + + if(index != keys.length) { + current = null; + return null; + } else { + var v:V = current.getTerminalValue(); + current = null; + return v; + } + } + + public function getAutocompletion(_keys:Array):Array { + var res = []; + + // get last matching node + var cur = root; + var val = ""; + var index:Int = 0; + for(key in _keys) { + var node = getChildren(key, cur); + switch(node) { + case Terminal(_): + return [_keys.join('')]; + case Node(v, _): + val += v; + cur = node; + case Root(_): + cur = node; + case null: + return res; + } + } + + // now travese down all children + var open = [{val: val.substring(0, val.length - 1), node:cur}]; + while (open.length > 0) { + var cur = open.shift(); + switch(cur.node) { + case Node(key, children): + cur.val += key; + for (c in children) + open.push({val: ""+cur.val, node: c}); + case Terminal(_): + res.push(cur.val); + default: + } + } + + return res; + } + + public function add(keys:Array, value:V) { + var node = createBranch(keys); + + if(node.getTerminalValue() == null) { + node.appendChild(Terminal(value)); + } else { + throw "Value is already set"; + } + + current = null; + } + + public function set(keys:Array, value:V) { + var node = createBranch(keys); + node.updateTerminalValue(value); + + current = null; + } + + public function clear() { + root = Root([]); + } + + private function createBranch(keys:Array) { + var begin:Int = followPath(keys); + + for(i in begin...keys.length) { + var newNode:NodeType = Node(keys[i], []); + current.appendChild(newNode); + current = newNode; + } + + return current; + } + + private function followPath(keys:Array):Int { + current = root; + var index:Int = 0; + for(key in keys) { + var node = getChildren(key, current); + switch(node) { + case null | Terminal(_) | Root(_): + return index; + case Node(_, _): + ++index; + current = node; + } + } + + return index; + } + + private function getChildren(key:K, node:NodeType):NodeType { + switch(node) { + case Root(children) | Node(_, children): + for(child in children) { + @as(child => Node(k, _), @if matcher(k, key)) { + return child; + } + } + default: + } + + return null; + } +} \ No newline at end of file diff --git a/haxe/utils/trie/Util.hx b/haxe/utils/trie/Util.hx new file mode 100644 index 000000000..e21bd1af7 --- /dev/null +++ b/haxe/utils/trie/Util.hx @@ -0,0 +1,45 @@ +package haxe.utils.trie; + +import haxe.utils.enumExtractor.EnumExtractor; +import haxe.utils.trie.NodeType; + +class NodeTypeUtil implements EnumExtractor { + public static function getTerminalValue(node:NodeType) : V { + switch(node) { + case null: + return null; + case Terminal(v): + return v; + case Node(_, children) | Root(children): + for(child in children) { + @as(child => Terminal(v)) { + return v; + } + } + return null; + } + } + + public static function updateTerminalValue(node:NodeType, value:V) { + switch(node) { + case Node(_, children) | Root(children): + var hasChanged:Bool = false; + for(i in 0...children.length) { + @as(children[i] => Terminal(_)) { + children[i] = Terminal(value); + hasChanged = true; + } + } + if(!hasChanged) { + children.push(Terminal(value)); + } + default: + } + } + + public static function appendChild(node:NodeType, child:NodeType) : Void { + @as(node => Node(_, children) | Root(children)) { + children.push(child); + } + } +} \ No newline at end of file