From bb2ec811cb8abdcbbf1a66d5e77fb3d2e8e397ae Mon Sep 17 00:00:00 2001 From: Manolo Carrasco Date: Tue, 1 Jun 2010 09:09:26 +0000 Subject: [PATCH] added a new XPath engine based in Andrea Giammarchi Css2Xpath library --- .../java/com/google/gwt/query/Query.gwt.xml | 4 +- .../client/impl/SelectorEngineCssToXPath.java | 188 ++++++++++++++++++ .../client/impl/SelectorEngineSizzle.java | 5 +- .../rebind/SelectorGeneratorCssToXPath.java | 86 ++++++++ .../gwt/query/client/GQuerySelectorsTest.java | 4 +- .../client/impl/SelectorEnginesTest.java | 55 +++++ .../query/rebind/SelectorGeneratorsTest.java | 63 ++++++ 7 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineCssToXPath.java create mode 100644 gwtquery-core/src/main/java/com/google/gwt/query/rebind/SelectorGeneratorCssToXPath.java create mode 100644 gwtquery-core/src/test/java/com/google/gwt/query/client/impl/SelectorEnginesTest.java create mode 100644 gwtquery-core/src/test/java/com/google/gwt/query/rebind/SelectorGeneratorsTest.java diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/Query.gwt.xml b/gwtquery-core/src/main/java/com/google/gwt/query/Query.gwt.xml index bd9a76c3..9d0e83ff 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/Query.gwt.xml +++ b/gwtquery-core/src/main/java/com/google/gwt/query/Query.gwt.xml @@ -33,7 +33,7 @@ - + @@ -68,7 +68,7 @@ class="com.google.gwt.query.client.impl.SelectorEngineImpl"/> - + diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineCssToXPath.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineCssToXPath.java new file mode 100644 index 00000000..3e3d482b --- /dev/null +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineCssToXPath.java @@ -0,0 +1,188 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.query.client.impl; + +import java.util.ArrayList; + +import com.google.gwt.core.client.JsArray; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.query.client.JSArray; +import com.google.gwt.query.client.Regexp; +import com.google.gwt.query.client.SelectorEngine; + +/** + * Runtime selector engine implementation which translates selectors to XPath + * and delegates to document.evaluate(). + * It is based on the regular expressions in Andrea Giammarchi's Css2Xpath + */ +public class SelectorEngineCssToXPath extends SelectorEngineImpl { + + /** + * Interface for callbacks in replaceAll operations. + */ + public static interface ReplaceCallback { + String foundMatch(ArrayList s); + } + + /** + * Interface for replacer implementations (GWT and JVM). + */ + public static interface Replacer { + String replaceAll(String s, String expr, Object replacement); + } + + private static SelectorEngineCssToXPath instance; + + private static ReplaceCallback rc_$Attr = new ReplaceCallback() { + public String foundMatch(ArrayList s) { + return "[substring(@" + s.get(1) + ",string-length(@" + s.get(1) + ")-" + (s.get(2).replaceAll("'", "").length() - 1) + ")=" + s.get(2) + "]"; + } + }; + + private static ReplaceCallback rc_Not = new ReplaceCallback() { + public String foundMatch(ArrayList s) { + return s.get(1) + "[not(" + getInstance().css2Xpath(s.get(2)).replaceAll("^[^\\[]+\\[([^\\]]*)\\].*$", "$1" + ")]"); + } + }; + + private static ReplaceCallback rc_nth_child = new ReplaceCallback() { + public String foundMatch(ArrayList s) { + if (s.get(1).length() == 0) { + s.set(1, "0"); + } + if ("n".equals(s.get(2))) { + return s.get(1); + } + if ("even".equals(s.get(2))) { + return "*[position() mod 2=0 and position()>=0]/self::" + s.get(1); + } + if ("odd".equals(s.get(2))) { + return s.get(1) + "[(count(preceding-sibling::*) + 1) mod 2=1]"; + } + String[] t = s.get(2).replaceAll("^([0-9]*)n.*?([0-9]*)?$", "$1+$2").split("\\+"); + String t0 = t[0]; + String t1 = t.length > 1 ? t[1] : "0"; + return "*[(position()-" + t1 + ") mod " + t0 + "=0 and position()>=" + t1 + "]/self::" + s.get(1); + } + }; + + private static Object[] regs = new Object[]{ + "\\[([^\\]~\\$\\*\\^\\|\\!]+)(=[^\\]]+)?\\]", "[@$1$2]", + // multiple queries + "\\s*,\\s*", "|", + // , + ~ > + "\\s*(\\+|~|>)\\s*", "$1", + //* ~ + > + "([a-zA-Z0-9_\\-\\*])~([a-zA-Z0-9_\\-\\*])", "$1/following-sibling::$2", + "([a-zA-Z0-9_\\-\\*])\\+([a-zA-Z0-9_\\-\\*])", "$1/following-sibling::*[1]/self::$2", + "([a-zA-Z0-9_\\-\\*])>([a-zA-Z0-9_\\-\\*])", "$1/$2", + // all unescaped stuff escaped + "\\[([^=]+)=([^'|\"][^\\]]*)\\]", "[$1='$2']", + // all descendant or self to + "(^|[^a-zA-Z0-9_\\-\\*])(#|\\.)([a-zA-Z0-9_\\-]+)", "$1*$2$3", + "([\\>\\+\\|\\~\\,\\s])([a-zA-Z\\*]+)", "$1//$2", + "\\s+//", "//", + // :first-child + "([a-zA-Z0-9_\\-\\*]+):first-child", "*[1]/self::$1", + // :first + "([a-zA-Z0-9_\\-\\*]+):first", "*[1]/self::$1", + // :last-child + "([a-zA-Z0-9_\\-\\*]+):last-child", "$1[not(following-sibling::*)]", + // :only-child + "([a-zA-Z0-9_\\-\\*]+):only-child", "*[last()=1]/self::$1", + // :empty + "([a-zA-Z0-9_\\-\\*]+):empty", "$1[not(*) and not(normalize-space())]", + "(.+):not\\(([^\\)]*)\\)", rc_Not, + "([a-zA-Z0-9\\_\\-\\*]+):nth-child\\(([^\\)]*)\\)", rc_nth_child, + // :contains(selectors) + ":contains\\(([^\\)]*)\\)", "[contains(string(.),'$1')]", + // |= attrib + "\\[([a-zA-Z0-9_\\-]+)\\|=([^\\]]+)\\]", "[@$1=$2 or starts-with(@$1,concat($2,'-'))]", + // *= attrib + "\\[([a-zA-Z0-9_\\-]+)\\*=([^\\]]+)\\]", "[contains(@$1,$2)]", + // ~= attrib + "\\[([a-zA-Z0-9_\\-]+)~=([^\\]]+)\\]", "[contains(concat(' ',normalize-space(@$1),' '),concat(' ',$2,' '))]", + // ^= attrib + "\\[([a-zA-Z0-9_\\-]+)\\^=([^\\]]+)\\]", "[starts-with(@$1,$2)]", + // $= attrib + "\\[([a-zA-Z0-9_\\-]+)\\$=([^\\]]+)\\]", rc_$Attr, + // != attrib + "\\[([a-zA-Z0-9_\\-]+)\\!=([^\\]]+)\\]", "[not(@$1) or @$1!=$2]", + // ids and classes + "#([a-zA-Z0-9_\\-]+)", "[@id='$1']", + "\\.([a-zA-Z0-9_\\-]+)", "[contains(concat(' ',normalize-space(@class),' '),' $1 ')]", + // normalize multiple filters + "\\]\\[([^\\]]+)", " and ($1)"}; + + public static SelectorEngineCssToXPath getInstance() { + if (instance == null) { + instance = new SelectorEngineCssToXPath(); + } + return instance; + } + + // This replacer only works in browser, it must be replaced + // when using this engine in generators and tests for the JVM + private Replacer replacer = new Replacer() { + public String replaceAll(String s, String r, Object o) { + Regexp p = new Regexp(r); + if (o instanceof ReplaceCallback) { + ReplaceCallback callback = (ReplaceCallback) o; + while (p.test(s)) { + JSArray a = p.match(s); + ArrayList args = new ArrayList(); + for (int i = 0; i < a.getLength(); i++) { + args.add(a.getStr(i)); + } + String f = callback.foundMatch(args); + s = s.replaceFirst(r, f); + } + return s; + } else { + return s.replaceAll(r, o.toString()); + } + } + }; + + public SelectorEngineCssToXPath() { + instance = this; + } + + public SelectorEngineCssToXPath(Replacer r) { + replacer = r; + instance = this; + } + + public String css2Xpath(String selector) { + String ret = selector; + for (int i = 0; i < regs.length;) { + ret = replacer.replaceAll(ret, regs[i++].toString(), regs[i++]); + } + return "//" + ret; + } + + public NodeList select(String sel, Node ctx) { + JSArray elm = JSArray.create(); + if (!sel.startsWith("//") && !sel.startsWith("./") && !sel.startsWith("/")) { + sel = css2Xpath(sel); + } + SelectorEngine.xpathEvaluate(sel, ctx, elm); + return unique(elm.> cast()).cast(); + } + +} diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineSizzle.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineSizzle.java index 57af8277..389b424a 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineSizzle.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/impl/SelectorEngineSizzle.java @@ -628,7 +628,7 @@ public class SelectorEngineSizzle extends SelectorEngineImpl { } return @com.google.gwt.query.client.impl.SelectorEngineSizzle::filter(Ljava/lang/String;Lcom/google/gwt/core/client/JsArray;ZLjava/lang/Object;)( later, tmpSet, false ); }-*/; - + private static native JsArray select(String selector, Node context, JsArray results, JsArray seed) /*-{ results = results || []; var origContext = context = context || document; @@ -718,7 +718,6 @@ public class SelectorEngineSizzle extends SelectorEngineImpl { } if ( extra ) { @com.google.gwt.query.client.impl.SelectorEngineSizzle::select(Ljava/lang/String;Lcom/google/gwt/dom/client/Node;Lcom/google/gwt/core/client/JsArray;Lcom/google/gwt/core/client/JsArray;)(extra, origContext, results, seed); - @com.google.gwt.query.client.impl.SelectorEngineSizzle::unique(Lcom/google/gwt/core/client/JsArray;)(results); } return results; }-*/; @@ -729,6 +728,6 @@ public class SelectorEngineSizzle extends SelectorEngineImpl { public NodeList select(String selector, Node context) { JsArray results = JavaScriptObject.createArray().cast(); - return select(selector, context, results, null).cast(); + return unique(select(selector, context, results, null)).cast(); } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/rebind/SelectorGeneratorCssToXPath.java b/gwtquery-core/src/main/java/com/google/gwt/query/rebind/SelectorGeneratorCssToXPath.java new file mode 100644 index 00000000..04582af6 --- /dev/null +++ b/gwtquery-core/src/main/java/com/google/gwt/query/rebind/SelectorGeneratorCssToXPath.java @@ -0,0 +1,86 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.query.rebind; + +import java.util.ArrayList; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.core.ext.UnableToCompleteException; +import com.google.gwt.core.ext.typeinfo.JMethod; +import com.google.gwt.query.client.Selector; +import com.google.gwt.query.client.impl.SelectorEngineCssToXPath; +import com.google.gwt.query.client.impl.SelectorEngineCssToXPath.ReplaceCallback; +import com.google.gwt.query.client.impl.SelectorEngineCssToXPath.Replacer; +import com.google.gwt.user.rebind.SourceWriter; + +/** + * Compile time selector generator which translates selector into XPath at + * compile time. It Uses the SelectorEngineCssToXpath to produce the xpath + * selectors + */ +public class SelectorGeneratorCssToXPath extends SelectorGeneratorBase { + + /** + * The replacer implementation for the JVM. + */ + public static final Replacer replacer = new Replacer() { + public String replaceAll(String s, String r, Object o) { + Pattern p = Pattern.compile(r); + if (o instanceof ReplaceCallback) { + final Matcher matcher = p.matcher(s); + ReplaceCallback callback = (ReplaceCallback) o; + while (matcher.find()) { + final MatchResult matchResult = matcher.toMatchResult(); + ArrayList argss = new ArrayList(); + for (int i = 0; i < matchResult.groupCount() + 1; i++) { + argss.add(matchResult.group(i)); + } + final String replacement = callback.foundMatch(argss); + s = s.substring(0, matchResult.start()) + replacement + s.substring(matchResult.end()); + matcher.reset(s); + } + return s; + } else { + return p.matcher(s).replaceAll(o.toString()); + } + } + }; + + private SelectorEngineCssToXPath engine = new SelectorEngineCssToXPath( + replacer); + + protected String css2Xpath(String s) { + return engine.css2Xpath(s); + } + + protected void generateMethodBody(SourceWriter sw, JMethod method, + TreeLogger treeLogger, boolean hasContext) + throws UnableToCompleteException { + + String selector = method.getAnnotation(Selector.class).value(); + sw.println("return " + + wrap(method, "SelectorEngine.xpathEvaluate(\"" + css2Xpath(selector) + + "\", root)") + ";"); + } + + protected String getImplSuffix() { + return "XPath" + super.getImplSuffix(); + } + +} diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/client/GQuerySelectorsTest.java b/gwtquery-core/src/test/java/com/google/gwt/query/client/GQuerySelectorsTest.java index 057679e5..7bbc438e 100644 --- a/gwtquery-core/src/test/java/com/google/gwt/query/client/GQuerySelectorsTest.java +++ b/gwtquery-core/src/test/java/com/google/gwt/query/client/GQuerySelectorsTest.java @@ -170,7 +170,7 @@ public class GQuerySelectorsTest extends GWTTestCase { // assertArrayContains(sel.title().getLength(), 1); assertEquals(1, sel.body().getLength()); - assertEquals(53, sel.bodyDiv().getLength()); + assertArrayContains(sel.bodyDiv().getLength(), 53, 55); sel.setRoot(e); assertArrayContains(sel.aHrefLangClass().getLength(), 0, 1); assertArrayContains(sel.allChecked().getLength(), 1); @@ -325,7 +325,7 @@ public class GQuerySelectorsTest extends GWTTestCase { $(e).html(getTestContent()); assertArrayContains(selEng.select("body", Document.get()).getLength(), 1); - assertArrayContains(selEng.select("body div", Document.get()).getLength(), 53); + assertArrayContains(selEng.select("body div", Document.get()).getLength(), 53, 55); assertArrayContains(selEng.select("h1[id]:contains(Selectors)", e).getLength(), 1); assertArrayContains(selEng.select("*:first", e).getLength(), 1, 0); diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/client/impl/SelectorEnginesTest.java b/gwtquery-core/src/test/java/com/google/gwt/query/client/impl/SelectorEnginesTest.java new file mode 100644 index 00000000..43d15416 --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/query/client/impl/SelectorEnginesTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.query.client.impl; + +import com.google.gwt.junit.client.GWTTestCase; + +/** + * Test for selector engine implementations + */ +public class SelectorEnginesTest extends GWTTestCase { + + public String getModuleName() { + return "com.google.gwt.query.Query"; + } + + public void testCssToXpath() { + SelectorEngineCssToXPath sel = new SelectorEngineCssToXPath(); + + assertEquals("//div[starts-with(@class,'exa') and (substring(@class,string-length(@class)-3)='mple')]", + sel.css2Xpath("div[class^=exa][class$=mple]")); + assertEquals("//div[not(contains(concat(' ',normalize-space(@class),' '),' example '))]", + sel.css2Xpath("div:not(.example)")); + + assertEquals("//p", + sel.css2Xpath("p:nth-child(n)")); + assertEquals("//p[(count(preceding-sibling::*) + 1) mod 2=1]", + sel.css2Xpath("p:nth-child(odd)")); + assertEquals("//*[(position()-0) mod 2=0 and position()>=0]/self::p", + sel.css2Xpath("p:nth-child(2n)")); + + assertEquals("//div[substring(@class,string-length(@class)-3)='mple']", + sel.css2Xpath("div[class$=mple]")); + assertEquals("//div[substring(@class,string-length(@class)-5)='xample']", + sel.css2Xpath("div[class$=xample]")); + + assertEquals("//div[not(contains(concat(' ',normalize-space(@class),' '),' example '))]", + sel.css2Xpath("div:not(.example)")); + + } + + +} diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/rebind/SelectorGeneratorsTest.java b/gwtquery-core/src/test/java/com/google/gwt/query/rebind/SelectorGeneratorsTest.java new file mode 100644 index 00000000..4f605797 --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/query/rebind/SelectorGeneratorsTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.query.rebind; + +import java.util.ArrayList; + +import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.query.client.impl.SelectorEngineCssToXPath.ReplaceCallback; + +/** + * Test class for selector generators. + */ +public class SelectorGeneratorsTest extends GWTTestCase { + + public String getModuleName() { + return null; + } + + public void testCss2Xpath() { + SelectorGeneratorCssToXPath sel = new SelectorGeneratorCssToXPath(); + + assertEquals("//div[starts-with(@class,'exa') and (substring(@class,string-length(@class)-3)='mple')]", + sel.css2Xpath("div[class^=exa][class$=mple]")); + assertEquals("//div[not(contains(concat(' ',normalize-space(@class),' '),' example '))]", + sel.css2Xpath("div:not(.example)")); + + assertEquals("//p", + sel.css2Xpath("p:nth-child(n)")); + assertEquals("//p[(count(preceding-sibling::*) + 1) mod 2=1]", + sel.css2Xpath("p:nth-child(odd)")); + assertEquals("//*[(position()-0) mod 2=0 and position()>=0]/self::p", + sel.css2Xpath("p:nth-child(2n)")); + + assertEquals("//div[substring(@class,string-length(@class)-3)='mple']", + sel.css2Xpath("div[class$=mple]")); + assertEquals("//div[substring(@class,string-length(@class)-5)='xample']", + sel.css2Xpath("div[class$=xample]")); + + assertEquals("//div[not(contains(concat(' ',normalize-space(@class),' '),' example '))]", + sel.css2Xpath("div:not(.example)")); + } + + public void testReplaceAll() { + assertEquals(" ", SelectorGeneratorCssToXPath.replacer.replaceAll("/[thumb01]/ /[thumb03]/", "/\\[thumb(\\d+)\\]/", new ReplaceCallback() { + public String foundMatch(ArrayLists) { + return ""; + } + })); + } +} -- 2.39.5