summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2017-01-18 20:07:16 +0100
committeremkael <emkael@tlen.pl>2017-01-18 20:07:16 +0100
commit9a9c04512e5dcb77c7fe5d850e3f2a0250cc160e (patch)
treefed46b5f4c2ed3a050bb1a7ad7c6d0a3ea844d55
parentc5bcf8f74fb80b7e163663845b0d6e35cabface3 (diff)
* Motor Sport Magazine feed provider
-rw-r--r--lib/querypath/.gitignore14
-rw-r--r--lib/querypath/API61
-rw-r--r--lib/querypath/COPYING-MIT.txt22
-rwxr-xr-xlib/querypath/CREDITS14
-rw-r--r--lib/querypath/INSTALL80
-rw-r--r--lib/querypath/Makefile30
-rw-r--r--lib/querypath/README.md142
-rw-r--r--lib/querypath/RELEASE168
-rw-r--r--lib/querypath/build.xml590
-rw-r--r--lib/querypath/composer.json16
-rw-r--r--lib/querypath/config.doxy1631
-rw-r--r--lib/querypath/package.xml546
-rw-r--r--lib/querypath/package_compatible.xml56
-rw-r--r--lib/querypath/pear-summary.txt5
-rw-r--r--lib/querypath/quickstart-guide.md113
-rw-r--r--lib/querypath/src/QueryPath.php356
-rw-r--r--lib/querypath/src/QueryPath/CSS/DOMTraverser.php775
-rw-r--r--lib/querypath/src/QueryPath/CSS/DOMTraverser/PseudoClass.php421
-rw-r--r--lib/querypath/src/QueryPath/CSS/DOMTraverser/Util.php139
-rw-r--r--lib/querypath/src/QueryPath/CSS/EventHandler.php171
-rw-r--r--lib/querypath/src/QueryPath/CSS/InputStream.php57
-rw-r--r--lib/querypath/src/QueryPath/CSS/NotImplementedException.php15
-rw-r--r--lib/querypath/src/QueryPath/CSS/ParseException.php15
-rw-r--r--lib/querypath/src/QueryPath/CSS/Parser.php575
-rw-r--r--lib/querypath/src/QueryPath/CSS/QueryPathEventHandler.php1424
-rw-r--r--lib/querypath/src/QueryPath/CSS/Scanner.php306
-rw-r--r--lib/querypath/src/QueryPath/CSS/Selector.php144
-rw-r--r--lib/querypath/src/QueryPath/CSS/SimpleSelector.php138
-rw-r--r--lib/querypath/src/QueryPath/CSS/Token.php60
-rw-r--r--lib/querypath/src/QueryPath/CSS/Traverser.php38
-rw-r--r--lib/querypath/src/QueryPath/DOMQuery.php3993
-rw-r--r--lib/querypath/src/QueryPath/Entities.php162
-rw-r--r--lib/querypath/src/QueryPath/Exception.php13
-rw-r--r--lib/querypath/src/QueryPath/Extension.php94
-rw-r--r--lib/querypath/src/QueryPath/Extension/QPXML.php211
-rw-r--r--lib/querypath/src/QueryPath/Extension/QPXSL.php76
-rw-r--r--lib/querypath/src/QueryPath/ExtensionRegistry.php130
-rw-r--r--lib/querypath/src/QueryPath/IOException.php20
-rw-r--r--lib/querypath/src/QueryPath/Options.php84
-rw-r--r--lib/querypath/src/QueryPath/ParseException.php44
-rw-r--r--lib/querypath/src/QueryPath/Query.php12
-rw-r--r--lib/querypath/src/QueryPath/QueryPathIterator.php33
-rw-r--r--lib/querypath/src/documentation.php261
-rw-r--r--lib/querypath/src/qp.php82
-rw-r--r--lib/querypath/src/qp_functions.php216
-rw-r--r--lib/querypath/test/DOMTraverserTest.xml35
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/DOMTraverserTest.php357
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/ParserTest.php520
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/PseudoClassTest.php828
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/QueryPathEventHandlerTest.php1439
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/SelectorTest.php120
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/TokenTest.php23
-rw-r--r--lib/querypath/test/Tests/QueryPath/CSS/UtilTest.php51
-rw-r--r--lib/querypath/test/Tests/QueryPath/DOMQueryTest.php1865
-rw-r--r--lib/querypath/test/Tests/QueryPath/EntitiesTest.php54
-rw-r--r--lib/querypath/test/Tests/QueryPath/ExtensionTest.php153
-rw-r--r--lib/querypath/test/Tests/QueryPath/Extensions/QPXMLTest.php41
-rw-r--r--lib/querypath/test/Tests/QueryPath/Extensions/QPXSLTest.php60
-rw-r--r--lib/querypath/test/Tests/QueryPath/OptionsTest.php60
-rw-r--r--lib/querypath/test/Tests/QueryPath/QueryPathTest.php56
-rw-r--r--lib/querypath/test/Tests/QueryPath/TestCase.php22
-rw-r--r--lib/querypath/test/Tests/QueryPath/XMLIshTest.php63
-rw-r--r--lib/querypath/test/advanced.xml14
-rw-r--r--lib/querypath/test/amplify.xml4370
-rw-r--r--lib/querypath/test/coverage.sh6
-rw-r--r--lib/querypath/test/data.html15
-rw-r--r--lib/querypath/test/data.xml15
-rw-r--r--lib/querypath/test/html.xml15
-rw-r--r--lib/querypath/test/no-writing.xml2
-rwxr-xr-xlib/querypath/test/runTests.sh8
-rwxr-xr-xlib/querypath/test/test.php153
-rw-r--r--providers/Motorsport.php116
72 files changed, 24014 insertions, 0 deletions
diff --git a/lib/querypath/.gitignore b/lib/querypath/.gitignore
new file mode 100644
index 0000000..02a5796
--- /dev/null
+++ b/lib/querypath/.gitignore
@@ -0,0 +1,14 @@
+bin/build
+bin/local
+bin/*.tgz
+bin/*.zip
+*.tmproj
+releases
+dist
+test/coverage
+test/reports
+test/db
+docs/*
+doc/*
+bin/querypath-200x333.png
+test/fakepear
diff --git a/lib/querypath/API b/lib/querypath/API
new file mode 100644
index 0000000..31c9a93
--- /dev/null
+++ b/lib/querypath/API
@@ -0,0 +1,61 @@
+API Changes in 3.0.0
+
+This is a major update. See the main changes for details.
+
+API Changes in 2.1.1
+
+ * NEW METHOD: document() returns the DOMDocument
+ * BUG FIX: Issue #10 has been re-fixed to correctly collapse certain empty tags.
+ * BUG FIX: Issue #10 has been re-fixed to correctly escape JavaScript for browsers.
+ * BUG FIX: Issue #47 has been fixed to only remove XML declaration, but leave DOCTYPE.
+ * NEW ARGUMENT: xpath() now supports $options, which includes the ability to set a namespace.
+
+API Changes in 2.1
+
+All changes are documented in their respective doc blocks. So take a
+look at http://api.querypath.org to learn more.
+
+New Functions
+ * The `htmlqp()` method has been added for parsing icky HTML. Use
+ this for web scraping.
+
+Altered Functions
+ * The qp() function now supports the following new options:
+ - convert_to_encoding
+ - convert_from_encoding
+ - strip_low_ascii
+ - use_parser
+
+New Methods
+ * attach()/detach()
+ * has()
+ * emptyElement()
+ * even()/odd()
+ * first()/last()
+ * firstChild()/lastChild()
+ * nextUntil()/prevUntil()
+ * parentsUntil()
+ * encodeDataURL()
+ * dataURL()
+ * filterPreg()
+ * textBefore()/textAfter()
+
+Altered Methods
+ * css() has been changed to allow subsequent calls
+ to modify the style attribute (issue #28)
+ * attr() has been changed. If it is called with no
+ arguments, it now returns all attributes.
+
+New CSS Selectors Behavior
+
+ * :contains-exactly() performs as :contains() used to perform.
+
+Altered CSS Selectors Behavior
+
+ * The star operator (*) is now non-greedy, per spec. Before, the star would match
+ any descendants. Now it will only match children.
+ * :contains() now does substring matching instead of exact matching. This conforms
+ to jQuery's behavior.
+ * Quotes are now checked carefully before being stripped from pseudo-class values.
+ * Issue #40 identified a potential infinite looping problem on poorly formed selectors.
+ This was fixed.
diff --git a/lib/querypath/COPYING-MIT.txt b/lib/querypath/COPYING-MIT.txt
new file mode 100644
index 0000000..3e26983
--- /dev/null
+++ b/lib/querypath/COPYING-MIT.txt
@@ -0,0 +1,22 @@
+QueryPath: Find your way
+Matt Butcher <mattbutcher@google.com>
+Copyright (C) 2009-2014 Matt Butcher
+Copyright (C) 2015 Google
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/lib/querypath/CREDITS b/lib/querypath/CREDITS
new file mode 100755
index 0000000..1c4a940
--- /dev/null
+++ b/lib/querypath/CREDITS
@@ -0,0 +1,14 @@
+Matt Butcher [technosophos] <matt@aleph-null.tv> (lead)
+Emily Brand [eabrand] <emily@example.com> (developer)
+Woody Gilk [shadowhand] <woody@wingsc.com> (contributor)
+Xandar Guzman [theshadow] <theshadow@shadowpedia.info> (contributor)
+Bobby Jack [fiveminuteargument] <editor@fiveminuteargument.com> (contributor)
+Steven Lischer [TomorrowToday] <xcpostman06@gmail.com> (contributor)
+GDMac [GDMac] <gdmac@example.com> (contributor)
+Bill Ortell [billortell] <allthingsweb20@gmail.com> (contributor)
+hakre [hakre] <http://hakre.wordpress.com/credits/> (contributor)
+katzwebservices [katzwebservices] <katzwebservices@example.com> (contributor)
+Markus Kalkbrenner [mkalkbrenner] <mkalkbrenner@users.sourceforge.net> (contributor)
+Akihiro Yamanoi [noisan] <akihiro.yamanoi@gmail.com> (contributor)
+Sandeep Shetty [sandeepshetty] <sandeep.shetty@gmail.com> (contributor)
+Zemistr [Zemistr] <Zemistr@example.com> (contributor)
diff --git a/lib/querypath/INSTALL b/lib/querypath/INSTALL
new file mode 100644
index 0000000..e6f4555
--- /dev/null
+++ b/lib/querypath/INSTALL
@@ -0,0 +1,80 @@
+INSTALLING QUERYPATH
+==============================
+
+There are two distributions of QueryPath:
+
+1. The full version, which includes source, documentation, unit tests,
+and examples.
+
+2. The minimal version, which includes only the source code in
+compacted form.
+
+INSTALLING THE MINIMAL VERSION
+==============================
+To install QueryPath-minimal:
+- Extract the contents of the archive:
+
+ tar -zxvf QueryPath-2.0-minimal.tgz
+
+- Move the extracted directory to the desired location. Generally,
+ it is best to rename this folder 'QueryPath'.
+
+ mv QueryPath-2.0-minimal myproject/QueryPath
+
+ (Alternately, you may wish to install QueryPath in a location available on
+ you PHP library path for use in all applications on your system.)
+
+- Include QueryPath in your scripts:
+
+ require 'QueryPath/QueryPath.php';
+
+- The minimal package does not contain any documentation. To view
+ the documentation online, go to http://api.querypath.org.
+
+INSTALLING THE FULL VERSION
+==============================
+To install QueryPath:
+
+- Extract the contents of the archive.
+
+ tar -zxvf QueryPath-2.0.tgz
+
+- Copy the src/QueryPath directory to the desired location.
+
+ cp QueryPath-2.0/src/QueryPath myproject
+
+- Include QueryPath/QueryPath.php in your PHP scripts
+
+ require 'QueryPath/QueryPath.php';
+
+The structure of the full version:
+
+/ -- Base information about the library
+ src/ -- The source (PHP) code. Include the contents of this directory in your
+ applications.
+ test/ -- Unit tests for QueryPath. You can run these with PHPUnit.
+ examples/ -- Examples scripts written with QueryPath.
+ docs/ -- Full API documentation in HTML format. You can regenerate this from
+ the source using PHPDocumentor.
+ tutorials/ -- DocBook additions to the QueryPath PHPDocumentor data. An HTML
+ version of this is included in the docs/ directory. The files here are
+ probably only useful when re-generating the API documentation.
+
+
+INSTALLING FROM GitHub
+==============================
+
+The complete source code and all utilities for QueryPath are located in a GitHub
+repository. If you plan on contributing back to QueryPath, you will be able to
+achieve the most by working from the GitHub source. Here's how:
+
+1. Obtain the source code from here: http://github.com/technosophos/querypath
+
+2. Install PHPDocumentor, PHPUnit, and Phing. XDebug is strongly recommended for
+ doing coverage analysis. These tools are used to auto-generate
+ documentation, run unit tests, and manage project builds.
+
+3. Run 'phing info' from the base QueryPath checkout. This will tell you how to
+ perform various tasks with QueryPath.
+
+For more information, visit http://querypath.org. \ No newline at end of file
diff --git a/lib/querypath/Makefile b/lib/querypath/Makefile
new file mode 100644
index 0000000..7267813
--- /dev/null
+++ b/lib/querypath/Makefile
@@ -0,0 +1,30 @@
+PROJ := 'QueryPath'
+SRCDIR := src
+TESTS := test/Tests
+VERSION := 'DEV'
+DATE := `date "+%Y%m%d"`
+
+VFILES = src/HPCloud
+
+docs :
+ @cat ./config.doxy | sed 's/-UNSTABLE%/$(VERSION)/' | doxygen -
+
+test :
+ phpunit --verbose --color --exclude-group=deprecated $(TESTS);
+
+test-group :
+ phpunit --verbose --color --group=$(GROUP) $(TESTS);
+
+fulltest:
+ phpunit --color $(TESTS)
+
+lint : $(SRCDIR)
+ find $(SRCDIR) -iname *.php -exec php -l {} ';'
+
+dist: tar
+
+tar:
+ @echo $(PROJ)-$(VERSION)-$(DATE).tgz
+ # @tar -zcvf $(PROJ)-$(VERSION)-$(DATE).tgz $(SRCDIR)
+
+.PHONY: docs test dist tar lint
diff --git a/lib/querypath/README.md b/lib/querypath/README.md
new file mode 100644
index 0000000..fa4e885
--- /dev/null
+++ b/lib/querypath/README.md
@@ -0,0 +1,142 @@
+# QueryPath: Find your way.
+
+Authors: Matt Butcher (lead), Emily Brand, and many others
+
+[Website](http://querypath.org) |
+[API Docs](http://api.querypath.org/docs) |
+[VCS and Issue Tracking](http://github.com/technosophos/querypath) |
+[Support List](http://groups.google.com/group/support-querypath) |
+[Developer List](http://groups.google.com/group/devel-querypath) |
+[Pear channel](http://pear.querypath.org) |
+
+This package is licensed under an MIT license (COPYING-MIT.txt).
+
+## At A Glance
+
+QueryPath is a jQuery-like library for working with XML and HTML
+documents in PHP. It now contains support for HTML5 via the
+[HTML5-PHP project](https://github.com/Masterminds/html5-php).
+
+### Gettings Started
+
+Assuming you have successfully installed QueryPath via Composer, you can
+parse documents like this:
+
+```
+require_once "vendor/autoload.php";
+
+// HTML5 (new)
+$qp = html5qp("path/to/file.html");
+
+// Legacy HTML via libxml
+$qp = htmlqp("path/to/file.html");
+
+// XML or XHTML
+$qp = qp("path/to/file.html");
+
+// All of the above can take string markup instead of a file name:
+$qp = qp("<?xml version='1.0'?><hello><world/></hello>")
+
+```
+
+But the real power comes from chaining. Check out the example below.
+
+### Example Usage
+
+Say we have a document like this:
+```xml
+<?xml version="1.0"?>
+<table>
+ <tr id="row1">
+ <td>one</td><td>two</td><td>three</td>
+ </tr>
+ <tr id="row2">
+ <td>four</td><td>five</td><td>six</td>
+ </tr>
+</table>
+```
+
+And say that the above is stored in the variable `$xml`. Now
+we can use QueryPath like this:
+
+```php
+<?php
+// Add the attribute "foo=bar" to every "td" element.
+qp($xml, 'td')->attr('foo', 'bar');
+
+// Print the contents of the third TD in the second row:
+print qp($xml, '#row2>td:nth(3)')->text();
+
+// Append another row to the XML and then write the
+// result to standard output:
+qp($xml, 'tr:last')->after('<tr><td/><td/><td/></tr>')->writeXML();
+
+?>
+```
+
+(This example is in `examples/at-a-glance.php`.)
+
+With over 60 functions and robust support for chaining, you can
+accomplish sophisticated XML and HTML processing using QueryPath.
+
+## QueryPath Installers
+
+The preferred method of installing QueryPath is via [Composer](http://getcomposer.org).
+
+You can also download the package from GitHub.
+
+### Composer (Preferred)
+
+To add QueryPath as a library in your project, add this to the 'require'
+section of your `composer.json`:
+
+```json
+{
+ "require": {
+ "querypath/QueryPath": ">=3.0.0"
+ }
+}
+```
+
+The run `php composer.phar install` in that directory.
+
+To stay up to date on stable code, you can use `dev-master` instead of `>=3.0.0`.
+
+### Manual Install
+
+You can either download a stable release from the
+[GitHub Tags page](https://github.com/technosophos/querypath/tags)
+or you can use `git` to clone
+[this repository](http://github.com/technosophos/querypath) and work from
+the code.
+
+## Including QueryPath
+
+As of QueryPath 3.x, QueryPath uses the Composer autoloader if you
+installed with composer:
+```php
+<?php
+require 'vendor/autoload.php';
+?>
+```
+
+Without Composer, you can include QueryPath like this:
+
+```php
+<?php
+require 'QueryPath/src/qp.php';
+?>
+```
+
+QueryPath can also be compiled into a Phar and then included like this:
+
+```php
+<?php
+require 'QueryPath.phar';
+?>
+```
+
+From there, the main functions you will want to use are `qp()`
+(alias of `QueryPath::with()`) and `htmlqp()` (alias of
+`QueryPath::withHTML()`). Start with the
+[API docs](http://api.querypath.org/docs).
diff --git a/lib/querypath/RELEASE b/lib/querypath/RELEASE
new file mode 100644
index 0000000..68a9d38
--- /dev/null
+++ b/lib/querypath/RELEASE
@@ -0,0 +1,168 @@
+# RELEASE NOTES
+
+3.0.5 (unstable)
+
+
+3.0.4
+- Addition of namespace fetching method ns().
+- Various fixes
+- Basic support for HTML5 via Masterminds\HTML5
+- Fixed #164
+
+3.0.3
+
+- Bug #141: Fixed :gt and :lt pseudoclasses (thanks to lsv)
+- Bug #124: Fixed inheritance issue with late static binding (via noisan)
+- Bug #126: text(string) was not updating all matched elements (via noisan)
+- Bug #127: wrapInner() was mangling the HTML if matched element set was greater than 1 (via noisan)
+- Bug #128: Improved jQuery compatibility with wrap*() *pend(), before(), after(), etc. (via noisan)
+
+3.0.2
+
+- Bug #112: children() was not correctly restricting filters.
+- Bug #108: QueryPath\Query interface was too restrictive. (via mkalkbrenner)
+- Feature #106: :contains() is now case-insensitive. (via katzwebservices)
+
+3.0.1
+
+- Issue #100: qp.php divided into qp.php and qp_functions.php. Composer now includes qp(). (via hakre)
+- Issue #102: Added QueryPath.VERSION_MAJOR, changed QueryPath.VERSION
+- Issue #97: Rewrite children() and filter() to be a little more efficient.
+
+3.0.0
+
+** REALLY IMPORTANT CHANGE:** To match jQuery, and to correctly implement
+ a bottom-up parser, the following will no longer work:
+ $qp($html, 'li')->find(':root ul');
+ You can no longer use find() to ever, ever move UP the document true
+ (like back to the root from somewhere in the tree). You MUST use
+ top() to do this now. This is how jQuery works, and making this
+ minor change makes things much faster.
+
+** REALLY IMPORTANT CHANGE:** Many "destructive" operations now return a
+ new QueryPath object. This mirrors jQuery's behavior.
+
+ THAT MEANS: find() now works like jQuery find(), branch() is
+ deprecated. findInPlace() does what find() used to do.
+
+1. removeAll() now works as it does in recent jQuery. Thanks to GDMac
+ (issues #77 #78) for the fix.
+2. Refactored to use namespaces.
+3. Refactored to be SPR-0 compliant.
+4. Now uses Composer.
+5. The traversal mechanism is now bottom-up, which means Querypath is
+ faster.
+6. Issue #83: Fixed broken forloop in techniques.php. Thanks to BillOrtell for
+ the fix.
+7. ID-based searches no longer guarantee that only one element will be returned.
+ This accomodates XML documents that may use 'id' in a way different than
+ HTML.
+8. The base CSS Traverser is now optimized for selectors that use ID or
+ class, but no element.
+9. Pseudo-element behavior has been rewritten to better conform to the
+ standard. Using a pseudo-element no longer changes the match. Rather,
+ it checks to see if the condition is met by the present element, and
+ returns TRUE if it does. This means we do not need special case logic
+ to deal with text fragments.
+10. :x-root, :x-reset, and :scope are now implemented using the
+ same algorithm (they are, in other words, aliases). Since
+ :scope is part of CSS 4, you should use that.
+11. Support for the following CSS 4 Selectors featues has been added:
+ - :matches() pseudoclass, which behaves the way :has() behaves.
+ - :any-link
+ - :local-link (with some restrictions, as it does not know what the real
+ URL of the document is).
+ - :scope (see above)
+12. Traversing UP the DOM tree with find() is no longer allowed. Use top().
+13. :first is an alias of :nth(1) instead of :first-of-type. This follows
+ jQuery now.
+14. eachLambda() is now deprecated. It WILL be removed in the future.
+15. **Extensions:** QPList, QPTPL, and QPDB have all been moved to the new
+ project QueryPath-Ext.
+
+2.1.3
+
+1. QueryPath Templates have gotten an overhaul from TomorrowToday (Issue #59).
+ Templates now support attributes.
+
+2.1.2:
+
+1. Fixed the case where remove() caused an error when no items were found
+ to remove (issue #63). Thanks marktheunissen for the bug report and fix.
+
+2. New XML extensions to deal with adding namespaced elements (#64). Thanks to
+ theshadow for contributing an entire extension, and to farinspace for
+ detailed experiments with QP and XML namespaces.
+
+3. The adjacent CSS selector has been modified to ignore text elements. This
+ seems to be inline with the spec, but I am not 100% sure. Thanks to
+ fiveminuteargument for the patch.
+
+2.1.1:
+
+1. The xhtml() and writeXHTML() methods now correctly escape JS/CSS and also correctly
+ fold some tags into unaries will keeping other empty tags. See issues #10, #47.
+ Thanks to Alex Lawrence for his input.
+
+2. The method document() has been added. Thanks to Alex Lawrence for suggesting this
+ addition.
+
+3. The fetch_rss.php example created broken HREFs in some cases. Thanks to yaph for
+ the patch.
+
+4. The xpath() method now supports setting default namespaces. Thanks to Xavier Prud'homme
+ for a patch.
+
+5. The remove() method was fixed (issue #55) to now correctly return a QueryPath with
+ just the removed nodes, while not altering the base QueryPath object. Thanks to MarcusX
+ for finding and reporting the problem.
+
+6. Added childrenText() convenience method. (Safe -- no changes to existing functions.)
+ Thanks to Xavatar for suggestion and proofing initial code.
+
+7. Fixed bad character stripping in htmlqp() (Issue #58, #52) so that meaningful whitespace
+ is no longer stripped prior to parsing. Thanks to NortherRaven for detailed report
+ and help testing and debugging.
+
+8. Fixed broken :nth-of-type pseudo-class (Issue #57). Thanks to NorthernRaven for the
+ detailed report and help debugging.
+
+9. Fixed broken an+b rule handling in the special case '-n+b'. Thanks to NorthernRaven for
+ reporting and helping out.
+
+10. Xinclude support has been added via the xinclude() method on QueryPath. Thanks to Crell
+ for the suggestion and to sdboyer for help (Issue #50).
+
+11. QueryPath now implements Countable, which means you can do `count(qp($xml, 'div'))`. The
+ size() function has been marked deprecated.
+
+12. is() now takes both DOMNodes and Traversables (including QueryPath) as an argument. See
+ issue #53.
+
+13. The dirty_html.php example (contributed by Emily Brand, thanks!) is now fixed. Thanks to
+ MartyIX for tracking down the issue (#59).
+
+2.1.0:
+Big Changes:
+
+1. There is now an `htmlqp()` function that parses crufty HTML in a far
+more reliable way than `qp()`. Use this instead of any variant of
+the older `@qp()` setup.
+
+2. The API has been brought into alignment with jQuery 1.4. See
+API-2.1.0 for details.
+
+3. This release was driven substantially by eabrand's GSOC 2010
+contributions. Thanks, Emily!
+
+4. There are now Phar and PEAR packages available. Got to
+http://pear.querypath.org for PEAR packages.
+
+5. The minimal QP distribution is no longer minified, as it reportedly
+causes XDebug to crash.
+
+7. Data URs are now supported. QueryPath can now embed images directly
+into HTML and XML this way.
+
+8. Documentation is now in Doxygen instead of PhpDocumentor. Thanks
+to Matt Farina and Kevin O'Brien for their input.
diff --git a/lib/querypath/build.xml b/lib/querypath/build.xml
new file mode 100644
index 0000000..061fe19
--- /dev/null
+++ b/lib/querypath/build.xml
@@ -0,0 +1,590 @@
+<?xml version="1.0"?>
+<project
+ name="QueryPath"
+ description="A simple, powerful, robust tool for working with XML, HTML, and HTTP"
+ default="ftest">
+<!--
+This is the Phing build file for QueryPath.
+
+Phing is a build tool. Learn more about it at http://phing.info.
+
+Copyright (c) 2009, Matt Butcher, some rights reserved.
+
+License:
+QueryPath is released under the GNU LGPL 2.1 or, at your preference,
+an MIT license. See COPYING-LGPL.txt and COPYING-MIT.txt for details.
+-->
+ <target name="info" description="Information on using the QueryPath Phing build.">
+ <echo>
+To build QueryPath, run:
+
+ phing build
+
+This will create a generic dev release of QueryPath and copy the releasable files to the dist/ directory. All documentation will be generated, and both a minimal and full version of the code will be generated. The non-compressed files will be available for inspection in bin/build/.
+
+IMPORTANT: The full version of the package does not include build scripts (including this file). The only way to obtain the entire QueryPath development environment is to check it out from http://github.com/technosophos/querypath.
+
+A numbered release can be built with:
+
+ phing build -Dversion=2.1-Alpha1
+
+These are the basic tasks we anticipate performing with phing. However, the build script supports the running of several other tasks which may help you when debugging or developing QueryPath. Important ones are listed below. A complete list can be obtained by running 'phing -l' in this directory.
+
+To generate docs, do:
+
+ phing doc
+
+Documentation will be stored in docs/. You can start with docs/index.html.
+
+To run unit tests, do:
+
+ phing test
+
+The above will generate HTML test results which will be placed in test/reports/. If you wish to run the test and print the results directly the the command line, you should run 'phing ftest' instead.
+
+To run coverage analysis, do:
+
+ phing coverage
+
+This will create HTML pages describing code coverage. The coverage analysis will be available in test/coverage
+
+To print this message, do:
+
+ phing info
+
+ </echo>
+ </target>
+
+ <!-- This is used for Pirum tasks. -->
+ <includepath classpath="../Phing-Pirum/src"/>
+ <taskdef classname="PhingPirum.Task.PirumBuildTask" name="pirumbuild"/>
+ <taskdef classname="PhingPirum.Task.PirumAddTask" name="pirumadd"/>
+
+ <!-- Doxygen -->
+ <includepath classpath="../PhingDoxygen/src"/>
+ <taskdef classname="PhingDoxygen.Task.DoxygenTask" name="doxygen"/>
+
+ <!-- Pyrus tasks. -->
+ <includepath classpath="/Users/mattbutcher/Code"/> <!-- Pyrus is here. -->
+ <includepath classpath="../PhingPyrus/src"/>
+ <taskdef classname="PhingPyrus.Task.PyrusMakeTask" name="pyrusmake"/>
+ <taskdef classname="PhingPyrus.Task.PyrusPackageTask" name="pyruspackage"/>
+ <taskdef classname="PhingPyrus.Task.PyrusHelpTask" name="pyrushelp"/>
+ <taskdef classname="PhingPyrus.Task.PyrusExecTask" name="pyrusexec"/>
+
+ <!--
+ <includepath classpath="../Phing-QueryPath/src"/>
+ <taskdef classname="PhingQueryPath.Task.QueryPathReplacementTask" name="qpreplace"/>
+ -->
+
+ <!-- Global properties -->
+ <property name="srcdir" value="./src"/>
+ <property name="testdir" value="./test"/>
+ <property name="builddir" value="./bin/build"/>
+ <property name="docsdir" value="./doc"/>
+ <property name="packagedir" value="./dist"/>
+ <property name="phardir" value="./phar"/>
+ <property name="pirum.channel.path" value="../pear.querypath.org/pear"/>
+
+ <!-- ported from PhingPyrus -->
+ <property name="pear.channel" value="pear.querypath.org"/>
+ <property name="projectname" value="QueryPath"/>
+
+ <!-- Too hard to link to, nav is very difficult. -->
+ <!-- property name="phpdoc.style" value="HTML:frames:phpedit"/ -->
+
+ <property name="phpdoc.style" value="HTML:frames:earthli"/>
+
+
+ <!-- Files that must be included in the release -->
+ <fileset id="licensefiles" dir=".">
+ <include name="README"/>
+ <include name="INSTALL"/>
+ <include name="LGPL-2.1.txt"/>
+ <include name="COPYING-LGPL.txt"/>
+ <include name="COPYING-MIT.txt"/>
+ <include name="RELEASE"/>
+ <include name="API"/>
+ <include name="CREDITS"/>
+ </fileset>
+
+ <!-- Files to be treated as source code -->
+ <fileset id="sourcecode" dir="${srcdir}">
+ <include name="**/*.php" />
+ </fileset>
+
+ <!-- Unit tests and auxilliary files -->
+ <fileset id="unittests" dir="${testdir}/Tests">
+ <include name="**/*Test.php" />
+ </fileset>
+
+ <!-- Examples -->
+ <fileset id="examplecode" dir="./examples">
+ <include name="**/*" />
+ </fileset>
+
+ <!-- DocBook Tutorials -->
+ <fileset id="tutorials" dir="./tutorials">
+ <include name="**/*" />
+ </fileset>
+
+ <!-- Documentation -->
+ <fileset id="docs" dir="${docsdir}">
+ <include name="**/*" />
+ </fileset>
+
+ <!-- BUILD: The primary build target. -->
+ <target name="build" depends="lint, setup, prebuild, docBuild, minimalBuild, fullBuild, pharBuild, pearPkg"
+ description="Generate docs, minimal, and full builds and then creates packages."
+ ></target>
+
+ <!-- Target that should be run always. -->
+ <target name="setup" description="Run required configuration for any build.">
+ <tstamp/>
+ <!-- Default version -->
+ <property name="version" value="dev-${DSTAMP}"/>
+ </target>
+
+ <!-- tasks to do before any build. -->
+ <target name="prebuild">
+ <mkdir dir="./dist"/>
+ </target>
+
+ <!-- Build just the minimal package -->
+ <target name="minimalBuild" description="Minimal QueryPath build."
+ depends="setup,prebuild">
+ <property name="releasedir" value="${builddir}/QueryPath-${version}-minimal" override="true"/>
+ <echo>${releasedir}</echo>
+ <delete dir="${releasedir}" />
+ <mkdir dir="${releasedir}"/>
+ <copy todir="${releasedir}">
+ <filterchain>
+ <stripphpcomments/>
+ <!-- Removed per issue #20 -->
+ <!-- stripwhitespace/ -->
+ <replacetokens begintoken="-" endtoken="%">
+ <token key="UNSTABLE" value="${version}"/>
+ </replacetokens>
+ </filterchain>
+ <fileset dir="${srcdir}/QueryPath">
+ <include name="**/*"/>
+ </fileset>
+ </copy>
+ <copy todir="${releasedir}">
+ <fileset refid="licensefiles"/>
+ </copy>
+ <tar destfile="${packagedir}/QueryPath-${version}-minimal.tgz"
+ compression="gzip">
+ <fileset dir="${builddir}">
+ <include name="QueryPath-${version}-minimal/**/*"/>
+ </fileset>
+ </tar>
+ <zip destfile="${packagedir}/QueryPath-${version}-minimal.zip" basedir="${releasedir}"/>
+ </target>
+
+ <!-- Build the full package -->
+ <target name="fullBuild" description="Full QueryPath build, including docs."
+ depends="setup,prebuild">
+ <property name="releasedir" value="${builddir}/QueryPath-${version}" override="true"/>
+ <delete dir="${releasedir}" />
+
+ <!-- Make all necessary directories. -->
+ <mkdir dir="${releasedir}"/>
+ <mkdir dir="${releasedir}/src"/>
+ <mkdir dir="${releasedir}/examples"/>
+ <mkdir dir="${releasedir}/tests"/>
+ <mkdir dir="${releasedir}/tutorials"/>
+ <mkdir dir="${releasedir}/doc"/>
+
+ <!-- Copy license files. -->
+ <copy todir="${releasedir}">
+ <fileset refid="licensefiles"/>
+ </copy>
+
+ <!-- Copy source code, doing token replacement on version. -->
+ <copy todir="${releasedir}/src">
+ <filterchain>
+ <replacetokens begintoken="-" endtoken="%">
+ <token key="UNSTABLE" value="${version}"/>
+ </replacetokens>
+ </filterchain>
+ <fileset refid="sourcecode"/>
+ </copy>
+
+ <!-- Copy examples. -->
+ <copy todir="${releasedir}/examples">
+ <fileset refid="examplecode"/>
+ </copy>
+
+ <!-- Copy tests. -->
+ <copy todir="${releasedir}/tests">
+ <fileset refid="unittests"/>
+ </copy>
+
+ <!-- Copy tutorials -->
+ <copy todir="${releasedir}/tutorials">
+ <fileset refid="tutorials"/>
+ </copy>
+
+ <!-- Copy documentation -->
+ <copy todir="${releasedir}/doc">
+ <fileset refid="docs"/>
+ </copy>
+
+ <!-- Create tgz and zip versions. -->
+ <tar destfile="${packagedir}/QueryPath-${version}.tgz" compression="gzip">
+ <fileset dir="${builddir}">
+ <include name="QueryPath-${version}/**/*"/>
+ </fileset>
+ </tar>
+ <zip destfile="${packagedir}/QueryPath-${version}.zip" basedir="${releasedir}"/>
+ </target>
+
+ <!-- Check syntax -->
+ <target name="lint" description="Check syntax of source.">
+ <phplint>
+ <fileset refid="sourcecode" />
+ </phplint>
+ </target>
+
+ <!-- Build documentation -->
+ <!--
+ output="HTML:Smarty:QueryPath"
+ -->
+
+ <target name="doc" depends="lint,setup" description="Generate API docs.">
+ <delete dir="${docsdir}"/>
+ <doxygen config="./config.doxy"/>
+
+ <!-- Need to replace version information in generated documentation -->
+ <reflexive>
+ <fileset refid="docs"/>
+ <filterchain>
+ <replacetokens begintoken="-" endtoken="%">
+ <token key="UNSTABLE" value="${version}"/>
+ </replacetokens>
+ </filterchain>
+ </reflexive>
+ </target>
+
+ <!-- tmtarget for textmate phing -->
+ <target name="tmtarget" depends="ftest" description="Target for textmate"/>
+
+ <!-- Run a fast test and print the results to the console -->
+ <target name="ftest" description="Run a quick unit test." depends="pretest">
+ <!-- Fast test. -->
+ <phpunit>
+ <formatter type="summary" usefile="no"/>
+ <batchtest>
+ <fileset refid="unittests"/>
+ </batchtest>
+ </phpunit>
+ <delete file="./test/db/qpTest.db" />
+ <delete file="./test/db/qpTest2.db" />
+ </target>
+
+ <target name="pretest" description="tasks done before any unit testing.">
+ <mkdir dir="test/reports"/>
+ <mkdir dir="test/reports/html"/>
+ <mkdir dir="test/db"/>
+ <!-- touch file="./test/no-writing.xml" / -->
+ <chmod mode="444" file="./test/no-writing.xml"/>
+ </target>
+
+ <!-- Run a fast test and print the results to the console -->
+ <target name="tmtest" description="Run test, optimized for TextMate output." depends="pretest">
+
+ <!-- Fast test. -->
+ <phpunit>
+ <formatter todir="test/reports" type="xml" usefile="yes"/>
+ <batchtest>
+ <fileset refid="unittests"/>
+ </batchtest>
+ </phpunit>
+ <phpunitreport
+ infile="test/reports/testsuites.xml"
+ format="noframes"
+ todir="test/reports/html"
+ />
+ <delete file="./test/db/qpTest.db" />
+ <delete file="./test/db/qpTest2.db" />
+ </target>
+
+ <!-- Run a coverage report -->
+ <target name="coverage" depends="lint" description="Run a coverage analysis.">
+ <coverage-setup database="./test/db/coverage.db">
+ <fileset dir="src">
+ <include name="QueryPath/*.php"/>
+ </fileset>
+ </coverage-setup>
+ <phpunit codecoverage="true" haltonfailure="true">
+ <formatter type="plain" todir="test/reports" outfile="coverage.xml"/>
+ <batchtest>
+ <fileset dir="test/Tests">
+ <include name="**/*Test.php"/>
+ </fileset>
+ </batchtest>
+ </phpunit>
+ <coverage-report outfile="test/reports/coverage.xml">
+ <report todir="test/coverage"/>
+ </coverage-report>
+ </target>
+
+ <!-- Run a full test and format an HTML report -->
+ <target name="test" depends="lint,pretest" description="Run full tests">
+ <mkdir dir="test/reports/html"/>
+ <mkdir dir="test/db"/>
+ <phpunit>
+ <formatter todir="test/reports" type="xml"/>
+ <batchtest>
+ <fileset dir="test/Tests">
+ <include name="**/*Test.php"/>
+ </fileset>
+ </batchtest>
+ </phpunit>
+ <phpunitreport
+ infile="test/reports/testsuites.xml"
+ format="frames"
+ todir="test/reports/html"
+ />
+ <delete file="./test/db/qpTest.db" />
+ <delete file="./test/db/qpTest2.db" />
+ </target>
+
+ <target name="docBuild" description="Build a package containing just docs."
+ depends="setup,prebuild,doc">
+ <property name="releasedir" value="${builddir}/QueryPath-${version}-docs"/>
+ <delete dir="${releasedir}" />
+
+ <!-- Make all necessary directories. -->
+ <mkdir dir="${releasedir}"/>
+
+ <!-- Copy license files. -->
+ <copy todir="${releasedir}">
+ <fileset refid="licensefiles"/>
+ </copy>
+
+ <!-- Copy documentation -->
+ <copy todir="${releasedir}/doc">
+ <fileset refid="docs"/>
+ </copy>
+
+ <!-- Create tgz and zip versions. -->
+ <tar destfile="${packagedir}/QueryPath-${version}-docs.tgz" compression="gzip">
+ <fileset dir="${builddir}">
+ <include name="QueryPath-${version}-docs/**/*"/>
+ </fileset>
+ </tar>
+ <zip destfile="${packagedir}/QueryPath-${version}-docs.zip" basedir="${releasedir}"/>
+ </target>
+
+ <target name="pharBuild" depends="setup,prebuild">
+
+ <property name="releasedir" value="${builddir}/QueryPath-${version}-phar" override="true"/>
+ <echo>${releasedir}</echo>
+ <delete dir="${releasedir}" />
+ <mkdir dir="${releasedir}"/>
+ <copy todir="${releasedir}">
+ <filterchain>
+ <stripphpcomments/>
+ <stripwhitespace/>
+ <replacetokens begintoken="-" endtoken="%">
+ <token key="UNSTABLE" value="${version}"/>
+ <token key="PHAR_FILENAME" value="QueryPath.phar"/>
+ </replacetokens>
+ </filterchain>
+ <fileset dir="${srcdir}/QueryPath">
+ <include name="**/*"/>
+ </fileset>
+ <fileset dir="${phardir}">
+ <include name="**/*.php"/>
+ </fileset>
+ </copy>
+ <copy todir="${releasedir}">
+ <fileset refid="licensefiles"/>
+ </copy>
+
+ <pharpackage
+ destfile="${packagedir}/QueryPath-${version}.phar"
+ basedir="${releasedir}"
+ stub="${releasedir}/basic_loader.php"
+ alias="QueryPath.phar">
+ <fileset dir="${releasedir}">
+ <include name="**/**"/>
+ </fileset>
+ <metadata>
+ <element name="version" value="${version}" />
+ <element name="authors">
+ <element name="M Butcher">
+ <element name="e-mail" value="technosophos@gmail.com" />
+ </element>
+ </element>
+ </metadata>
+ </pharpackage>
+ </target>
+
+ <!-- Use Pyrus to generate package.xml files. -->
+ <target name="pyrusMake" depends="setup,prebuild">
+
+ </target>
+
+ <!-- Use Pyrus to create a PEAR package. -->
+ <target name="pyrusPackage" depends="setup,prebuild">
+
+ </target>
+
+ <!-- Requires PEAR_PackageFileManager2 -->
+ <target name="pearBuild" depends="setup,prebuild,setPearStability">
+ <property name="stability" value="stable"/>
+ <!-- Follow the pear naming conventions for builds -->
+ <if>
+ <equals arg1="${version}" arg2="dev-${DSTAMP}"/>
+ <then>
+ <property name="stability" value="devel" override="true"/>
+ <property name="version" value="2.1dev${DSTAMP}" override="true"/>
+ <echo>Setting version string to ${version}.</echo>
+ </then>
+ </if>
+
+ <!-- Create PEAR release directory -->
+ <property name="peardir" value="${builddir}/pear"/>
+ <property name="releasedir" value="${peardir}/QueryPath-${version}" override="true"/>
+ <echo>${releasedir}</echo>
+ <delete dir="${releasedir}" />
+ <mkdir dir="${releasedir}"/>
+ <mkdir dir="${releasedir}/doc"/>
+ <copy todir="${releasedir}/src">
+ <filterchain>
+ <!-- stripphpcomments/ -->
+ <!-- stripwhitespace/ -->
+ <replacetokens begintoken="-" endtoken="%">
+ <token key="UNSTABLE" value="${version}"/>
+ </replacetokens>
+ </filterchain>
+ <fileset dir="${srcdir}">
+ <include name="**/*"/>
+ <exclude name="**/documentation.php"/>
+ </fileset>
+ </copy>
+ <copy todir="${releasedir}">
+ <fileset refid="licensefiles"/>
+ <fileset dir=".">
+ <include name="package.xml"/>
+ <include name="package_compatible.xml"/>
+ </fileset>
+ </copy>
+ <!-- Pyrus seems incapable of dealing with docs in it's 2.0.0a1 state.
+ <copy todir="${releasedir}/doc">
+ <fileset dir="${docsdir}">
+ <include name="**/*"/>
+ </fileset>
+ </copy>
+ -->
+
+ <copy file="./pear-summary.txt" tofile="${releasedir}/README"/>
+ <move file="${releasedir}/RELEASE" tofile="${releasedir}/RELEASE-${version}"/>
+ <move file="${releasedir}/API" tofile="${releasedir}/API-${version}"/>
+
+ <!-- Generate PEAR package. -->
+ <pyrusmake dir="${releasedir}" packagename="${projectname}" channel="${pear.channel}"/>
+ <pyruspackage packagexml="${releasedir}/package.xml"/>
+ <move file="${projectname}-${version}.tgz" tofile="${packagedir}/${projectname}-${version}.tgz"/>
+
+
+ <!--
+ <pearpkg2 name="QueryPath" dir="./">
+ <fileset dir="${releasedir}">
+ <include name="**/**"/>
+ </fileset>
+ <option name="uri" value="http://querypath.org"/>
+ <option name="packagefile" value="package2.xml"/>
+ <option name="channel" value="pear.querypath.org"/>
+
+ <option name="baseinstalldir" value="QueryPath"/>
+
+ <option name="summary" value="The QueryPath XML/HTML library"/>
+ <option name="description" value="QueryPath is a simple, powerful, robust library for working with XML, HTML, and HTTP. It is like jQuery for PHP. http://querypath.org"/>
+ <option name="apiversion" value="${version}"/>
+ <option name="apistability" value="${stability}"/>
+ <option name="releaseversion" value="${version}"/>
+ <option name="releasestability" value="${stability}"/>
+ <option name="license" value="LGPL 2 or MIT"/>
+ <option name="phpdep" value="5.0.0"/>
+ <option name="pearinstallerdep" value="1.4.6"/>
+ <option name="packagetype" value="php"/>
+ <option name="notes" value="Latest info is available at http://github.com/technosophos/querypath"/>
+ <option name="outputdirectory" value="./${builddir}"/>
+ <option name="packagedirectory" value="./${releasedir}"/>
+ <mapping name="maintainers">
+ <element>
+ <element key="handle" value="technosophos"/>
+ <element key="name" value="M Butcher"/>
+ <element key="email" value="matt@aleph-null.tv"/>
+ <element key="role" value="lead"/>
+ </element>
+ <element>
+ <element key="handle" value="eabrand"/>
+ <element key="name" value="Emily Brand"/>
+ <element key="email" value="unknown@example.com"/>
+ <element key="role" value="developer"/>
+ </element>
+ </mapping>
+ </pearpkg2>
+ -->
+
+ <!-- Copy the package.xml -->
+ <!-- move file="${builddir}/package2.xml" tofile="${peardir}/package.xml"/ -->
+
+ <!--
+ <tar destfile="${packagedir}/QueryPath-${version}.tgz" compression="gzip">
+ <fileset dir="./${peardir}">
+ <include name="QueryPath-${version}/**/*"/>
+ <include name="package.xml"/>
+ </fileset>
+ </tar>
+ -->
+
+ </target>
+
+ <target name="pearPkg" depends="pearBuild">
+ <echo>${packagedir}/QueryPath-${version}.tgz</echo>
+ <!--property name="pirumdir" value="${testdir}/fakepear"/-->
+ <property name="pirumdir" value="${pirum.channel.path}"/>
+ <pirumbuild targetdir="${pirumdir}"/>
+ <pirumadd targetdir="${pirumdir}" packagefile="${packagedir}/QueryPath-${version}.tgz"/>
+ </target>
+
+ <!--
+ This is an attempt to use the Pyrus builder to create a full package.
+ Let's see if it works.
+ -->
+ <!--
+ <target name="pyrusBuild" depends="setup,prebuild">
+ <property name="pyrusPath" value="~/Code/pyrus.phar"/>
+ <exec command="php ${pyrusPath}"/>
+ </target>
+ -->
+
+ <target name="setPearStability" depends="setup">
+ <property name="stability">stable</property>
+ <if>
+ <contains string="${version}" substring="dev"/>
+ <then>
+ <property name="stability" override="true">snapshot</property>
+ </then>
+ <elseif>
+ <contains string="${version}" substring="alpha"/>
+ <then>
+ <property name="stability" override="true">alpha</property>
+ </then>
+ </elseif>
+ <elseif>
+ <contains string="${version}" substring="beta"/>
+ <then>
+ <property name="stability" override="true">beta</property>
+ </then>
+ </elseif>
+ </if>
+ <echo>${stability}</echo>
+ </target>
+</project>
diff --git a/lib/querypath/composer.json b/lib/querypath/composer.json
new file mode 100644
index 0000000..63e1c35
--- /dev/null
+++ b/lib/querypath/composer.json
@@ -0,0 +1,16 @@
+{
+ "name": "querypath/QueryPath",
+ "type": "library",
+ "description": "HTML/XML querying (CSS 4 or XPath) and processing (like jQuery)",
+ "homepage": "https://github.com/technosophos/querypath",
+ "license": "MIT",
+ "keywords": ["xml", "html", "css", "jquery", "xslt"],
+ "require" : {
+ "php" : ">=5.3.0",
+ "masterminds/html5": "2.*"
+ },
+ "autoload": {
+ "psr-0": {"QueryPath": "src/"},
+ "files": ["src/qp_functions.php"]
+ }
+}
diff --git a/lib/querypath/config.doxy b/lib/querypath/config.doxy
new file mode 100644
index 0000000..4a71ea4
--- /dev/null
+++ b/lib/querypath/config.doxy
@@ -0,0 +1,1631 @@
+# Doxyfile 1.7.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+# TAG = value [value, ...]
+# For lists items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded
+# by quotes) that should identify the project.
+
+PROJECT_NAME = QueryPath
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER = -UNSTABLE%
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = doc
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create
+# 4096 sub-directories (in 2 levels) under the output directory of each output
+# format and will distribute the generated files over these directories.
+# Enabling this option can be useful when feeding doxygen a huge amount of
+# source files, where putting all generated files in the same directory would
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
+# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
+# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
+# messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian,
+# Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrilic, Slovak,
+# Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
+
+OUTPUT_LANGUAGE = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator
+# that is used to form the text in various listings. Each string
+# in this list, if found as the leading text of the brief description, will be
+# stripped from the text and the result after processing the whole list, is
+# used as the annotated text. Otherwise, the brief description is used as-is.
+# If left blank, the following values are used ("$name" is automatically
+# replaced with the name of the entity): "The $name class" "The $name widget"
+# "The $name file" "is" "provides" "specifies" "contains"
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES = YES
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the
+# path to strip.
+
+STRIP_FROM_PATH =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of
+# the path mentioned in the documentation of a class, which tells
+# the reader which header file to include in order to use a class.
+# If left blank only the name of the header file containing the class
+# definition is used. Otherwise one should specify the include paths that
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful is your file systems
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like regular Qt-style comments
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF = YES
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will
+# interpret the first line (until the first dot) of a Qt-style
+# comment as the brief description. If set to NO, the comments
+# will behave just like regular Qt-style comments (thus requiring
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# re-implements.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce
+# a new page for each member. If set to NO, the documentation of a member will
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE = 2
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
+# sources only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java
+# sources only. Doxygen will then generate output that is more tailored for
+# Java. For instance, namespaces will be presented as packages, qualified
+# scopes will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources only. Doxygen will then generate output that is more tailored for
+# Fortran.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for
+# VHDL.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given extension.
+# Doxygen has a built-in mapping, but you can override or extend it using this
+# tag. The format is ext=language, where ext is a file extension, and language
+# is one of the parsers supported by doxygen: IDL, Java, Javascript, CSharp, C,
+# C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, C++. For instance to make
+# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C
+# (default is Fortran), use: inc=Fortran f=C. Note that for custom extensions
+# you also need to set FILE_PATTERNS otherwise the files are not read by doxygen.
+
+EXTENSION_MAPPING =
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should
+# set this tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s.
+# func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only.
+# Doxygen will parse them like normal C++ but will assume all classes use public
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate getter
+# and setter methods for a property. Setting this option to YES (the default)
+# will make doxygen to replace the get and set methods by a property in the
+# documentation. This will only work if the methods are indeed getting or
+# setting a simple type. If this is not the case, or you want to show the
+# methods anyway, you should set this option to NO.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING = YES
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum
+# is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically
+# be useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+
+TYPEDEF_HIDES_STRUCT = NO
+
+# The SYMBOL_CACHE_SIZE determines the size of the internal cache use to
+# determine which symbols to keep in memory and which to flush to disk.
+# When the cache is full, less often used symbols will be written to disk.
+# For small to medium size projects (<1000 input files) the default value is
+# probably good enough. For larger projects a too small cache size can cause
+# doxygen to be busy swapping symbols to and from disk most of the time
+# causing a significant performance penality.
+# If the system has enough physical memory increasing the cache will improve the
+# performance by keeping more symbols in memory. Note that the value works on
+# a logarithmic scale so increasing the size by one will rougly double the
+# memory usage. The cache size is given by this formula:
+# 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0,
+# corresponding to a cache size of 2^16 = 65536 symbols
+
+SYMBOL_CACHE_SIZE = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC = YES
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# This flag is only useful for Objective-C code. When set to YES local
+# methods, which are defined in the implementation section but not in
+# the interface are included in the documentation.
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base
+# name of the file that contains the anonymous namespace. By default
+# anonymous namespace are hidden.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES = YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen
+# will list include files with double quotes in the documentation
+# rather than with sharp brackets.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the
+# brief documentation of file, namespace and class members alphabetically
+# by member name. If set to NO (the default) the members will appear in
+# declaration order.
+
+SORT_BRIEF_DOCS = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen
+# will sort the (brief and detailed) documentation of class members so that
+# constructors and destructors are listed first. If set to NO (the default)
+# the constructors will appear in the respective orders defined by
+# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS.
+# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO
+# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
+# hierarchy of group names into alphabetical order. If set to NO (the default)
+# the group names will appear in their defined order.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be
+# sorted by fully-qualified names, including namespaces. If set to
+# NO (the default), the class list will be sorted only by class name,
+# not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or define consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and defines in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES = YES
+
+# If the sources in your project are distributed over multiple directories
+# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy
+# in the documentation. The default is NO.
+
+SHOW_DIRECTORIES = NO
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page.
+# This will remove the Files entry from the Quick Index and from the
+# Folder Tree View (if specified). The default is YES.
+
+SHOW_FILES = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the
+# Namespaces page. This will remove the Namespaces entry from the Quick Index
+# and from the Folder Tree View (if specified). The default is YES.
+
+SHOW_NAMESPACES = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command <command> <input-file>, where <command> is the value of
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file
+# provided by doxygen. Whatever the program writes to standard output
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. The create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option.
+# You can optionally specify a file name after the option, if omitted
+# DoxygenLayout.xml will be used as the name of the layout file.
+
+LAYOUT_FILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some
+# parameters in a documented function, or documenting parameters that
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR = YES
+
+# This WARN_NO_PARAMDOC option can be abled to get warnings for
+# functions that are documented, but have no documentation for their parameters
+# or return value. If set to NO (the default) doxygen will only warn about
+# wrong or incomplete parameter documentation, but not about the absence of
+# documentation.
+
+WARN_NO_PARAMDOC = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text. Optionally the format may contain
+# $version, which will be replaced by the version of the file (if it could
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT = ./src ./test ./examples
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
+# also the default input encoding. Doxygen uses libiconv (or the iconv built
+# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for
+# the list of possible encodings.
+
+INPUT_ENCODING = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx
+# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py *.f90
+
+FILE_PATTERNS = *.php \ *.module \ *.inc \ *.install \ *.js \ *.theme
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or
+# directories that are symbolic links (a Unix filesystem feature) are excluded
+# from the input.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories. Note that the wildcards are matched
+# against the file with absolute path, so to exclude all test directories
+# for example use the pattern */test/*
+
+EXCLUDE_PATTERNS =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+
+EXCLUDE_SYMBOLS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH = ./examples
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output. If FILTER_PATTERNS is specified, this tag will be
+# ignored.
+
+INPUT_FILTER =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form:
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further
+# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER
+# is applied to all files.
+
+FILTER_PATTERNS =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+# Note: To get rid of all source code in the generated output, make sure also
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER = YES
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS = NO
+
+# If the REFERENCED_BY_RELATION tag is set to YES
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION = YES
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES (the default)
+# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from
+# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will
+# link to the source code. Otherwise they will link to the documentation.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code
+# will point to the HTML generated by the htags(1) tool instead of doxygen
+# built-in source browser. The htags tool is part of GNU's global source
+# tagging system (see http://www.gnu.org/software/global/global.html). You
+# will need version 4.8.6 or higher.
+
+USE_HTAGS = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX = YES
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If the tag is left blank doxygen
+# will generate a default style sheet. Note that doxygen will try to copy
+# the style sheet file to the HTML output directory, so don't put your own
+# stylesheet in the HTML output directory as well, or it will be erased!
+
+HTML_STYLESHEET =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output.
+# Doxygen will adjust the colors in the stylesheet and background images
+# according to this color. Hue is specified as an angle on a colorwheel,
+# see http://en.wikipedia.org/wiki/Hue for more information.
+# For instance the value 0 represents red, 60 is yellow, 120 is green,
+# 180 is cyan, 240 is blue, 300 purple, and 360 is red again.
+# The allowed range is 0 to 359.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of
+# the colors in the HTML output. For a value of 0 the output will use
+# grayscales only. A value of 255 will produce the most vivid colors.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to
+# the luminance component of the colors in the HTML output. Values below
+# 100 gradually make the output lighter, whereas values above 100 make
+# the output darker. The value divided by 100 is the actual gamma applied,
+# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2,
+# and 100 does not change the gamma.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting
+# this to NO can help when comparing the output of multiple runs.
+
+HTML_TIMESTAMP = YES
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes,
+# files or namespaces will be aligned in HTML using tables. If set to
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded. For this to work a browser that supports
+# JavaScript and DHTML is required (for instance Mozilla 1.0+, Firefox
+# Netscape 6.0+, Internet explorer 5.0+, Konqueror, or Safari).
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files
+# will be generated that can be used as input for Apple's Xcode 3
+# integrated development environment, introduced with OSX 10.5 (Leopard).
+# To create a documentation set, doxygen will generate a Makefile in the
+# HTML output directory. Running make will produce the docset in that
+# directory and running "make install" will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find
+# it at startup.
+# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html
+# for more information.
+
+GENERATE_DOCSET = NO
+
+# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the
+# feed. A documentation feed provides an umbrella under which multiple
+# documentation sets from a single provider (such as a company or product suite)
+# can be grouped.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that
+# should uniquely identify the documentation set bundle. This should be a
+# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen
+# will append .docset to the name.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+
+# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher.
+
+DOCSET_PUBLISHER_NAME = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compiled HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can
+# be used to specify the file name of the resulting .chm file. You
+# can add a path in front of the file if the result should not be
+# written to the html output directory.
+
+CHM_FILE =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can
+# be used to specify the location (absolute path including file name) of
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING
+# is used to encode HtmlHelp index (hhk), content (hhc) and project file
+# content.
+
+CHM_INDEX_ENCODING =
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated
+# that can be used as input for Qt's qhelpgenerator to generate a
+# Qt Compressed Help (.qch) of the generated HTML documentation.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can
+# be used to specify the file name of the resulting .qch file.
+# The path specified is relative to the HTML output folder.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#namespace
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating
+# Qt Help Project output. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#virtual-folders
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to
+# add. For more information please see
+# http://doc.trolltech.com/qthelpproject.html#custom-filters
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see
+# <a href="http://doc.trolltech.com/qthelpproject.html#custom-filters">
+# Qt Help Project / Custom Filters</a>.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's
+# filter section matches.
+# <a href="http://doc.trolltech.com/qthelpproject.html#filter-attributes">
+# Qt Help Project / Filter Attributes</a>.
+
+QHP_SECT_FILTER_ATTRS =
+
+# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can
+# be used to specify the location of Qt's qhelpgenerator.
+# If non-empty doxygen will try to run qhelpgenerator on the generated
+# .qhp file.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files
+# will be generated, which together with the HTML files, form an Eclipse help
+# plugin. To install this plugin and make it available under the help contents
+# menu in Eclipse, the contents of the directory containing the HTML and XML
+# files needs to be copied into the plugins directory of eclipse. The name of
+# the directory within the plugins directory should be the same as
+# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before
+# the help appears.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have
+# this name.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at
+# top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it.
+
+DISABLE_INDEX = NO
+
+# This tag can be used to set the number of enum values (range [1..20])
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE = 4
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information.
+# If the tag value is set to YES, a side panel will be generated
+# containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser).
+# Windows users are probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW = YES
+
+# By enabling USE_INLINE_TREES, doxygen will generate the Groups, Directories,
+# and Class Hierarchy pages using a tree view instead of an ordered list.
+
+USE_INLINE_TREES = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH = 250
+
+# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open
+# links to external symbols imported via tag files in a separate window.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# Use this tag to change the font size of Latex formulas included
+# as images in the HTML documentation. The default is 10. Note that
+# when you change the font size after a successful doxygen run you need
+# to manually remove any form_*.png images from the HTML output directory
+# to force them to be regenerated.
+
+FORMULA_FONTSIZE = 10
+
+# Use the FORMULA_TRANPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are
+# not supported properly for IE 6.0, but are supported on all modern browsers.
+# Note that when changing this option you need to delete any form_*.png files
+# in the HTML output before the changes have effect.
+
+FORMULA_TRANSPARENT = YES
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box
+# for the HTML output. The underlying search engine uses javascript
+# and DHTML and should work on any modern browser. Note that when using
+# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets
+# (GENERATE_DOCSET) there is already a search function so this one should
+# typically be disabled. For large projects the javascript based search engine
+# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution.
+
+SEARCHENGINE = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a PHP enabled web server instead of at the web client
+# using Javascript. Doxygen will generate the search PHP script and index
+# file to put on the web server. The advantage of the server
+# based approach is that it scales better to large projects and allows
+# full text search. The disadvances is that it is more difficult to setup
+# and does not have live searching capabilities.
+
+SERVER_BASED_SEARCH = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked. If left blank `latex' will be used as the default command name.
+# Note that when enabling USE_PDFLATEX this option is only used for
+# generating bitmaps for formulas in the HTML output, but not in the
+# Makefile that is written to the output directory.
+
+LATEX_CMD_NAME = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, a4wide, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS = YES
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not
+# include the index chapters (such as File Index, Compound Index, etc.)
+# in the output.
+
+LATEX_HIDE_INDICES = NO
+
+# If LATEX_SOURCE_CODE is set to YES then doxygen will include
+# source code with syntax highlighting in the LaTeX output.
+# Note that which sources are shown also depends on other settings
+# such as SOURCE_BROWSER.
+
+LATEX_SOURCE_CODE = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimized for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation.
+
+GENERATE_XML = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_SCHEMA =
+
+# The XML_DTD tag can be used to specify an XML DTD,
+# which can be used by a validating XML parser to check the
+# syntax of the XML files.
+
+XML_DTD =
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will
+# dump the program listings (including syntax highlighting
+# and cross-referencing information) to the XML output. Note that
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will
+# generate a Perl module file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_PERLMOD = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be
+# nicely formatted so it can be parsed by a human reader. This is useful
+# if you want to understand what is going on. On the other hand, if this
+# tag is set to NO the size of the Perl module output will be much smaller
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY = YES
+
+# The names of the make variables in the generated doxyrules.make file
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX.
+# This is useful so different doxyrules.make files included by the same
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_DEFINED tags.
+
+EXPAND_ONLY_PREDEF = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed. To prevent a macro definition from being
+# undefined via #undef or recursively expanded use the := operator
+# instead of the = operator.
+
+PREDEFINED =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all function-like macros that are alone
+# on a line, have an all uppercase name, and do not end with a semicolon. Such
+# function macros are typically used for boiler-plate code, and will confuse
+# the parser if not removed.
+
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles.
+# Optionally an initial location of the external documentation
+# can be added for each tagfile. The format of a tag file without
+# this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where "loc1" and "loc2" can be relative or absolute paths or
+# URLs. If a location is present for each tag, the installdox tool
+# does not have to be run to correct the links.
+# Note that each tag file must have a unique name
+# (where the name does NOT include the path)
+# If a tag file is not located in the directory in which doxygen
+# is run, you must also specify the path to the tagfile here.
+
+TAGFILES =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base
+# or super classes. Setting the tag to NO turns the diagrams off. Note that
+# this option is superseded by the HAVE_DOT option below. This is only a
+# fallback. It is recommended to install and use dot, since it yields more
+# powerful graphs.
+
+CLASS_DIAGRAMS = YES
+
+# You can define message sequence charts within doxygen comments using the \msc
+# command. Doxygen will then run the mscgen tool (see
+# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the
+# documentation. The MSCGEN_PATH tag allows you to specify the directory where
+# the mscgen tool resides. If left empty the tool is assumed to be found in the
+# default search path.
+
+MSCGEN_PATH =
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is
+# allowed to run in parallel. When set to 0 (the default) doxygen will
+# base this on the number of processors available in the system. You can set it
+# explicitly to a value larger than 0 to get control over the balance
+# between CPU load and processing speed.
+
+DOT_NUM_THREADS = 0
+
+# By default doxygen will write a font called FreeSans.ttf to the output
+# directory and reference it in all dot files that doxygen generates. This
+# font does not include all possible unicode characters however, so when you need
+# these (or just want a differently looking font) you can specify the font name
+# using DOT_FONTNAME. You need need to make sure dot is able to find the font,
+# which can be done by putting it in a standard location or by setting the
+# DOTFONTPATH environment variable or by setting DOT_FONTPATH to the directory
+# containing the font.
+
+DOT_FONTNAME = FreeSans.ttf
+
+# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs.
+# The default size is 10pt.
+
+DOT_FONTSIZE = 10
+
+# By default doxygen will tell dot to use the output directory to look for the
+# FreeSans.ttf font (which doxygen will put there itself). If you specify a
+# different font using DOT_FONTNAME you can set the path where dot
+# can find it using this tag.
+
+DOT_FONTPATH =
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH = YES
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+
+UML_LOOK = NO
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH = YES
+
+# If the CALL_GRAPH and HAVE_DOT options are set to YES then
+# doxygen will generate a call dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable call graphs
+# for selected functions only using the \callgraph command.
+
+CALL_GRAPH = NO
+
+# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then
+# doxygen will generate a caller dependency graph for every global function
+# or class method. Note that enabling this option will significantly increase
+# the time of a run. So in most cases it will be better to enable caller
+# graphs for selected functions only using the \callergraph command.
+
+CALLER_GRAPH = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY = YES
+
+# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES
+# then doxygen will show the dependencies a directory has on other directories
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of
+# nodes that will be shown in the graph. If the number of nodes in a graph
+# becomes larger than this value, doxygen will truncate the graph, which is
+# visualized by representing a node as a red box. Note that doxygen if the
+# number of direct children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note
+# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+
+DOT_GRAPH_MAX_NODES = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the
+# graphs generated by dot. A depth value of 3 means that only nodes reachable
+# from the root by following a path via at most 3 edges will be shown. Nodes
+# that lay further from the root node will be omitted. Note that setting this
+# option to 1 or 2 may greatly reduce the computation time needed for large
+# code bases. Also note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+
+MAX_DOT_GRAPH_DEPTH = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not
+# seem to support this out of the box. Warning: Depending on the platform used,
+# enabling this option may lead to badly anti-aliased labels on the edges of
+# a graph (i.e. they become hard to read).
+
+DOT_TRANSPARENT = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10)
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS = NO
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermediate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP = YES
diff --git a/lib/querypath/package.xml b/lib/querypath/package.xml
new file mode 100644
index 0000000..017d150
--- /dev/null
+++ b/lib/querypath/package.xml
@@ -0,0 +1,546 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package version="2.1" xmlns="http://pear.php.net/dtd/package-2.1" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.1 http://pear.php.net/dtd/package-2.1.xsd">
+ <name>QueryPath</name>
+ <channel>pear.querypath.org</channel>
+ <summary>QueryPath: A library for working with XML, HTML, and web services.
+</summary>
+ <description>
+QueryPath implements much of the jQuery API in PHP, and supports CSS
+selector queries, jQuery's traversal and manipulation APIs, and
+adds many other useful methods. Learn more at http://querypath.org.</description>
+ <lead>
+ <name>Matt Butcher</name>
+ <user>technosophos</user>
+ <email>matt@aleph-null.tv</email>
+ <active>yes</active>
+ </lead>
+ <developer>
+ <name>Emily Brand</name>
+ <user>eabrand</user>
+ <email>emily@example.com</email>
+ <active>yes</active>
+ </developer>
+ <contributor>
+ <name>Woody Gilk</name>
+ <user>shadowhand</user>
+ <email>woody@wingsc.com</email>
+ <active>yes</active>
+ </contributor>
+ <date>2010-10-21</date>
+ <time>17:56:42</time>
+ <version>
+ <release>2.1.0dev2</release>
+ <api>2.1.0dev2</api>
+ </version>
+ <stability>
+ <release>devel</release>
+ <api>devel</api>
+ </stability>
+ <license uri="http://www.opensource.org/licenses/lgpl-license.php">LGPL or MIT (Your Choice)</license>
+ <notes>RELEASE NOTES
+
+Big Changes:
+
+1. There is now an `htmlqp()` function that parses crufty HTML in a far
+more reliable way than `qp()`. Use this instead of any variant of
+the older `@qp()` setup.
+
+2. The API has been brought into alignment with jQuery 1.4. See
+API-2.1.0 for details.
+
+3. This release was driven substantially by eabrand's GSOC 2010
+contributions. Thanks, Emily!
+
+4. There are now Phar and PEAR packages available. Got to
+http://pear.querypath.org for PEAR packages.
+
+5. The minimal QP distribution is no longer minified, as it reportedly
+causes XDebug to crash.
+
+7. Data URs are now supported. QueryPath can now embed images directly
+into HTML and XML this way.
+
+8. Documentation is now in Doxygen instead of PhpDocumentor. Thanks
+to Matt Farina and Kevin O'Brien for their input.
+
+Changes in 2.1
+
+All changes are documented in their respective doc blocks. So take a
+look at http://api.querypath.org to learn more.
+
+New Functions
+ * The `htmlqp()` method has been added for parsing icky HTML. Use
+ this for web scraping.
+
+Altered Functions
+ * The qp() function now supports the following new options:
+ - convert_to_encoding
+ - convert_from_encoding
+ - strip_low_ascii
+ - use_parser
+
+New Methods
+ * attach()/detach()
+ * has()
+ * emptyElement()
+ * even()/odd()
+ * first()/last()
+ * firstChild()/lastChild()
+ * nextUntil()/prevUntil()
+ * parentsUntil()
+ * encodeDataURL()
+ * dataURL()
+ * filterPreg()
+
+Altered Methods
+ * css() has been changed to allow subsequent calls
+ to modify the style attribute (issue #28)
+ * attr() has been changed. If it is called with no
+ arguments, it now returns all attributes.
+
+New CSS Selectors Behavior
+
+ * :contains-exactly() performs as :contains() used to perform.
+
+Altered CSS Selectors Behavior
+
+ * The star operator (*) is now non-greedy, per spec. Before, the star would match
+ any descendants. Now it will only match children.
+ * :contains() now does substring matching instead of exact matching. This conforms
+ to jQuery's behavior.
+ * Quotes are now checked carefully before being stripped from pseudo-class values.
+ * Issue #40 identified a potential infinite looping problem on poorly formed selectors.
+ This was fixed.
+</notes>
+ <contents>
+ <dir name="/">
+ <dir name="doc" baseinstalldir="/">
+ <dir name="html">
+ <dir name="search">
+ <file role="doc" name="all_5f.html"/>
+ <file role="doc" name="all_6c.html"/>
+ <file role="doc" name="all_6d.html"/>
+ <file role="doc" name="all_6e.html"/>
+ <file role="doc" name="all_6f.html"/>
+ <file role="doc" name="all_24.html"/>
+ <file role="doc" name="all_61.html"/>
+ <file role="doc" name="all_62.html"/>
+ <file role="doc" name="all_63.html"/>
+ <file role="doc" name="all_64.html"/>
+ <file role="doc" name="all_65.html"/>
+ <file role="doc" name="all_66.html"/>
+ <file role="doc" name="all_67.html"/>
+ <file role="doc" name="all_68.html"/>
+ <file role="doc" name="all_69.html"/>
+ <file role="doc" name="all_70.html"/>
+ <file role="doc" name="all_71.html"/>
+ <file role="doc" name="all_72.html"/>
+ <file role="doc" name="all_73.html"/>
+ <file role="doc" name="all_74.html"/>
+ <file role="doc" name="all_75.html"/>
+ <file role="doc" name="all_76.html"/>
+ <file role="doc" name="all_77.html"/>
+ <file role="doc" name="all_78.html"/>
+ <file role="doc" name="classes_6e.html"/>
+ <file role="doc" name="classes_62.html"/>
+ <file role="doc" name="classes_63.html"/>
+ <file role="doc" name="classes_66.html"/>
+ <file role="doc" name="classes_69.html"/>
+ <file role="doc" name="classes_71.html"/>
+ <file role="doc" name="classes_73.html"/>
+ <file role="doc" name="classes_74.html"/>
+ <file role="doc" name="classes_78.html"/>
+ <file role="doc" name="close.png"/>
+ <file role="doc" name="enums_6d.html"/>
+ <file role="doc" name="enums_6e.html"/>
+ <file role="doc" name="enums_64.html"/>
+ <file role="doc" name="enums_68.html"/>
+ <file role="doc" name="files_6d.html"/>
+ <file role="doc" name="files_6f.html"/>
+ <file role="doc" name="files_62.html"/>
+ <file role="doc" name="files_63.html"/>
+ <file role="doc" name="files_64.html"/>
+ <file role="doc" name="files_66.html"/>
+ <file role="doc" name="files_68.html"/>
+ <file role="doc" name="files_70.html"/>
+ <file role="doc" name="files_71.html"/>
+ <file role="doc" name="files_72.html"/>
+ <file role="doc" name="files_73.html"/>
+ <file role="doc" name="files_74.html"/>
+ <file role="doc" name="files_78.html"/>
+ <file role="doc" name="functions_5f.html"/>
+ <file role="doc" name="functions_6c.html"/>
+ <file role="doc" name="functions_6d.html"/>
+ <file role="doc" name="functions_6e.html"/>
+ <file role="doc" name="functions_6f.html"/>
+ <file role="doc" name="functions_61.html"/>
+ <file role="doc" name="functions_62.html"/>
+ <file role="doc" name="functions_63.html"/>
+ <file role="doc" name="functions_64.html"/>
+ <file role="doc" name="functions_65.html"/>
+ <file role="doc" name="functions_66.html"/>
+ <file role="doc" name="functions_67.html"/>
+ <file role="doc" name="functions_68.html"/>
+ <file role="doc" name="functions_69.html"/>
+ <file role="doc" name="functions_70.html"/>
+ <file role="doc" name="functions_71.html"/>
+ <file role="doc" name="functions_72.html"/>
+ <file role="doc" name="functions_73.html"/>
+ <file role="doc" name="functions_74.html"/>
+ <file role="doc" name="functions_75.html"/>
+ <file role="doc" name="functions_76.html"/>
+ <file role="doc" name="functions_77.html"/>
+ <file role="doc" name="functions_78.html"/>
+ <file role="doc" name="mag_sel.png"/>
+ <file role="doc" name="nomatches.html"/>
+ <file role="doc" name="search.css"/>
+ <file role="doc" name="search.js"/>
+ <file role="doc" name="search_l.png"/>
+ <file role="doc" name="search_m.png"/>
+ <file role="doc" name="search_r.png"/>
+ <file role="doc" name="variables_6c.html"/>
+ <file role="doc" name="variables_6f.html"/>
+ <file role="doc" name="variables_24.html"/>
+ <file role="doc" name="variables_61.html"/>
+ <file role="doc" name="variables_62.html"/>
+ <file role="doc" name="variables_63.html"/>
+ <file role="doc" name="variables_64.html"/>
+ <file role="doc" name="variables_65.html"/>
+ <file role="doc" name="variables_68.html"/>
+ <file role="doc" name="variables_69.html"/>
+ <file role="doc" name="variables_70.html"/>
+ <file role="doc" name="variables_71.html"/>
+ <file role="doc" name="variables_72.html"/>
+ <file role="doc" name="variables_73.html"/>
+ <file role="doc" name="variables_74.html"/>
+ <file role="doc" name="variables_75.html"/>
+ <file role="doc" name="variables_76.html"/>
+ <file role="doc" name="variables_77.html"/>
+ <file role="doc" name="variables_78.html"/>
+ </dir>
+ <file role="doc" name="annotated.html"/>
+ <file role="doc" name="bc_s.png"/>
+ <file role="doc" name="classes.html"/>
+ <file role="doc" name="class_bottom_up_css_event_handler_tests-members.html"/>
+ <file role="doc" name="class_bottom_up_css_event_handler_tests.html"/>
+ <file role="doc" name="class_bottom_up_css_event_handler_tests.png"/>
+ <file role="doc" name="class_css_event_parser_tests-members.html"/>
+ <file role="doc" name="class_css_event_parser_tests.html"/>
+ <file role="doc" name="class_css_input_stream-members.html"/>
+ <file role="doc" name="class_css_input_stream.html"/>
+ <file role="doc" name="class_css_parser-members.html"/>
+ <file role="doc" name="class_css_parser.html"/>
+ <file role="doc" name="class_css_scanner-members.html"/>
+ <file role="doc" name="class_css_scanner.html"/>
+ <file role="doc" name="class_css_token-members.html"/>
+ <file role="doc" name="class_css_token.html"/>
+ <file role="doc" name="class_css_token_test-members.html"/>
+ <file role="doc" name="class_css_token_test.html"/>
+ <file role="doc" name="class_c_s_s_parse_exception.html"/>
+ <file role="doc" name="class_fixture_one-members.html"/>
+ <file role="doc" name="class_fixture_one.html"/>
+ <file role="doc" name="class_fixture_two-members.html"/>
+ <file role="doc" name="class_fixture_two.html"/>
+ <file role="doc" name="class_introspect_me-members.html"/>
+ <file role="doc" name="class_introspect_me.html"/>
+ <file role="doc" name="class_not_implemented_exception.html"/>
+ <file role="doc" name="class_query_path-members.html"/>
+ <file role="doc" name="class_query_path.html"/>
+ <file role="doc" name="class_query_path.png"/>
+ <file role="doc" name="class_query_path_css_event_handler-members.html"/>
+ <file role="doc" name="class_query_path_css_event_handler.html"/>
+ <file role="doc" name="class_query_path_css_event_handler.png"/>
+ <file role="doc" name="class_query_path_css_event_handler_tests-members.html"/>
+ <file role="doc" name="class_query_path_css_event_handler_tests.html"/>
+ <file role="doc" name="class_query_path_css_event_handler_tests.png"/>
+ <file role="doc" name="class_query_path_entities-members.html"/>
+ <file role="doc" name="class_query_path_entities.html"/>
+ <file role="doc" name="class_query_path_entities_test-members.html"/>
+ <file role="doc" name="class_query_path_entities_test.html"/>
+ <file role="doc" name="class_query_path_exception.html"/>
+ <file role="doc" name="class_query_path_exception.png"/>
+ <file role="doc" name="class_query_path_extended-members.html"/>
+ <file role="doc" name="class_query_path_extended.html"/>
+ <file role="doc" name="class_query_path_extended.png"/>
+ <file role="doc" name="class_query_path_extension_registry-members.html"/>
+ <file role="doc" name="class_query_path_extension_registry.html"/>
+ <file role="doc" name="class_query_path_extension_test-members.html"/>
+ <file role="doc" name="class_query_path_extension_test.html"/>
+ <file role="doc" name="class_query_path_extension_test.png"/>
+ <file role="doc" name="class_query_path_iterator-members.html"/>
+ <file role="doc" name="class_query_path_iterator.html"/>
+ <file role="doc" name="class_query_path_i_o_exception-members.html"/>
+ <file role="doc" name="class_query_path_i_o_exception.html"/>
+ <file role="doc" name="class_query_path_i_o_exception.png"/>
+ <file role="doc" name="class_query_path_options-members.html"/>
+ <file role="doc" name="class_query_path_options.html"/>
+ <file role="doc" name="class_query_path_options_test-members.html"/>
+ <file role="doc" name="class_query_path_options_test.html"/>
+ <file role="doc" name="class_query_path_parse_exception-members.html"/>
+ <file role="doc" name="class_query_path_parse_exception.html"/>
+ <file role="doc" name="class_query_path_parse_exception.png"/>
+ <file role="doc" name="class_query_path_test-members.html"/>
+ <file role="doc" name="class_query_path_test.html"/>
+ <file role="doc" name="class_query_path_test.png"/>
+ <file role="doc" name="class_q_p_d_b-members.html"/>
+ <file role="doc" name="class_q_p_d_b.html"/>
+ <file role="doc" name="class_q_p_d_b.png"/>
+ <file role="doc" name="class_q_p_d_b_test-members.html"/>
+ <file role="doc" name="class_q_p_d_b_test.html"/>
+ <file role="doc" name="class_q_p_list-members.html"/>
+ <file role="doc" name="class_q_p_list.html"/>
+ <file role="doc" name="class_q_p_list.png"/>
+ <file role="doc" name="class_q_p_list_tests-members.html"/>
+ <file role="doc" name="class_q_p_list_tests.html"/>
+ <file role="doc" name="class_q_p_table_data-members.html"/>
+ <file role="doc" name="class_q_p_table_data.html"/>
+ <file role="doc" name="class_q_p_table_data.png"/>
+ <file role="doc" name="class_q_p_table_text_data-members.html"/>
+ <file role="doc" name="class_q_p_table_text_data.html"/>
+ <file role="doc" name="class_q_p_table_text_data.png"/>
+ <file role="doc" name="class_q_p_t_p_l-members.html"/>
+ <file role="doc" name="class_q_p_t_p_l.html"/>
+ <file role="doc" name="class_q_p_t_p_l.png"/>
+ <file role="doc" name="class_q_p_t_p_l_test-members.html"/>
+ <file role="doc" name="class_q_p_t_p_l_test.html"/>
+ <file role="doc" name="class_q_p_x_m_l-members.html"/>
+ <file role="doc" name="class_q_p_x_m_l.html"/>
+ <file role="doc" name="class_q_p_x_m_l.png"/>
+ <file role="doc" name="class_q_p_x_m_l_tests-members.html"/>
+ <file role="doc" name="class_q_p_x_m_l_tests.html"/>
+ <file role="doc" name="class_q_p_x_s_l-members.html"/>
+ <file role="doc" name="class_q_p_x_s_l.html"/>
+ <file role="doc" name="class_q_p_x_s_l.png"/>
+ <file role="doc" name="class_q_p_x_s_l_tests-members.html"/>
+ <file role="doc" name="class_q_p_x_s_l_tests.html"/>
+ <file role="doc" name="class_simple_test_css_event_handler-members.html"/>
+ <file role="doc" name="class_simple_test_css_event_handler.html"/>
+ <file role="doc" name="class_simple_test_css_event_handler.png"/>
+ <file role="doc" name="class_stub_extension_one-members.html"/>
+ <file role="doc" name="class_stub_extension_one.html"/>
+ <file role="doc" name="class_stub_extension_one.png"/>
+ <file role="doc" name="class_stub_extension_three-members.html"/>
+ <file role="doc" name="class_stub_extension_three.html"/>
+ <file role="doc" name="class_stub_extension_three.png"/>
+ <file role="doc" name="class_stub_extension_two-members.html"/>
+ <file role="doc" name="class_stub_extension_two.html"/>
+ <file role="doc" name="class_stub_extension_two.png"/>
+ <file role="doc" name="class_test_css_event_handler-members.html"/>
+ <file role="doc" name="class_test_css_event_handler.html"/>
+ <file role="doc" name="class_test_css_event_handler.png"/>
+ <file role="doc" name="class_test_event-members.html"/>
+ <file role="doc" name="class_test_event.html"/>
+ <file role="doc" name="class_x_m_lish_mock-members.html"/>
+ <file role="doc" name="class_x_m_lish_mock.html"/>
+ <file role="doc" name="class_x_m_lish_mock.png"/>
+ <file role="doc" name="class_x_m_lish_test-members.html"/>
+ <file role="doc" name="class_x_m_lish_test.html"/>
+ <file role="doc" name="closed.png"/>
+ <file role="doc" name="database__import_8php.html"/>
+ <file role="doc" name="database__import_8php_source.html"/>
+ <file role="doc" name="dbpedia_8php.html"/>
+ <file role="doc" name="dbpedia_8php_source.html"/>
+ <file role="doc" name="deprecated.html"/>
+ <file role="doc" name="dirty__html_8php.html"/>
+ <file role="doc" name="dirty__html_8php_source.html"/>
+ <file role="doc" name="documentation_8php.html"/>
+ <file role="doc" name="documentation_8php_source.html"/>
+ <file role="doc" name="docx_8php.html"/>
+ <file role="doc" name="docx_8php_source.html"/>
+ <file role="doc" name="doc_8php.html"/>
+ <file role="doc" name="doc_8php_source.html"/>
+ <file role="doc" name="doxygen.css"/>
+ <file role="doc" name="doxygen.png"/>
+ <file role="doc" name="extensions.html"/>
+ <file role="doc" name="fetch__rss_8php.html"/>
+ <file role="doc" name="fetch__rss_8php_source.html"/>
+ <file role="doc" name="files.html"/>
+ <file role="doc" name="ftv2blank.png"/>
+ <file role="doc" name="ftv2doc.png"/>
+ <file role="doc" name="ftv2folderclosed.png"/>
+ <file role="doc" name="ftv2folderopen.png"/>
+ <file role="doc" name="ftv2lastnode.png"/>
+ <file role="doc" name="ftv2link.png"/>
+ <file role="doc" name="ftv2mlastnode.png"/>
+ <file role="doc" name="ftv2mnode.png"/>
+ <file role="doc" name="ftv2node.png"/>
+ <file role="doc" name="ftv2plastnode.png"/>
+ <file role="doc" name="ftv2pnode.png"/>
+ <file role="doc" name="ftv2vertline.png"/>
+ <file role="doc" name="functions.html"/>
+ <file role="doc" name="functions_0x5f.html"/>
+ <file role="doc" name="functions_0x6c.html"/>
+ <file role="doc" name="functions_0x6d.html"/>
+ <file role="doc" name="functions_0x6e.html"/>
+ <file role="doc" name="functions_0x6f.html"/>
+ <file role="doc" name="functions_0x61.html"/>
+ <file role="doc" name="functions_0x62.html"/>
+ <file role="doc" name="functions_0x63.html"/>
+ <file role="doc" name="functions_0x64.html"/>
+ <file role="doc" name="functions_0x65.html"/>
+ <file role="doc" name="functions_0x66.html"/>
+ <file role="doc" name="functions_0x67.html"/>
+ <file role="doc" name="functions_0x68.html"/>
+ <file role="doc" name="functions_0x69.html"/>
+ <file role="doc" name="functions_0x70.html"/>
+ <file role="doc" name="functions_0x71.html"/>
+ <file role="doc" name="functions_0x72.html"/>
+ <file role="doc" name="functions_0x73.html"/>
+ <file role="doc" name="functions_0x74.html"/>
+ <file role="doc" name="functions_0x75.html"/>
+ <file role="doc" name="functions_0x76.html"/>
+ <file role="doc" name="functions_0x77.html"/>
+ <file role="doc" name="functions_0x78.html"/>
+ <file role="doc" name="functions_func.html"/>
+ <file role="doc" name="functions_func_0x6c.html"/>
+ <file role="doc" name="functions_func_0x6d.html"/>
+ <file role="doc" name="functions_func_0x6e.html"/>
+ <file role="doc" name="functions_func_0x6f.html"/>
+ <file role="doc" name="functions_func_0x61.html"/>
+ <file role="doc" name="functions_func_0x62.html"/>
+ <file role="doc" name="functions_func_0x63.html"/>
+ <file role="doc" name="functions_func_0x64.html"/>
+ <file role="doc" name="functions_func_0x65.html"/>
+ <file role="doc" name="functions_func_0x66.html"/>
+ <file role="doc" name="functions_func_0x67.html"/>
+ <file role="doc" name="functions_func_0x68.html"/>
+ <file role="doc" name="functions_func_0x69.html"/>
+ <file role="doc" name="functions_func_0x70.html"/>
+ <file role="doc" name="functions_func_0x71.html"/>
+ <file role="doc" name="functions_func_0x72.html"/>
+ <file role="doc" name="functions_func_0x73.html"/>
+ <file role="doc" name="functions_func_0x74.html"/>
+ <file role="doc" name="functions_func_0x75.html"/>
+ <file role="doc" name="functions_func_0x76.html"/>
+ <file role="doc" name="functions_func_0x77.html"/>
+ <file role="doc" name="functions_func_0x78.html"/>
+ <file role="doc" name="functions_vars.html"/>
+ <file role="doc" name="globals.html"/>
+ <file role="doc" name="globals_enum.html"/>
+ <file role="doc" name="globals_func.html"/>
+ <file role="doc" name="globals_vars.html"/>
+ <file role="doc" name="group__querypath__core.html"/>
+ <file role="doc" name="group__querypath__css.html"/>
+ <file role="doc" name="group__querypath__extensions.html"/>
+ <file role="doc" name="group__querypath__tests.html"/>
+ <file role="doc" name="group__querypath__util.html"/>
+ <file role="doc" name="hierarchy.html"/>
+ <file role="doc" name="html_8php.html"/>
+ <file role="doc" name="html_8php_source.html"/>
+ <file role="doc" name="index.html"/>
+ <file role="doc" name="installdox"/>
+ <file role="doc" name="interface_css_event_handler-members.html"/>
+ <file role="doc" name="interface_css_event_handler.html"/>
+ <file role="doc" name="interface_css_event_handler.png"/>
+ <file role="doc" name="interface_query_path_extension-members.html"/>
+ <file role="doc" name="interface_query_path_extension.html"/>
+ <file role="doc" name="interface_query_path_extension.png"/>
+ <file role="doc" name="interface_table_able-members.html"/>
+ <file role="doc" name="interface_table_able.html"/>
+ <file role="doc" name="interface_table_able.png"/>
+ <file role="doc" name="main.html"/>
+ <file role="doc" name="matching__text__content_8php.html"/>
+ <file role="doc" name="matching__text__content_8php_source.html"/>
+ <file role="doc" name="modules.html"/>
+ <file role="doc" name="musicbrainz_8php.html"/>
+ <file role="doc" name="musicbrainz_8php_source.html"/>
+ <file role="doc" name="nav_f.png"/>
+ <file role="doc" name="nav_h.png"/>
+ <file role="doc" name="odt_8php.html"/>
+ <file role="doc" name="odt_8php_source.html"/>
+ <file role="doc" name="open.png"/>
+ <file role="doc" name="pages.html"/>
+ <file role="doc" name="parse__php_8php.html"/>
+ <file role="doc" name="parse__php_8php_source.html"/>
+ <file role="doc" name="rss_8php.html"/>
+ <file role="doc" name="rss_8php_source.html"/>
+ <file role="doc" name="simple__example_8php.html"/>
+ <file role="doc" name="simple__example_8php_source.html"/>
+ <file role="doc" name="sparql_8php.html"/>
+ <file role="doc" name="sparql_8php_source.html"/>
+ <file role="doc" name="svg_8php.html"/>
+ <file role="doc" name="svg_8php_source.html"/>
+ <file role="doc" name="tabs.css"/>
+ <file role="doc" name="tab_a.png"/>
+ <file role="doc" name="tab_b.png"/>
+ <file role="doc" name="tab_h.png"/>
+ <file role="doc" name="tab_s.png"/>
+ <file role="doc" name="techniques_8php.html"/>
+ <file role="doc" name="techniques_8php_source.html"/>
+ <file role="doc" name="test_8php.html"/>
+ <file role="doc" name="test_8php_source.html"/>
+ <file role="doc" name="todo.html"/>
+ <file role="doc" name="tree.html"/>
+ <file role="doc" name="xml_8php.html"/>
+ <file role="doc" name="xml_8php_source.html"/>
+ <file role="doc" name="_bottom_up_css_event_handler_tests_8php.html"/>
+ <file role="doc" name="_bottom_up_css_event_handler_tests_8php_source.html"/>
+ <file role="doc" name="_css_event_handler_8php.html"/>
+ <file role="doc" name="_css_event_handler_8php_source.html"/>
+ <file role="doc" name="_css_event_test_8php.html"/>
+ <file role="doc" name="_css_event_test_8php_source.html"/>
+ <file role="doc" name="_css_parser_8php.html"/>
+ <file role="doc" name="_css_parser_8php_source.html"/>
+ <file role="doc" name="_query_path_8php.html"/>
+ <file role="doc" name="_query_path_8php_source.html"/>
+ <file role="doc" name="_query_path_entities_test_8php.html"/>
+ <file role="doc" name="_query_path_entities_test_8php_source.html"/>
+ <file role="doc" name="_query_path_extension_8php.html"/>
+ <file role="doc" name="_query_path_extension_8php_source.html"/>
+ <file role="doc" name="_query_path_extension_test_8php.html"/>
+ <file role="doc" name="_query_path_extension_test_8php_source.html"/>
+ <file role="doc" name="_query_path_options_test_8php.html"/>
+ <file role="doc" name="_query_path_options_test_8php_source.html"/>
+ <file role="doc" name="_query_path_test_8php.html"/>
+ <file role="doc" name="_query_path_test_8php_source.html"/>
+ <file role="doc" name="_q_p_d_b_8php.html"/>
+ <file role="doc" name="_q_p_d_b_8php_source.html"/>
+ <file role="doc" name="_q_p_d_b_test_8php.html"/>
+ <file role="doc" name="_q_p_d_b_test_8php_source.html"/>
+ <file role="doc" name="_q_p_list_8php.html"/>
+ <file role="doc" name="_q_p_list_8php_source.html"/>
+ <file role="doc" name="_q_p_list_test_8php.html"/>
+ <file role="doc" name="_q_p_list_test_8php_source.html"/>
+ <file role="doc" name="_q_p_t_p_l_8php.html"/>
+ <file role="doc" name="_q_p_t_p_l_8php_source.html"/>
+ <file role="doc" name="_q_p_t_p_l_test_8php.html"/>
+ <file role="doc" name="_q_p_t_p_l_test_8php_source.html"/>
+ <file role="doc" name="_q_p_x_m_l_8php.html"/>
+ <file role="doc" name="_q_p_x_m_l_8php_source.html"/>
+ <file role="doc" name="_q_p_x_m_l_test_8php.html"/>
+ <file role="doc" name="_q_p_x_m_l_test_8php_source.html"/>
+ <file role="doc" name="_q_p_x_s_l_8php.html"/>
+ <file role="doc" name="_q_p_x_s_l_8php_source.html"/>
+ <file role="doc" name="_q_p_x_s_l_test_8php.html"/>
+ <file role="doc" name="_q_p_x_s_l_test_8php_source.html"/>
+ </dir>
+ </dir>
+ <dir name="src" baseinstalldir="/">
+ <dir name="QueryPath">
+ <dir name="Extension">
+ <file role="php" name="QPDB.php"/>
+ <file role="php" name="QPList.php"/>
+ <file role="php" name="QPTPL.php"/>
+ <file role="php" name="QPXML.php"/>
+ <file role="php" name="QPXSL.php"/>
+ </dir>
+ <file role="php" name="CssEventHandler.php"/>
+ <file role="php" name="CssParser.php"/>
+ <file role="php" name="QueryPath.php"/>
+ <file role="php" name="QueryPathExtension.php"/>
+ </dir>
+ </dir>
+ </dir>
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>2.0.0a1</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease/>
+</package>
diff --git a/lib/querypath/package_compatible.xml b/lib/querypath/package_compatible.xml
new file mode 100644
index 0000000..50789b7
--- /dev/null
+++ b/lib/querypath/package_compatible.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package version="2.1" xmlns="http://pear.php.net/dtd/package-2.1" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.1 http://pear.php.net/dtd/package-2.1.xsd">
+ <name>QueryPath</name>
+ <channel>pear.querypath.org</channel>
+ <summary>QueryPath: A library for working with XML, HTML, and web services.
+</summary>
+ <description>
+QueryPath implements much of the jQuery API in PHP, and supports CSS
+selector queries, jQuery's traversal and manipulation APIs, and
+adds many other useful methods. Learn more at http://querypath.org.</description>
+ <lead>
+ <name>Matt Butcher</name>
+ <user>technosophos</user>
+ <email>matt@aleph-null.tv</email>
+ <active>yes</active>
+ </lead>
+ <developer>
+ <name>Emily Brand</name>
+ <user>eabrand</user>
+ <email>emily@example.com</email>
+ <active>yes</active>
+ </developer>
+ <contributor>
+ <name>Woody Gilk</name>
+ <user>shadowhand</user>
+ <email>woody@wingsc.com</email>
+ <active>yes</active>
+ </contributor>
+ <date>2010-10-20</date>
+ <time>12:15:12</time>
+ <version>
+ <release>2.1.0beta3</release>
+ <api>2.1.0beta3</api>
+ </version>
+ <stability>
+ <release>beta</release>
+ <api>beta</api>
+ </stability>
+ <license uri="http://www.opensource.org/licenses/lgpl-license.php">LGPL or MIT (your choice)</license>
+ <notes>
+</notes>
+ <contents>
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.2.0</min>
+ </php>
+ <pearinstaller>
+ <min>1.4.8</min>
+ </pearinstaller>
+ </required>
+ </dependencies>
+ <phprelease>
+ </phprelease>
+</package>
diff --git a/lib/querypath/pear-summary.txt b/lib/querypath/pear-summary.txt
new file mode 100644
index 0000000..015119a
--- /dev/null
+++ b/lib/querypath/pear-summary.txt
@@ -0,0 +1,5 @@
+QueryPath: A library for working with XML, HTML, and web services.
+
+QueryPath implements much of the jQuery API in PHP, and supports CSS
+selector queries, jQuery's traversal and manipulation APIs, and
+adds many other useful methods. Learn more at http://querypath.org. \ No newline at end of file
diff --git a/lib/querypath/quickstart-guide.md b/lib/querypath/quickstart-guide.md
new file mode 100644
index 0000000..da1cfde
--- /dev/null
+++ b/lib/querypath/quickstart-guide.md
@@ -0,0 +1,113 @@
+# QueryPath QuickStart
+
+This short guide is intended to help you get started with QueryPath 3.
+
+## Using QueryPath in Your Project
+
+To use QueryPath inside of your own application, you will need to make sure that PHP can find the QueryPath library. There are a few possible ways of doing this. The first is to use an autoloader. The second is to include QueryPath manually. We'll look briefly at each.
+
+### Autoloaders and QueryPath
+
+In recent time, PHP has standardized a method of automatically importing classes by name. This is often called [PSR-0 autoloading](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md). Symfony, Composer, and many other PHP projects use PSR-0 autoloaders, and QueryPath should work with those. In addition, QueryPath has its own autoloader in `qp.php`.
+
+To use QueryPath's autoloader, all you need to do is include `qp.php`. This will detect if another autoloader is already in place, and if not, it will configure it's own autoloader:
+
+```{php}
+<?php
+require 'qp.php';
+
+print QueryPath::withHTML('http://technosophos.com', 'title')->text();
+
+print htmlqp('http://technosophos.com', 'title')->text();
+?>
+```
+
+The above illustrates the requiring of QueryPath's autoloader. Note that in that case we don't need to do anything else to get the `QueryPath` class or the `htmlqp()` functions.
+
+QueryPath also ships with [Composer](http://getcomposer.org) support. Composer provides PSR-0 autoloading. To use Composer's autoloader, you can do this:
+
+```{php}
+<?php
+// The composer autoloader.
+require 'vender/autoload.php';
+
+print QueryPath::withHTML('http://technosophos.com', 'title')->text();
+
+// THIS DOESN'T WORK!
+// print htmlqp('http://technosophos.com', 'title')->text();
+?>
+```
+
+Notice, though, that the `qp()` and `htmlqp` functions *will not work* with this method. Why? Because PHP's autoloader does not know about functions. It operates on classes only. So you can use QueryPath's Object-Oriented API (`QueryPath::with()`, `QueryPath::withHTML()`, `QueryPath::withXML()`), but not the `qp()` and `qphtml()` functions. If you want to use those, too, simply include `qp.php`:
+
+```{php}
+<?php
+// The composer autoloader.
+require 'vender/autoload.php';
+require 'qp.php';
+
+print QueryPath::withHTML('http://technosophos.com', 'title')->text();
+
+// This works because qp.php was imported
+print htmlqp('http://technosophos.com', 'title')->text();
+?>
+```
+
+## A Simple Example
+
+So far, we have seen a few variations of the same program. Let's learn what it does. Here's the program:
+
+```{php}
+<?php
+require 'qp.php';
+
+print QueryPath::withHTML('http://technosophos.com', 'title')->text();
+
+print htmlqp('http://technosophos.com', 'title')->text();
+?>
+```
+
+This does the same thing two different ways. Let's look at line 3:
+
+```{php}
+<?php
+print QueryPath::withHTML('http://technosophos.com', 'title')->text();
+?>
+```
+
+This line does three things:
+
+1. It loads and parses the HTML document it finds at `http://techosophos.com`. QueryPath can load documents locally and remotely. It can also load strings of HTML or XML, as well as `SimpleXML` objects and `DOMDocument` objects. It should be easy to get your HTML or XML loaded into QueryPath.
+2. It performs a search for the tag named `title`. QueryPath uses CSS 4 Selectors (as the current draft stands) as a query language -- just like jQuery and CSS. (If you prefer XPath, check out the `xpath()` method on QueryPath). Of course, `title` is a very basic selector. You can do more advanced selectors like `#bar-one table>tr:odd td>a:first-of-type()`, which looks for the element with ID `bar-one` and then fetches every odd row from its table, then from each cell in the row, it finds the first hyperlink.
+3. Finally, the example calls `text()`, which will fetch the text content of the first element it's found (in this case, the `title` tag in the HTML head). If not title is found, this will return an empty string. Otherwise it will return the text of that tag.
+
+QueryPath has well over 60 methods like `text()`. Some are for navigating, like `top()`, `children()`, `next()`, and `prev()`. Some are for manipulating the parts of an HTML or XML element, like `attar()`. Others are for doing sophisticated finding and filtering operations (`find()`, `filter()`, `filterCallback()`, `map()`, and so on). And, of course, there are methods for modifying the document (`append()`, `before()`, `after()`, `attr()`, `text()`, and many more).
+
+The goal of QueryPath is to make it easy for you to process XML and HTML documents. There may be a lot of methods to learn (just like jQuery), but those methods are there to make your life simpler.
+
+## HTML vs XML
+
+When QueryPath was first introduced, it did not distinguish between XML and HTML documents. At that time, momentum was behind XHTML, and it looked like the future was XML. But over time, it has become abundantly clear that HTML documents cannot be treated as XML during parsing and processing, or during output.
+
+So there are now separate parser functions for HTML and XML -- as well as a generic parser function that inspects the document and attempts to determine whether it is XML or HTML:
+
+* `QueryPath::withXML()`: This *only* handles XML documents. If you give it an HTML document, it will attempt to force XML parsing on that document.
+* `htmlqp()`, `QueryPath::withHTML()`: This will force QueryPath to use the HTML parser. it will also make a number of adjustments to QueryPath to accommodate common HTML breakages.
+* `qp()`, `QueryPath::with()`: This will attempt to guess whether the document is XML or HTML. In general, it favors XML slightly. Guessing may be done by…
+ - File extension
+ - XML declaration
+ - The suggestions made by any options passed into the document
+
+###… And Character Encoding
+
+XML suggests that all documents be encoded as UTF-8. Most HTML documents are encoded using one of the ISO specifications (typically ISO-8859-1). And web servers are often misconfigured to report that documents are using one character set when they are actually using another.
+
+To work around all of these issues, QueryPath attempts to convert documents automatically. It does this using PHP's internal character detection libraries. But sometimes it guesses wrong. You can adjust this feature manually by passing in language settings in the `$options` array. See the documentation on `qp()` for details.
+
+
+## Where to go from here
+
+* [QueryPath.org](http://querypath.org) has pointers to other resources.
+* [The API docs](http://api.querypath.org) have detailed explanations of every single part of QueryPath.
+
+ \ No newline at end of file
diff --git a/lib/querypath/src/QueryPath.php b/lib/querypath/src/QueryPath.php
new file mode 100644
index 0000000..2013377
--- /dev/null
+++ b/lib/querypath/src/QueryPath.php
@@ -0,0 +1,356 @@
+<?php
+/** @file
+ * The Query Path package provides tools for manipulating a structured document.
+ * Typically, the sort of structured document is one using a Document Object Model
+ * (DOM).
+ * The two major DOMs are the XML DOM and the HTML DOM. Using Query Path, you can
+ * build, parse, search, and modify DOM documents.
+ *
+ * To use QueryPath, only one file must be imported: qp.php. This file defines
+ * the `qp()` function, and also registers an autoloader if necessary.
+ *
+ * Standard usage:
+ * @code
+ * <?php
+ * require 'qp.php';
+ *
+ * $xml = '<?xml version="1.0"?><test><foo id="myID"/></test>';
+ *
+ * // Procedural call a la jQuery:
+ * $qp = qp($xml, '#myID');
+ * $qp->append('<new><elements/></new>')->writeHTML();
+ *
+ * // Object-oriented version with a factory:
+ * $qp = QueryPath::with($xml)->find('#myID')
+ * $qp->append('<new><elements/></new>')->writeHTML();
+ * ?>
+ * @endcode
+ *
+ * The above would print (formatted for readability):
+ * @code
+ * <?xml version="1.0"?>
+ * <test>
+ * <foo id="myID">
+ * <new>
+ * <element/>
+ * </new>
+ * </foo>
+ * </test>
+ * @endcode
+ *
+ * ## Discovering the Library
+ *
+ * To gain familiarity with QueryPath, the following three API docs are
+ * the best to start with:
+ *
+ *- qp(): This function constructs new queries, and is the starting point
+ * for manipulating a document. htmlqp() is an alias tuned for HTML
+ * documents (especially old HTML), and QueryPath::with(), QueryPath::withXML()
+ * and QueryPath::withHTML() all perform a similar role, but in a purely
+ * object oriented way.
+ *- QueryPath: This is the top-level class for the library. It defines the
+ * main factories and some useful functions.
+ *- QueryPath::Query: This defines all of the functions in QueryPath. When
+ * working with HTML and XML, the QueryPath::DOMQuery is the actual
+ * implementation that you work with.
+ *
+ * Included with the source code for QueryPath is a complete set of unit tests
+ * as well as some example files. Those are good resources for learning about
+ * how to apply QueryPath's tools. The full API documentation can be generated
+ * from these files using Doxygen, or you can view it online at
+ * http://api.querypath.org.
+ *
+ * If you are interested in building extensions for QueryPath, see the
+ * QueryPath and QueryPath::Extension classes. There you will find information on adding
+ * your own tools to QueryPath.
+ *
+ * QueryPath also comes with a full CSS 3 selector implementation (now
+ * with partial support for the current draft of the CSS 4 selector spec). If
+ * you are interested in reusing that in other code, you will want to start
+ * with QueryPath::CSS::EventHandler.php, which is the event interface for the parser.
+ *
+ * All of the code in QueryPath is licensed under an MIT-style license
+ * license. All of the code is Copyright, 2012 by Matt Butcher.
+ *
+ * @author M Butcher <matt @aleph-null.tv>
+ * @license MIT
+ * @see QueryPath
+ * @see qp()
+ * @see http://querypath.org The QueryPath home page.
+ * @see http://api.querypath.org An online version of the API docs.
+ * @see http://technosophos.com For how-tos and examples.
+ * @copyright Copyright (c) 2009-2012, Matt Butcher.
+ * @version -UNSTABLE% (3.x.x)
+ *
+ */
+
+use \Masterminds\HTML5;
+
+/**
+ *
+ */
+class QueryPath {
+ /**
+ * The version string for this version of QueryPath.
+ *
+ * Standard releases will be of the following form: <MAJOR>.<MINOR>[.<PATCH>][-STABILITY].
+ *
+ * Examples:
+ * - 2.0
+ * - 2.1.1
+ * - 2.0-alpha1
+ *
+ * Developer releases will always be of the form dev-<DATE>.
+ *
+ * @since 2.0
+ */
+ const VERSION = '3.0.x';
+
+ /**
+ * Major version number.
+ *
+ * Examples:
+ * - 3
+ * - 4
+ *
+ * @since 3.0.1
+ */
+ const VERSION_MAJOR = 3;
+
+ /**
+ * This is a stub HTML 4.01 document.
+ *
+ * <b>Using {@link QueryPath::XHTML_STUB} is preferred.</b>
+ *
+ * This is primarily for generating legacy HTML content. Modern web applications
+ * should use QueryPath::XHTML_STUB.
+ *
+ * Use this stub with the HTML familiy of methods (QueryPath::Query::html(),
+ * QueryPath::Query::writeHTML(), QueryPath::Query::innerHTML()).
+ */
+ const HTML_STUB = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+ <html lang="en">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Untitled</title>
+ </head>
+ <body></body>
+ </html>';
+
+ const HTML5_STUB = '<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Untitled</title>
+ </head>
+ <body></body>
+ </html>';
+
+ /**
+ * This is a stub XHTML document.
+ *
+ * Since XHTML is an XML format, you should use XML functions with this document
+ * fragment. For example, you should use {@link xml()}, {@link innerXML()}, and
+ * {@link writeXML()}.
+ *
+ * This can be passed into {@link qp()} to begin a new basic HTML document.
+ *
+ * Example:
+ * @code
+ * $qp = qp(QueryPath::XHTML_STUB); // Creates a new XHTML document
+ * $qp->writeXML(); // Writes the document as well-formed XHTML.
+ * @endcode
+ * @since 2.0
+ */
+ const XHTML_STUB = '<?xml version="1.0"?>
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ <html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title>Untitled</title>
+ </head>
+ <body></body>
+ </html>';
+
+
+ public static function with($document = NULL, $selector = NULL, $options = array()) {
+ $qpClass = isset($options['QueryPath_class']) ? $options['QueryPath_class'] : '\QueryPath\DOMQuery';
+
+ $qp = new $qpClass($document, $selector, $options);
+ return $qp;
+ }
+
+ public static function withXML($source = NULL, $selector = NULL, $options = array()) {
+ $options += array(
+ 'use_parser' => 'xml',
+ );
+ return self::with($source, $selector, $options);
+ }
+
+ public static function withHTML($source = NULL, $selector = NULL, $options = array()) {
+ // Need a way to force an HTML parse instead of an XML parse when the
+ // doctype is XHTML, since many XHTML documents are not valid XML
+ // (because of coding errors, not by design).
+
+ $options += array(
+ 'ignore_parser_warnings' => TRUE,
+ 'convert_to_encoding' => 'ISO-8859-1',
+ 'convert_from_encoding' => 'auto',
+ //'replace_entities' => TRUE,
+ 'use_parser' => 'html',
+ // This is stripping actually necessary low ASCII.
+ //'strip_low_ascii' => TRUE,
+ );
+ return @self::with($source, $selector, $options);
+ }
+
+ /**
+ * Parse HTML5 documents.
+ *
+ * This uses HTML5-PHP to parse the document. In actuality, this parser does
+ * a fine job with pre-HTML5 documents in most cases, though really old HTML
+ * (like 2.0) may have some substantial quirks.
+ *
+ * <b>Supported Options</b>
+ * Any options supported by HTML5-PHP are allowed here. Additionally, the
+ * following options have meaning to QueryPath.
+ * - QueryPath_class
+ *
+ *
+ * @param mixed $source
+ * A document as an HTML string, or a path/URL. For compatibility with
+ * existing functions, a DOMDocument, SimpleXMLElement, DOMNode or array
+ * of DOMNodes will be passed through as well. However, these types are not
+ * validated in any way.
+ *
+ * @param string $selector
+ * A CSS3 selector.
+ *
+ * @param array $options
+ * An associative array of options, which is passed on into HTML5-PHP. Note
+ * that the standard QueryPath options may be ignored for this function,
+ * since it uses a different parser.
+ *
+ * @return QueryPath
+ */
+ public static function withHTML5($source = NULL, $selector = NULL, $options = array()) {
+ $qpClass = isset($options['QueryPath_class']) ? $options['QueryPath_class'] : '\QueryPath\DOMQuery';
+
+ if(is_string($source)) {
+ $html5 = new HTML5();
+ if (strpos($source, '<') !== FALSE && strpos($source, '>') !== FALSE) {
+ $source = $html5->loadHTML($source);
+ }
+ else {
+ $source = $html5->load($source);
+ }
+ }
+
+ $qp = new $qpClass($source, $selector, $options);
+ return $qp;
+ }
+
+ /**
+ * Enable one or more extensions.
+ *
+ * Extensions provide additional features to QueryPath. To enable and
+ * extension, you can use this method.
+ *
+ * In this example, we enable the QPTPL extension:
+ * @code
+ * <?php
+ * QueryPath::enable('\QueryPath\QPTPL');
+ * ?>
+ * @endcode
+ *
+ * Note that the name is a fully qualified class name.
+ *
+ * We can enable more than one extension at a time like this:
+ *
+ * @code
+ * <?php
+ * $extensions = array('\QueryPath\QPXML', '\QueryPath\QPDB');
+ * QueryPath::enable($extensions);
+ * ?>
+ * @endcode
+ *
+ * @attention If you are not using an autoloader, you will need to
+ * manually `require` or `include` the files that contain the
+ * extensions.
+ *
+ * @param mixed $extensionNames
+ * The name of an extension or an array of extension names.
+ * QueryPath assumes that these are extension class names,
+ * and attempts to register these as QueryPath extensions.
+ */
+ public static function enable($extensionNames) {
+
+ if (is_array($extensionNames)) {
+ foreach ($extensionNames as $extension) {
+ \QueryPath\ExtensionRegistry::extend($extension);
+ }
+ }
+ else {
+ \QueryPath\ExtensionRegistry::extend($extensionNames);
+ }
+ }
+
+ /**
+ * Get a list of all of the enabled extensions.
+ *
+ * This example dumps a list of extensions to standard output:
+ * @code
+ * <?php
+ * $extensions = QueryPath::enabledExtensions();
+ * print_r($extensions);
+ * ?>
+ * @endcode
+ *
+ * @return array
+ * An array of extension names.
+ *
+ * @see QueryPath::ExtensionRegistry
+ */
+ public static function enabledExtensions() {
+ return \QueryPath\ExtensionRegistry::extensionNames();
+ }
+
+
+
+ /**
+ * A static function for transforming data into a Data URL.
+ *
+ * This can be used to create Data URLs for injection into CSS, JavaScript, or other
+ * non-XML/HTML content. If you are working with QP objects, you may want to use
+ * dataURL() instead.
+ *
+ * @param mixed $data
+ * The contents to inject as the data. The value can be any one of the following:
+ * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
+ * MUST BE A FULL URL, not a relative path.
+ * - A string of data: If this is given, then the subsystem will encode the string.
+ * - A stream or file handle: If this is given, the stream's contents will be encoded
+ * and inserted as data.
+ * (Note that we make the assumption here that you would never want to set data to be
+ * a URL. If this is an incorrect assumption, file a bug.)
+ * @param string $mime
+ * The MIME type of the document.
+ * @param resource $context
+ * A valid context. Use this only if you need to pass a stream context. This is only necessary
+ * if $data is a URL. (See {@link stream_context_create()}).
+ * @return
+ * An encoded data URL.
+ */
+ public static function encodeDataURL($data, $mime = 'application/octet-stream', $context = NULL) {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+ elseif (filter_var($data, FILTER_VALIDATE_URL)) {
+ $data = file_get_contents($data, FALSE, $context);
+ }
+
+ $encoded = base64_encode($data);
+
+ return 'data:' . $mime . ';base64,' . $encoded;
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/DOMTraverser.php b/lib/querypath/src/QueryPath/CSS/DOMTraverser.php
new file mode 100644
index 0000000..be8c2af
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/DOMTraverser.php
@@ -0,0 +1,775 @@
+<?php
+/** @file
+ * Traverse a DOM.
+ */
+
+namespace QueryPath\CSS;
+
+use \QueryPath\CSS\DOMTraverser\Util;
+use \QueryPath\CSS\DOMTraverser\PseudoClass;
+use \QueryPath\CSS\DOMTraverser\PseudoElement;
+
+/**
+ * Traverse a DOM, finding matches to the selector.
+ *
+ * This traverses a DOMDocument and attempts to find
+ * matches to the provided selector.
+ *
+ * \b How this works
+ *
+ * This performs a bottom-up search. On the first pass,
+ * it attempts to find all of the matching elements for the
+ * last simple selector in a selector.
+ *
+ * Subsequent passes attempt to eliminate matches from the
+ * initial matching set.
+ *
+ * Example:
+ *
+ * Say we begin with the selector `foo.bar baz`. This is processed
+ * as follows:
+ *
+ * - First, find all baz elements.
+ * - Next, for any baz element that does not have foo as an ancestor,
+ * eliminate it from the matches.
+ * - Finally, for those that have foo as an ancestor, does that foo
+ * also have a class baz? If not, it is removed from the matches.
+ *
+ * \b Extrapolation
+ *
+ * Partial simple selectors are almost always expanded to include an
+ * element.
+ *
+ * Examples:
+ *
+ * - `:first` is expanded to `*:first`
+ * - `.bar` is expanded to `*.bar`.
+ * - `.outer .inner` is expanded to `*.outer *.inner`
+ *
+ * The exception is that IDs are sometimes not expanded, e.g.:
+ *
+ * - `#myElement` does not get expanded
+ * - `#myElement .class` \i may be expanded to `*#myElement *.class`
+ * (which will obviously not perform well).
+ */
+class DOMTraverser implements Traverser {
+
+ protected $matches = array();
+ protected $selector;
+ protected $dom;
+ protected $initialized = TRUE;
+ protected $psHandler;
+ protected $scopeNode;
+
+ /**
+ * Build a new DOMTraverser.
+ *
+ * This requires a DOM-like object or collection of DOM nodes.
+ */
+ public function __construct(\SPLObjectStorage $splos, $initialized = FALSE, $scopeNode = NULL) {
+
+ $this->psHandler = new \QueryPath\CSS\DOMTraverser\PseudoClass();
+ $this->initialized = $initialized;
+
+ // Re-use the initial splos
+ $this->matches = $splos;
+
+ if (count($splos) != 0) {
+ $splos->rewind();
+ $first = $splos->current();
+ if ($first instanceof \DOMDocument) {
+ $this->dom = $first;//->documentElement;
+ }
+ else {
+ $this->dom = $first->ownerDocument;//->documentElement;
+ }
+ if (empty($scopeNode)) {
+ $this->scopeNode = $this->dom->documentElement;
+ }
+ else {
+ $this->scopeNode = $scopeNode;
+ }
+ }
+
+ // This assumes a DOM. Need to also accomodate the case
+ // where we get a set of elements.
+ /*
+ $this->dom = $dom;
+ $this->matches = new \SplObjectStorage();
+ $this->matches->attach($this->dom);
+ */
+ }
+
+ public function debug($msg) {
+ fwrite(STDOUT, PHP_EOL . $msg);
+ }
+
+ /**
+ * Given a selector, find the matches in the given DOM.
+ *
+ * This is the main function for querying the DOM using a CSS
+ * selector.
+ *
+ * @param string $selector
+ * The selector.
+ * @return \SPLObjectStorage
+ * An SPLObjectStorage containing a list of matched
+ * DOMNode objects.
+ */
+ public function find($selector) {
+ // Setup
+ $handler = new Selector();
+ $parser = new Parser($selector, $handler);
+ $parser->parse();
+ $this->selector = $handler;
+
+ //$selector = $handler->toArray();
+ $found = $this->newMatches();
+ foreach ($handler as $selectorGroup) {
+ // fprintf(STDOUT, "Selector group.\n");
+ // Initialize matches if necessary.
+ if ($this->initialized) {
+ $candidates = $this->matches;
+ }
+ else {
+ //if (empty($selectorGroup)) {
+ // fprintf(STDOUT, "%s", print_r($handler->toArray(), TRUE));
+ //}
+ $candidates = $this->initialMatch($selectorGroup[0], $this->matches);
+ //$this->initialized = TRUE;
+ }
+
+ foreach ($candidates as $candidate) {
+ // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]);
+ if ($this->matchesSelector($candidate, $selectorGroup)) {
+ // $this->debug('Attaching ' . $candidate->nodeName);
+ $found->attach($candidate);
+ }
+ }
+ }
+ $this->setMatches($found);
+
+
+ return $this;
+ }
+
+ public function matches() {
+ return $this->matches;
+ }
+
+ /**
+ * Check whether the given node matches the given selector.
+ *
+ * A selector is a group of one or more simple selectors combined
+ * by combinators. This determines if a given selector
+ * matches the given node.
+ *
+ * @attention
+ * Evaluation of selectors is done recursively. Thus the length
+ * of the selector is limited to the recursion depth allowed by
+ * the PHP configuration. This should only cause problems for
+ * absolutely huge selectors or for versions of PHP tuned to
+ * strictly limit recursion depth.
+ *
+ * @param object DOMNode
+ * The DOMNode to check.
+ * @param array Selector->toArray()
+ * The Selector to check.
+ * @return boolean
+ * A boolean TRUE if the node matches, false otherwise.
+ */
+ public function matchesSelector($node, $selector) {
+ return $this->matchesSimpleSelector($node, $selector, 0);
+ }
+
+ /**
+ * Performs a match check on a SimpleSelector.
+ *
+ * Where matchesSelector() does a check on an entire selector,
+ * this checks only a simple selector (plus an optional
+ * combinator).
+ *
+ * @param object DOMNode
+ * The DOMNode to check.
+ * @param object SimpleSelector
+ * The Selector to check.
+ * @return boolean
+ * A boolean TRUE if the node matches, false otherwise.
+ */
+ public function matchesSimpleSelector($node, $selectors, $index) {
+ $selector = $selectors[$index];
+ // Note that this will short circuit as soon as one of these
+ // returns FALSE.
+ $result = $this->matchElement($node, $selector->element, $selector->ns)
+ && $this->matchAttributes($node, $selector->attributes)
+ && $this->matchId($node, $selector->id)
+ && $this->matchClasses($node, $selector->classes)
+ && $this->matchPseudoClasses($node, $selector->pseudoClasses)
+ && $this->matchPseudoElements($node, $selector->pseudoElements);
+
+ $isNextRule = isset($selectors[++$index]);
+ // If there is another selector, we process that if there a match
+ // hasn't been found.
+ /*
+ if ($isNextRule && $selectors[$index]->combinator == SimpleSelector::anotherSelector) {
+ // We may need to re-initialize the match set for the next selector.
+ if (!$this->initialized) {
+ $this->initialMatch($selectors[$index]);
+ }
+ if (!$result) fprintf(STDOUT, "Element: %s, Next selector: %s\n", $node->tagName, $selectors[$index]);
+ return $result || $this->matchesSimpleSelector($node, $selectors, $index);
+ }
+ // If we have a match and we have a combinator, we need to
+ // recurse up the tree.
+ else*/if ($isNextRule && $result) {
+ $result = $this->combine($node, $selectors, $index);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Combine the next selector with the given match
+ * using the next combinator.
+ *
+ * If the next selector is combined with another
+ * selector, that will be evaluated too, and so on.
+ * So if this function returns TRUE, it means that all
+ * child selectors are also matches.
+ *
+ * @param DOMNode $node
+ * The DOMNode to test.
+ * @param array $selectors
+ * The array of simple selectors.
+ * @param int $index
+ * The index of the current selector.
+ * @return boolean
+ * TRUE if the next selector(s) match.
+ */
+ public function combine($node, $selectors, $index) {
+ $selector = $selectors[$index];
+ //$this->debug(implode(' ', $selectors));
+ switch ($selector->combinator) {
+ case SimpleSelector::adjacent:
+ return $this->combineAdjacent($node, $selectors, $index);
+ case SimpleSelector::sibling:
+ return $this->combineSibling($node, $selectors, $index);
+ case SimpleSelector::directDescendant:
+ return $this->combineDirectDescendant($node, $selectors, $index);
+ case SimpleSelector::anyDescendant:
+ return $this->combineAnyDescendant($node, $selectors, $index);
+ case SimpleSelector::anotherSelector:
+ // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]);
+ return $this->matchesSimpleSelector($node, $selectors, $index);
+ ;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Process an Adjacent Sibling.
+ *
+ * The spec does not indicate whether Adjacent should ignore non-Element
+ * nodes, so we choose to ignore them.
+ *
+ * @param DOMNode $node
+ * A DOM Node.
+ * @param array $selectors
+ * The selectors array.
+ * @param int $index
+ * The current index to the operative simple selector in the selectors
+ * array.
+ * @return boolean
+ * TRUE if the combination matches, FALSE otherwise.
+ */
+ public function combineAdjacent($node, $selectors, $index) {
+ while (!empty($node->previousSibling)) {
+ $node = $node->previousSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE) {
+ //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index]));
+ return $this->matchesSimpleSelector($node, $selectors, $index);
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Check all siblings.
+ *
+ * According to the spec, this only tests elements LEFT of the provided
+ * node.
+ *
+ * @param DOMNode $node
+ * A DOM Node.
+ * @param array $selectors
+ * The selectors array.
+ * @param int $index
+ * The current index to the operative simple selector in the selectors
+ * array.
+ * @return boolean
+ * TRUE if the combination matches, FALSE otherwise.
+ */
+ public function combineSibling($node, $selectors, $index) {
+ while (!empty($node->previousSibling)) {
+ $node = $node->previousSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Handle a Direct Descendant combination.
+ *
+ * Check whether the given node is a rightly-related descendant
+ * of its parent node.
+ *
+ * @param DOMNode $node
+ * A DOM Node.
+ * @param array $selectors
+ * The selectors array.
+ * @param int $index
+ * The current index to the operative simple selector in the selectors
+ * array.
+ * @return boolean
+ * TRUE if the combination matches, FALSE otherwise.
+ */
+ public function combineDirectDescendant($node, $selectors, $index) {
+ $parent = $node->parentNode;
+ if (empty($parent)) {
+ return FALSE;
+ }
+ return $this->matchesSimpleSelector($parent, $selectors, $index);
+ }
+
+ /**
+ * Handle Any Descendant combinations.
+ *
+ * This checks to see if there are any matching routes from the
+ * selector beginning at the present node.
+ *
+ * @param DOMNode $node
+ * A DOM Node.
+ * @param array $selectors
+ * The selectors array.
+ * @param int $index
+ * The current index to the operative simple selector in the selectors
+ * array.
+ * @return boolean
+ * TRUE if the combination matches, FALSE otherwise.
+ */
+ public function combineAnyDescendant($node, $selectors, $index) {
+ while (!empty($node->parentNode)) {
+ $node = $node->parentNode;
+
+ // Catch case where element is child of something
+ // else. This should really only happen with a
+ // document element.
+ if ($node->nodeType != XML_ELEMENT_NODE) {
+ continue;
+ }
+
+ if ($this->matchesSimpleSelector($node, $selectors, $index)) {
+ return TRUE;
+ }
+ }
+ }
+
+ /**
+ * Get the intial match set.
+ *
+ * This should only be executed when not working with
+ * an existing match set.
+ */
+ protected function initialMatch($selector, $matches) {
+ $element = $selector->element;
+
+ // If no element is specified, we have to start with the
+ // entire document.
+ if ($element == NULL) {
+ $element = '*';
+ }
+
+ // fprintf(STDOUT, "Initial match using %s.\n", $selector);
+
+ // We try to do some optimization here to reduce the
+ // number of matches to the bare minimum. This will
+ // reduce the subsequent number of operations that
+ // must be performed in the query.
+
+ // Experimental: ID queries use XPath to match, since
+ // this should give us only a single matched element
+ // to work with.
+ if (/*$element == '*' &&*/ !empty($selector->id)) {
+ // fprintf(STDOUT, "ID Fastrack on %s\n", $selector);
+ $initialMatches = $this->initialMatchOnID($selector, $matches);
+ }
+ // If a namespace is set, find the namespace matches.
+ elseif (!empty($selector->ns)) {
+ $initialMatches = $this->initialMatchOnElementNS($selector, $matches);
+ }
+ // If the element is a wildcard, using class can
+ // substantially reduce the number of elements that
+ // we start with.
+ elseif ($element == '*' && !empty($selector->classes)) {
+ // fprintf(STDOUT, "Class Fastrack on %s\n", $selector);
+ $initialMatches = $this->initialMatchOnClasses($selector, $matches);
+ }
+ else {
+ $initialMatches = $this->initialMatchOnElement($selector, $matches);
+ }
+
+ //fprintf(STDOUT, "Found %d nodes.\n", count($this->matches));
+ return $initialMatches;
+ }
+
+ /**
+ * Shortcut for finding initial match by ID.
+ *
+ * If the element is set to '*' and an ID is
+ * set, then this should be used to find by ID,
+ * which will drastically reduce the amount of
+ * comparison operations done in PHP.
+ *
+ */
+ protected function initialMatchOnID($selector, $matches) {
+ $id = $selector->id;
+ $found = $this->newMatches();
+
+ // Issue #145: DOMXPath will through an exception if the DOM is
+ // not set.
+ if (!($this->dom instanceof \DOMDocument)) {
+ return $found;
+ }
+ $baseQuery = ".//*[@id='{$id}']";
+ $xpath = new \DOMXPath($this->dom);
+
+ // Now we try to find any matching IDs.
+ foreach ($matches as $node) {
+ if ($node->getAttribute('id') == $id) {
+ $found->attach($node);
+ }
+ $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
+ $this->attachNodeList($nl, $found);
+ }
+ // Unset the ID selector.
+ $selector->id = NULL;
+ return $found;
+ }
+
+ /**
+ * Shortcut for setting the intial match.
+ *
+ * This shortcut should only be used when the initial
+ * element is '*' and there are classes set.
+ *
+ * In any other case, the element finding algo is
+ * faster and should be used instead.
+ */
+ protected function initialMatchOnClasses($selector, $matches) {
+ $found = $this->newMatches();
+
+ // Issue #145: DOMXPath will through an exception if the DOM is
+ // not set.
+ if (!($this->dom instanceof \DOMDocument)) {
+ return $found;
+ }
+ $baseQuery = ".//*[@class]";
+ $xpath = new \DOMXPath($this->dom);
+
+ // Now we try to find any matching IDs.
+ foreach ($matches as $node) {
+ // Refactor me!
+ if ($node->hasAttribute('class')) {
+ $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class')));
+ if (count($intersect) == count($selector->classes)) {
+ $found->attach($node);
+ }
+ }
+
+ $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
+ foreach ($nl as $node) {
+ $classes = $node->getAttribute('class');
+ $classArray = explode(' ', $classes);
+
+ $intersect = array_intersect($selector->classes, $classArray);
+ if (count($intersect) == count($selector->classes)) {
+ $found->attach($node);
+ }
+ }
+ }
+
+ // Unset the classes selector.
+ $selector->classes = array();
+
+ return $found;
+ }
+
+ /**
+ * Internal xpath query.
+ *
+ * This is optimized for very specific use, and is not a general
+ * purpose function.
+ */
+ private function initialXpathQuery($xpath, $node, $query) {
+ // This works around a bug in which the document element
+ // does not correctly search with the $baseQuery.
+ if ($node->isSameNode($this->dom->documentElement)) {
+ $query = substr($query, 1);
+ }
+
+ return $xpath->query($query, $node);
+ }
+
+ /**
+ * Shortcut for setting the initial match.
+ */
+ protected function initialMatchOnElement($selector, $matches) {
+ $element = $selector->element;
+ if (is_null($element)) {
+ $element = '*';
+ }
+ $found = $this->newMatches();
+ foreach ($matches as $node) {
+ // Capture the case where the initial element is the root element.
+ if ($node->tagName == $element
+ || $element == '*' && $node->parentNode instanceof \DOMDocument) {
+ $found->attach($node);
+ }
+ $nl = $node->getElementsByTagName($element);
+ $this->attachNodeList($nl, $found);
+ }
+
+ $selector->element = NULL;
+ return $found;
+ }
+
+ /**
+ * Get elements and filter by namespace.
+ */
+ protected function initialMatchOnElementNS($selector, $matches) {
+ $ns = $selector->ns;
+
+ $elements = $this->initialMatchOnElement($selector, $matches);
+
+ // "any namespace" matches anything.
+ if ($ns == '*') {
+ return $elements;
+ }
+
+ // Loop through and make a list of items that need to be filtered
+ // out, then filter them. This is required b/c ObjectStorage iterates
+ // wrongly when an item is detached in an access loop.
+ $detach = array();
+ foreach ($elements as $node) {
+ // This lookup must be done PER NODE.
+ $nsuri = $node->lookupNamespaceURI($ns);
+ if (empty($nsuri) || $node->namespaceURI != $nsuri) {
+ $detach[] = $node;
+ }
+ }
+ foreach ($detach as $rem) {
+ $elements->detach($rem);
+ }
+ $selector->ns = NULL;
+ return $elements;
+ }
+
+ /**
+ * Checks to see if the DOMNode matches the given element selector.
+ *
+ * This handles the following cases:
+ *
+ * - element (foo)
+ * - namespaced element (ns|foo)
+ * - namespaced wildcard (ns|*)
+ * - wildcard (* or *|*)
+ */
+ protected function matchElement($node, $element, $ns = NULL) {
+ if (empty($element)) {
+ return TRUE;
+ }
+
+ // Handle namespace.
+ if (!empty($ns) && $ns != '*') {
+ // Check whether we have a matching NS URI.
+ $nsuri = $node->lookupNamespaceURI($ns);
+ if(empty($nsuri) || $node->namespaceURI !== $nsuri) {
+ return FALSE;
+ }
+ }
+
+ // Compare local name to given element name.
+ return $element == '*' || $node->localName == $element;
+ }
+
+ /**
+ * Checks to see if the given DOMNode matches an "any element" (*).
+ *
+ * This does not handle namespaced whildcards.
+ */
+ /*
+ protected function matchAnyElement($node) {
+ $ancestors = $this->ancestors($node);
+
+ return count($ancestors) > 0;
+ }
+ */
+
+ /**
+ * Get a list of ancestors to the present node.
+ */
+ protected function ancestors($node) {
+ $buffer = array();
+ $parent = $node;
+ while (($parent = $parent->parentNode) !== NULL) {
+ $buffer[] = $parent;
+ }
+ return $buffer;
+ }
+
+ /**
+ * Check to see if DOMNode has all of the given attributes.
+ *
+ * This can handle namespaced attributes, including namespace
+ * wildcards.
+ */
+ protected function matchAttributes($node, $attributes) {
+ if (empty($attributes)) {
+ return TRUE;
+ }
+
+ foreach($attributes as $attr) {
+ $val = isset($attr['value']) ? $attr['value'] : NULL;
+
+ // Namespaced attributes.
+ if (isset($attr['ns']) && $attr['ns'] != '*') {
+ $nsuri = $node->lookupNamespaceURI($attr['ns']);
+ if (empty($nsuri) || !$node->hasAttributeNS($nsuri, $attr['name'])) {
+ return FALSE;
+ }
+ $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']);
+ }
+ elseif (isset($attr['ns']) && $attr['ns'] == '*' && $node->hasAttributes()) {
+ // Cycle through all of the attributes in the node. Note that
+ // these are DOMAttr objects.
+ $matches = FALSE;
+ $name = $attr['name'];
+ foreach ($node->attributes as $attrNode) {
+ if ($attrNode->localName == $name) {
+ $nsuri = $attrNode->namespaceURI;
+ $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']);
+ }
+ }
+ }
+ // No namespace.
+ else {
+ $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']);
+ }
+
+ if (!$matches) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ /**
+ * Check that the given DOMNode has the given ID.
+ */
+ protected function matchId($node, $id) {
+ if (empty($id)) {
+ return TRUE;
+ }
+ return $node->hasAttribute('id') && $node->getAttribute('id') == $id;
+ }
+ /**
+ * Check that the given DOMNode has all of the given classes.
+ */
+ protected function matchClasses($node, $classes) {
+ if (empty($classes)) {
+ return TRUE;
+ }
+
+ if (!$node->hasAttribute('class')) {
+ return FALSE;
+ }
+
+ $eleClasses = preg_split('/\s+/', $node->getAttribute('class'));
+ if (empty($eleClasses)) {
+ return FALSE;
+ }
+
+ // The intersection should match the given $classes.
+ $missing = array_diff($classes, array_intersect($classes, $eleClasses));
+
+ return count($missing) == 0;
+ }
+ protected function matchPseudoClasses($node, $pseudoClasses) {
+ $ret = TRUE;
+ foreach ($pseudoClasses as $pseudoClass) {
+ $name = $pseudoClass['name'];
+ // Avoid E_STRICT violation.
+ $value = isset($pseudoClass['value']) ? $pseudoClass['value'] : NULL;
+ $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
+ }
+ return $ret;
+ }
+ /**
+ * Test whether the given node matches the pseudoElements.
+ *
+ * If any pseudo-elements are passed, this will test to see
+ * <i>if conditions obtain that would allow the pseudo-element
+ * to be created</i>. This does not modify the match in any way.
+ */
+ protected function matchPseudoElements($node, $pseudoElements) {
+ if (empty($pseudoElements)) {
+ return TRUE;
+ }
+
+ foreach ($pseudoElements as $pse) {
+ switch ($pse) {
+ case 'first-line':
+ case 'first-letter':
+ case 'before':
+ case 'after':
+ return strlen($node->textContent) > 0;
+ case 'selection':
+ throw new \QueryPath\CSS\NotImplementedException("::$name is not implemented.");
+ }
+ }
+ }
+
+ protected function newMatches() {
+ return new \SplObjectStorage();
+ }
+
+ /**
+ * Get the internal match set.
+ * Internal utility function.
+ */
+ protected function getMatches() {
+ return $this->matches();
+ }
+
+ /**
+ * Set the internal match set.
+ *
+ * Internal utility function.
+ */
+ protected function setMatches($matches) {
+ $this->matches = $matches;
+ }
+
+ /**
+ * Attach all nodes in a node list to the given \SplObjectStorage.
+ */
+ public function attachNodeList(\DOMNodeList $nodeList, \SplObjectStorage $splos) {
+ foreach ($nodeList as $item) $splos->attach($item);
+ }
+
+ public function getDocument() {
+ return $this->dom;
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/DOMTraverser/PseudoClass.php b/lib/querypath/src/QueryPath/CSS/DOMTraverser/PseudoClass.php
new file mode 100644
index 0000000..0bcaf79
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/DOMTraverser/PseudoClass.php
@@ -0,0 +1,421 @@
+<?php
+/**
+ * @file
+ *
+ * PseudoClass class.
+ *
+ * This is the first pass in an experiment to break PseudoClass handling
+ * out of the normal traversal. Eventually, this should become a
+ * top-level pluggable registry that will allow custom pseudoclasses.
+ * For now, though, we just handle the core pseudoclasses.
+ */
+namespace QueryPath\CSS\DOMTraverser;
+
+use \QueryPath\CSS\NotImplementedException;
+use \QueryPath\CSS\EventHandler;
+/**
+ * The PseudoClass handler.
+ *
+ */
+class PseudoClass {
+
+ /**
+ * Tests whether the given element matches the given pseudoclass.
+ *
+ * @param string $pseudoclass
+ * The string name of the pseudoclass
+ * @param resource $node
+ * The DOMNode to be tested.
+ * @param resource $scope
+ * The DOMElement that is the active root for this node.
+ * @param mixed $value
+ * The optional value string provided with this class. This is
+ * used, for example, in an+b psuedoclasses.
+ * @retval boolean
+ * TRUE if the node matches, FALSE otherwise.
+ */
+ public function elementMatches($pseudoclass, $node, $scope, $value = NULL) {
+ $name = strtolower($pseudoclass);
+ // Need to handle known pseudoclasses.
+ switch($name) {
+ case 'current':
+ case 'past':
+ case 'future':
+ case 'visited':
+ case 'hover':
+ case 'active':
+ case 'focus':
+ case 'animated': // Last 3 are from jQuery
+ case 'visible':
+ case 'hidden':
+ // These require a UA, which we don't have.
+ case 'valid':
+ case 'invalid':
+ case 'required':
+ case 'optional':
+ case 'read-only':
+ case 'read-write':
+ // Since we don't know how to validate elements,
+ // we can't supply these.
+ case 'dir':
+ // FIXME: I don't know how to get directionality info.
+ case 'nth-column':
+ case 'nth-last-column':
+ // We don't know what a column is in most documents.
+ // FIXME: Can we do this for HTML?
+ case 'target':
+ // This requires a location URL, which we don't have.
+ return FALSE;
+ case 'indeterminate':
+ // Because sometimes screwing with people is fun.
+ return (boolean) mt_rand(0, 1);
+ case 'lang':
+ // No value = exception.
+ if (!isset($value)) {
+ throw new NotImplementedException(":lang() requires a value.");
+ }
+ return $this->lang($node, $value);
+ case 'any-link':
+ return Util::matchesAttribute($node, 'href')
+ || Util::matchesAttribute($node, 'src')
+ || Util::matchesAttribute($node, 'link');
+ case 'link':
+ return Util::matchesAttribute($node, 'href');
+ case 'local-link':
+ return $this->isLocalLink($node);
+ case 'root':
+ return $node->isSameNode($node->ownerDocument->documentElement);
+
+ // CSS 4 declares the :scope pseudo-class, which describes what was
+ // the :x-root QueryPath extension.
+ case 'x-root':
+ case 'x-reset':
+ case 'scope':
+ return $node->isSameNode($scope);
+ // NON-STANDARD extensions for simple support of even and odd. These
+ // are supported by jQuery, FF, and other user agents.
+ case 'even':
+ return $this->isNthChild($node, 'even');
+ case 'odd':
+ return $this->isNthChild($node, 'odd');
+ case 'nth-child':
+ return $this->isNthChild($node, $value);
+ case 'nth-last-child':
+ return $this->isNthChild($node, $value, TRUE);
+ case 'nth-of-type':
+ return $this->isNthChild($node, $value, FALSE, TRUE);
+ case 'nth-last-of-type':
+ return $this->isNthChild($node, $value, TRUE, TRUE);
+ case 'first-of-type':
+ return $this->isFirstOfType($node);
+ case 'last-of-type':
+ return $this->isLastOfType($node);
+ case 'only-of-type':
+ return $this->isFirstOfType($node) && $this->isLastOfType($node);
+
+ // Additional pseudo-classes defined in jQuery:
+ case 'lt':
+ // I'm treating this as "less than or equal to".
+ $rule = sprintf('-n + %d', (int) $value);
+ // $rule = '-n+15';
+ return $this->isNthChild($node, $rule);
+ case 'gt':
+ // I'm treating this as "greater than"
+ // return $this->nodePositionFromEnd($node) > (int) $value;
+ return $this->nodePositionFromStart($node) > (int) $value;
+ case 'nth':
+ case 'eq':
+ $rule = (int)$value;
+ return $this->isNthChild($node, $rule);
+ case 'first':
+ return $this->isNthChild($node, 1);
+ case 'first-child':
+ return $this->isFirst($node);
+ case 'last':
+ case 'last-child':
+ return $this->isLast($node);
+ case 'only-child':
+ return $this->isFirst($node) && $this->isLast($node);
+ case 'empty':
+ return $this->isEmpty($node);
+ case 'parent':
+ return !$this->isEmpty($node);
+
+ case 'enabled':
+ case 'disabled':
+ case 'checked':
+ return Util::matchesAttribute($node, $name);
+ case 'text':
+ case 'radio':
+ case 'checkbox':
+ case 'file':
+ case 'password':
+ case 'submit':
+ case 'image':
+ case 'reset':
+ case 'button':
+ return Util::matchesAttribute($node, 'type', $name);
+
+ case 'header':
+ return $this->header($node);
+ case 'has':
+ case 'matches':
+ return $this->has($node, $value);
+ break;
+ case 'not':
+ if (empty($value)) {
+ throw new ParseException(":not() requires a value.");
+ }
+ return $this->isNot($node, $value);
+ // Contains == text matches.
+ // In QP 2.1, this was changed.
+ case 'contains':
+ return $this->contains($node, $value);
+ // Since QP 2.1
+ case 'contains-exactly':
+ return $this->containsExactly($node, $value);
+ default:
+ throw new \QueryPath\CSS\ParseException("Unknown Pseudo-Class: " . $name);
+ }
+ $this->findAnyElement = FALSE;
+ }
+
+ /**
+ * Pseudo-class handler for :lang
+ *
+ * Note that this does not implement the spec in its entirety because we do
+ * not presume to "know the language" of the document. If anyone is interested
+ * in making this more intelligent, please do so.
+ */
+ protected function lang($node, $value) {
+ // TODO: This checks for cases where an explicit language is
+ // set. The spec seems to indicate that an element should inherit
+ // language from the parent... but this is unclear.
+ $operator = (strpos($value, '-') !== FALSE) ? EventHandler::isExactly : EventHandler::containsWithHyphen;
+
+ $match = TRUE;
+ foreach ($node->attributes as $attrNode) {
+ if ($attrNode->localName == 'lang') {
+
+ if ($attrNode->nodeName == $attrNode->localName) {
+ // fprintf(STDOUT, "%s in NS %s\n", $attrNode->name, $attrNode->nodeName);
+ return Util::matchesAttribute($node, 'lang', $value, $operator);
+ }
+ else {
+ $nsuri = $attrNode->namespaceURI;
+ // fprintf(STDOUT, "%s in NS %s\n", $attrNode->name, $nsuri);
+ return Util::matchesAttributeNS($node, 'lang', $nsuri, $value, $operator);
+ }
+
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Provides jQuery pseudoclass ':header'.
+ */
+ protected function header($node) {
+ return preg_match('/^h[1-9]$/i', $node->tagName) == 1;
+ }
+
+ /**
+ * Provides pseudoclass :empty.
+ */
+ protected function isEmpty($node) {
+ foreach ($node->childNodes as $kid) {
+ // We don't want to count PIs and comments. From the spec, it
+ // appears that CDATA is also not counted.
+ if ($kid->nodeType == XML_ELEMENT_NODE || $kid->nodeType == XML_TEXT_NODE) {
+ // As soon as we hit a FALSE, return.
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Provides jQuery pseudoclass :first.
+ *
+ * @todo
+ * This can be replaced by isNthChild().
+ */
+ protected function isFirst($node) {
+ while (isset($node->previousSibling)) {
+ $node = $node->previousSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ /**
+ * Fast version of first-of-type.
+ */
+ protected function isFirstOfType($node) {
+ $type = $node->tagName;
+ while (isset($node->previousSibling)) {
+ $node = $node->previousSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE && $node->tagName == $type) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ /**
+ * Fast version of jQuery :last.
+ */
+ protected function isLast($node) {
+ while (isset($node->nextSibling)) {
+ $node = $node->nextSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ /**
+ * Provides last-of-type.
+ */
+ protected function isLastOfType($node) {
+ $type = $node->tagName;
+ while (isset($node->nextSibling)) {
+ $node = $node->nextSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE && $node->tagName == $type) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+ /**
+ * Provides :contains() as the original spec called for.
+ *
+ * This is an INEXACT match.
+ */
+ protected function contains($node, $value) {
+ $text = $node->textContent;
+ $value = Util::removeQuotes($value);
+ return isset($text) && (stripos($text, $value) !== FALSE);
+ }
+ /**
+ * Provides :contains-exactly QueryPath pseudoclass.
+ *
+ * This is an EXACT match.
+ */
+ protected function containsExactly($node, $value) {
+ $text = $node->textContent;
+ $value = Util::removeQuotes($value);
+ return isset($text) && $text == $value;
+ }
+
+ /**
+ * Provides :has pseudoclass.
+ */
+ protected function has($node, $selector) {
+ $splos = new \SPLObjectStorage();
+ $splos->attach($node);
+ $traverser = new \QueryPath\CSS\DOMTraverser($splos, TRUE);
+ $results = $traverser->find($selector)->matches();
+ return count($results) > 0;
+ }
+
+ /**
+ * Provides :not pseudoclass.
+ */
+ protected function isNot($node, $selector) {
+ return !$this->has($node, $selector);
+ }
+
+ /**
+ * Get the relative position of a node in its sibling set.
+ */
+ protected function nodePositionFromStart($node, $byType = FALSE) {
+ $i = 1;
+ $tag = $node->tagName;
+ while (isset($node->previousSibling)) {
+ $node = $node->previousSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE && (!$byType || $node->tagName == $tag)) {
+ ++$i;
+ }
+ }
+ return $i;
+ }
+ /**
+ * Get the relative position of a node in its sibling set.
+ */
+ protected function nodePositionFromEnd($node, $byType = FALSE) {
+ $i = 1;
+ $tag = $node->tagName;
+ while (isset($node->nextSibling)) {
+ $node = $node->nextSibling;
+ if ($node->nodeType == XML_ELEMENT_NODE && (!$byType || $node->tagName == $tag)) {
+ ++$i;
+ }
+ }
+ return $i;
+ }
+
+ /**
+ * Provides functionality for all "An+B" rules.
+ * Provides nth-child and also the functionality required for:
+ *
+ *- nth-last-child
+ *- even
+ *- odd
+ *- first
+ *- last
+ *- eq
+ *- nth
+ *- nth-of-type
+ *- first-of-type
+ *- last-of-type
+ *- nth-last-of-type
+ *
+ * See also QueryPath::CSS::DOMTraverser::Util::parseAnB().
+ */
+ protected function isNthChild($node, $value, $reverse = FALSE, $byType = FALSE) {
+ list($groupSize, $elementInGroup) = Util::parseAnB($value);
+ $parent = $node->parentNode;
+ if (empty($parent)
+ || ($groupSize == 0 && $elementInGroup == 0)
+ || ($groupSize > 0 && $elementInGroup > $groupSize)
+ ) {
+ return FALSE;
+ }
+
+ // First we need to find the position of $node in other elements.
+ if ($reverse) {
+ $pos = $this->nodePositionFromEnd($node, $byType);
+ }
+ else {
+ $pos = $this->nodePositionFromStart($node, $byType);
+ }
+
+ // If group size is 0, we just check to see if this
+ // is the nth element:
+ if ($groupSize == 0) {
+ return $pos == $elementInGroup;
+ }
+
+ // Next, we normalize $elementInGroup
+ if ($elementInGroup < 0) {
+ $elementInGroup = $groupSize + $elementInGroup;
+ }
+
+
+ $prod = ($pos - $elementInGroup) / $groupSize;
+ // fprintf(STDOUT, "%d n + %d on %d is %3.5f\n", $groupSize, $elementInGroup, $pos, $prod);
+
+ return is_int($prod) && $prod >= 0;
+ }
+
+ protected function isLocalLink($node) {
+ if (!$node->hasAttribute('href')) {
+ return FALSE;
+ }
+ $url = $node->getAttribute('href');
+ $scheme = parse_url($url, PHP_URL_SCHEME);
+ return empty($scheme) || $scheme == 'file';
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/DOMTraverser/Util.php b/lib/querypath/src/QueryPath/CSS/DOMTraverser/Util.php
new file mode 100644
index 0000000..ec01d8f
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/DOMTraverser/Util.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * @file
+ *
+ * Utilities for DOM traversal.
+ */
+namespace QueryPath\CSS\DOMTraverser;
+
+use \QueryPath\CSS\EventHandler;
+
+/**
+ * Utilities for DOM Traversal.
+ */
+class Util {
+
+ /**
+ * Check whether the given DOMElement has the given attribute.
+ */
+ public static function matchesAttribute($node, $name, $value = NULL, $operation = EventHandler::isExactly) {
+ if (!$node->hasAttribute($name)) {
+ return FALSE;
+ }
+
+ if (is_null($value)) {
+ return TRUE;
+ }
+
+ return self::matchesAttributeValue($value, $node->getAttribute($name), $operation);
+ }
+ /**
+ * Check whether the given DOMElement has the given namespaced attribute.
+ */
+ public static function matchesAttributeNS($node, $name, $nsuri, $value = NULL, $operation = EventHandler::isExactly) {
+ if (!$node->hasAttributeNS($nsuri, $name)) {
+ return FALSE;
+ }
+
+ if (is_null($value)) {
+ return TRUE;
+ }
+
+ return self::matchesAttributeValue($value, $node->getAttributeNS($nsuri, $name), $operation);
+ }
+
+ /**
+ * Check for attr value matches based on an operation.
+ */
+ public static function matchesAttributeValue($needle, $haystack, $operation) {
+
+ if (strlen($haystack) < strlen($needle)) return FALSE;
+
+ // According to the spec:
+ // "The case-sensitivity of attribute names in selectors depends on the document language."
+ // (6.3.2)
+ // To which I say, "huh?". We assume case sensitivity.
+ switch ($operation) {
+ case EventHandler::isExactly:
+ return $needle == $haystack;
+ case EventHandler::containsWithSpace:
+ // XXX: This needs testing!
+ return preg_match('/\b/', $haystack) == 1;
+ //return in_array($needle, explode(' ', $haystack));
+ case EventHandler::containsWithHyphen:
+ return in_array($needle, explode('-', $haystack));
+ case EventHandler::containsInString:
+ return strpos($haystack, $needle) !== FALSE;
+ case EventHandler::beginsWith:
+ return strpos($haystack, $needle) === 0;
+ case EventHandler::endsWith:
+ //return strrpos($haystack, $needle) === strlen($needle) - 1;
+ return preg_match('/' . $needle . '$/', $haystack) == 1;
+ }
+ return FALSE; // Shouldn't be able to get here.
+ }
+
+ /**
+ * Remove leading and trailing quotes.
+ */
+ public static function removeQuotes($str) {
+ $f = substr($str, 0, 1);
+ $l = substr($str, -1);
+ if ($f === $l && ($f == '"' || $f == "'")) {
+ $str = substr($str, 1, -1);
+ }
+ return $str;
+ }
+
+ /**
+ * Parse an an+b rule for CSS pseudo-classes.
+ *
+ * Invalid rules return `array(0, 0)`. This is per the spec.
+ *
+ * @param $rule
+ * Some rule in the an+b format.
+ * @retval array
+ * `array($aVal, $bVal)` of the two values.
+ */
+ public static function parseAnB($rule) {
+ if ($rule == 'even') {
+ return array(2, 0);
+ }
+ elseif ($rule == 'odd') {
+ return array(2, 1);
+ }
+ elseif ($rule == 'n') {
+ return array(1, 0);
+ }
+ elseif (is_numeric($rule)) {
+ return array(0, (int)$rule);
+ }
+
+ $regex = '/^\s*([+\-]?[0-9]*)n\s*([+\-]?)\s*([0-9]*)\s*$/';
+ $matches = array();
+ $res = preg_match($regex, $rule, $matches);
+
+ // If it doesn't parse, return 0, 0.
+ if (!$res) {
+ return array(0, 0);
+ }
+
+ $aVal = isset($matches[1]) ? $matches[1] : 1;
+ if ($aVal == '-') {
+ $aVal = -1;
+ }
+ else {
+ $aVal = (int) $aVal;
+ }
+
+ $bVal = 0;
+ if (isset($matches[3])) {
+ $bVal = (int) $matches[3];
+ if (isset($matches[2]) && $matches[2] == '-') {
+ $bVal *= -1;
+ }
+ }
+ return array($aVal, $bVal);
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/EventHandler.php b/lib/querypath/src/QueryPath/CSS/EventHandler.php
new file mode 100644
index 0000000..a003a0a
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/EventHandler.php
@@ -0,0 +1,171 @@
+<?php
+/** @file
+ * CSS selector parsing classes.
+ *
+ * This file contains the tools necessary for parsing CSS 3 selectors.
+ * In the future it may be expanded to handle all of CSS 3.
+ *
+ * The parser contained herein is has an event-based API. Implementors should
+ * begin by implementing the {@link EventHandler} interface. For an example
+ * of how this is done, see {@link EventHandler.php}.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license MIT
+ */
+namespace QueryPath\CSS;
+
+/** @addtogroup querypath_css CSS Parsing
+ * QueryPath includes a CSS 3 Selector parser.
+ *
+ *
+ * Typically the parser is not accessed directly. Most developers will use it indirectly from
+ * qp(), htmlqp(), or one of the methods on a QueryPath object.
+ *
+ * This parser is modular and is not tied to QueryPath, so you can use it in your
+ * own (non-QueryPath) projects if you wish. To dive in, start with EventHandler, the
+ * event interface that works like a SAX API for CSS selectors. If you want to check out
+ * the details, check out the parser (QueryPath::CSS::Parser), scanner
+ * (QueryPath::CSS::Scanner), and token list (QueryPath::CSS::Token).
+ */
+
+/**
+ * An event handler for handling CSS 3 Selector parsing.
+ *
+ * This provides a standard interface for CSS 3 Selector event handling. As the
+ * parser parses a selector, it will fire events. Implementations of EventHandler
+ * can then handle the events.
+ *
+ * This library is inspired by the SAX2 API for parsing XML. Each component of a
+ * selector fires an event, passing the necessary data on to the event handler.
+ *
+ * @ingroup querypath_css
+ */
+interface EventHandler {
+ /** The is-exactly (=) operator. */
+ const isExactly = 0; // =
+ /** The contains-with-space operator (~=). */
+ const containsWithSpace = 1; // ~=
+ /** The contains-with-hyphen operator (!=). */
+ const containsWithHyphen = 2; // |=
+ /** The contains-in-string operator (*=). */
+ const containsInString = 3; // *=
+ /** The begins-with operator (^=). */
+ const beginsWith = 4; // ^=
+ /** The ends-with operator ($=). */
+ const endsWith = 5; // $=
+ /** The any-element operator (*). */
+ const anyElement = '*';
+
+ /**
+ * This event is fired when a CSS ID is encountered.
+ * An ID begins with an octothorp: #name.
+ *
+ * @param string $id
+ * The ID passed in.
+ */
+ public function elementID($id); // #name
+ /**
+ * Handle an element name.
+ * Example: name
+ * @param string $name
+ * The name of the element.
+ */
+ public function element($name); // name
+ /**
+ * Handle a namespaced element name.
+ * example: namespace|name
+ * @param string $name
+ * The tag name.
+ * @param string $namespace
+ * The namespace identifier (Not the URI)
+ */
+ public function elementNS($name, $namespace = NULL);
+ /**
+ * Handle an any-element (*) operator.
+ * Example: *
+ */
+ public function anyElement(); // *
+ /**
+ * Handle an any-element operator that is constrained to a namespace.
+ * Example: ns|*
+ * @param string $ns
+ * The namespace identifier (not the URI).
+ */
+ public function anyElementInNS($ns); // ns|*
+ /**
+ * Handle a CSS class selector.
+ * Example: .name
+ * @param string $name
+ * The name of the class.
+ */
+ public function elementClass($name); // .name
+ /**
+ * Handle an attribute selector.
+ * Example: [name=attr]
+ * Example: [name~=attr]
+ * @param string $name
+ * The attribute name.
+ * @param string $value
+ * The value of the attribute, if given.
+ * @param int $operation
+ * The operation to be used for matching. See {@link EventHandler}
+ * constants for a list of supported operations.
+ */
+ public function attribute($name, $value = NULL, $operation = EventHandler::isExactly); // [name=attr]
+ /**
+ * Handle an attribute selector bound to a specific namespace.
+ * Example: [ns|name=attr]
+ * Example: [ns|name~=attr]
+ * @param string $name
+ * The attribute name.
+ * @param string $ns
+ * The namespace identifier (not the URI).
+ * @param string $value
+ * The value of the attribute, if given.
+ * @param int $operation
+ * The operation to be used for matching. See {@link EventHandler}
+ * constants for a list of supported operations.
+ */
+ public function attributeNS($name, $ns, $value = NULL, $operation = EventHandler::isExactly);
+ /**
+ * Handle a pseudo-class.
+ * Example: :name(value)
+ * @param string $name
+ * The pseudo-class name.
+ * @param string $value
+ * The value, if one is found.
+ */
+ public function pseudoClass($name, $value = NULL); //:name(value)
+ /**
+ * Handle a pseudo-element.
+ * Example: ::name
+ * @param string $name
+ * The pseudo-element name.
+ */
+ public function pseudoElement($name); // ::name
+ /**
+ * Handle a direct descendant combinator.
+ * Example: >
+ */
+ public function directDescendant(); // >
+ /**
+ * Handle a adjacent combinator.
+ * Example: +
+ */
+ public function adjacent(); // +
+ /**
+ * Handle an another-selector combinator.
+ * Example: ,
+ */
+ public function anotherSelector(); // ,
+ /**
+ * Handle a sibling combinator.
+ * Example: ~
+ */
+ public function sibling(); // ~ combinator
+ /**
+ * Handle an any-descendant combinator.
+ * Example: ' '
+ */
+ public function anyDescendant(); // ' ' (space) operator.
+}
diff --git a/lib/querypath/src/QueryPath/CSS/InputStream.php b/lib/querypath/src/QueryPath/CSS/InputStream.php
new file mode 100644
index 0000000..2967845
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/InputStream.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * @file
+ *
+ * The CSS Input Stream abstraction.
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * Simple wrapper to turn a string into an input stream.
+ * This provides a standard interface on top of an array of
+ * characters.
+ */
+class InputStream {
+ protected $stream = NULL;
+ public $position = 0;
+ /**
+ * Build a new CSS input stream from a string.
+ *
+ * @param string
+ * String to turn into an input stream.
+ */
+ function __construct($string) {
+ $this->stream = str_split($string);
+ }
+ /**
+ * Look ahead one character.
+ *
+ * @return char
+ * Returns the next character, but does not remove it from
+ * the stream.
+ */
+ function peek() {
+ return $this->stream[0];
+ }
+ /**
+ * Get the next unconsumed character in the stream.
+ * This will remove that character from the front of the
+ * stream and return it.
+ */
+ function consume() {
+ $ret = array_shift($this->stream);
+ if (!empty($ret)) {
+ $this->position++;
+ }
+ return $ret;
+ }
+ /**
+ * Check if the stream is empty.
+ * @return boolean
+ * Returns TRUE when the stream is empty, FALSE otherwise.
+ */
+ function isEmpty() {
+ return count($this->stream) == 0;
+ }
+}
diff --git a/lib/querypath/src/QueryPath/CSS/NotImplementedException.php b/lib/querypath/src/QueryPath/CSS/NotImplementedException.php
new file mode 100644
index 0000000..6705f30
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/NotImplementedException.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * @file
+ * An exception for CSS errors.
+ */
+namespace QueryPath\CSS;
+/**
+ * Exception thrown for unimplemented CSS.
+ *
+ * This is thrown in cases where some feature is expected, but the current
+ * implementation does not support that feature.
+ *
+ * @ingroup querypath_css
+ */
+class NotImplementedException extends \Exception {}
diff --git a/lib/querypath/src/QueryPath/CSS/ParseException.php b/lib/querypath/src/QueryPath/CSS/ParseException.php
new file mode 100644
index 0000000..957857a
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/ParseException.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * @file
+ *
+ * The CSS parsing exception class.
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * Exception indicating an error in CSS parsing.
+ *
+ * @ingroup querypath_css
+ */
+class ParseException extends \QueryPath\Exception {}
diff --git a/lib/querypath/src/QueryPath/CSS/Parser.php b/lib/querypath/src/QueryPath/CSS/Parser.php
new file mode 100644
index 0000000..f041612
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/Parser.php
@@ -0,0 +1,575 @@
+<?php
+/**
+ * @file
+ *
+ * The CSS parser
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * Parse a CSS selector.
+ *
+ * In CSS, a selector is used to identify which element or elements
+ * in a DOM are being selected for the application of a particular style.
+ * Effectively, selectors function as a query language for a structured
+ * document -- almost always HTML or XML.
+ *
+ * This class provides an event-based parser for CSS selectors. It can be
+ * used, for example, as a basis for writing a DOM query engine based on
+ * CSS.
+ *
+ * @ingroup querypath_css
+ */
+class Parser {
+ protected $scanner = NULL;
+ protected $buffer = '';
+ protected $handler = NULL;
+ protected $strict = FALSE;
+
+ protected $DEBUG = FALSE;
+
+ /**
+ * Construct a new CSS parser object. This will attempt to
+ * parse the string as a CSS selector. As it parses, it will
+ * send events to the EventHandler implementation.
+ */
+ public function __construct($string, EventHandler $handler) {
+ $this->originalString = $string;
+ $is = new InputStream($string);
+ $this->scanner = new Scanner($is);
+ $this->handler = $handler;
+ }
+
+ /**
+ * Parse the selector.
+ *
+ * This begins an event-based parsing process that will
+ * fire events as the selector is handled. A EventHandler
+ * implementation will be responsible for handling the events.
+ * @throws ParseException
+ */
+ public function parse() {
+
+ $this->scanner->nextToken();
+ while ($this->scanner->token !== FALSE) {
+ // Primitive recursion detection.
+ $position = $this->scanner->position();
+
+ if ($this->DEBUG) {
+ print "PARSE " . $this->scanner->token. "\n";
+ }
+ $this->selector();
+
+ $finalPosition = $this->scanner->position();
+
+ if ($this->scanner->token !== FALSE && $finalPosition == $position) {
+ // If we get here, then the scanner did not pop a single character
+ // off of the input stream during a full run of the parser, which
+ // means that the current input does not match any recognizable
+ // pattern.
+ throw new ParseException('CSS selector is not well formed.');
+ }
+
+ }
+
+ }
+
+ /**
+ * A restricted parser that can only parse simple selectors.
+ * The pseudoClass handler for this parser will throw an
+ * exception if it encounters a pseudo-element or the
+ * negation pseudo-class.
+ *
+ * @deprecated This is not used anywhere in QueryPath and
+ * may be removed.
+ *//*
+ public function parseSimpleSelector() {
+ while ($this->scanner->token !== FALSE) {
+ if ($this->DEBUG) print "SIMPLE SELECTOR\n";
+ $this->allElements();
+ $this->elementName();
+ $this->elementClass();
+ $this->elementID();
+ $this->pseudoClass(TRUE); // Operate in restricted mode.
+ $this->attribute();
+
+ // TODO: Need to add failure conditions here.
+ }
+ }*/
+
+ /**
+ * Handle an entire CSS selector.
+ */
+ private function selector() {
+ if ($this->DEBUG) print "SELECTOR{$this->scanner->position()}\n";
+ $this->consumeWhitespace(); // Remove leading whitespace
+ $this->simpleSelectors();
+ $this->combinator();
+ }
+
+ /**
+ * Consume whitespace and return a count of the number of whitespace consumed.
+ */
+ private function consumeWhitespace() {
+ if ($this->DEBUG) print "CONSUME WHITESPACE\n";
+ $white = 0;
+ while ($this->scanner->token == Token::white) {
+ $this->scanner->nextToken();
+ ++$white;
+ }
+ return $white;
+ }
+
+ /**
+ * Handle one of the five combinators: '>', '+', ' ', '~', and ','.
+ * This will call the appropriate event handlers.
+ * @see EventHandler::directDescendant(),
+ * @see EventHandler::adjacent(),
+ * @see EventHandler::anyDescendant(),
+ * @see EventHandler::anotherSelector().
+ */
+ private function combinator() {
+ if ($this->DEBUG) print "COMBINATOR\n";
+ /*
+ * Problem: ' ' and ' > ' are both valid combinators.
+ * So we have to track whitespace consumption to see
+ * if we are hitting the ' ' combinator or if the
+ * selector just has whitespace padding another combinator.
+ */
+
+ // Flag to indicate that post-checks need doing
+ $inCombinator = FALSE;
+ $white = $this->consumeWhitespace();
+ $t = $this->scanner->token;
+
+ if ($t == Token::rangle) {
+ $this->handler->directDescendant();
+ $this->scanner->nextToken();
+ $inCombinator = TRUE;
+ //$this->simpleSelectors();
+ }
+ elseif ($t == Token::plus) {
+ $this->handler->adjacent();
+ $this->scanner->nextToken();
+ $inCombinator = TRUE;
+ //$this->simpleSelectors();
+ }
+ elseif ($t == Token::comma) {
+ $this->handler->anotherSelector();
+ $this->scanner->nextToken();
+ $inCombinator = TRUE;
+ //$this->scanner->selectors();
+ }
+ elseif ($t == Token::tilde) {
+ $this->handler->sibling();
+ $this->scanner->nextToken();
+ $inCombinator = TRUE;
+ }
+
+ // Check that we don't get two combinators in a row.
+ if ($inCombinator) {
+ $white = 0;
+ if ($this->DEBUG) print "COMBINATOR: " . Token::name($t) . "\n";
+ $this->consumeWhitespace();
+ if ($this->isCombinator($this->scanner->token)) {
+ throw new ParseException("Illegal combinator: Cannot have two combinators in sequence.");
+ }
+ }
+ // Check to see if we have whitespace combinator:
+ elseif ($white > 0) {
+ if ($this->DEBUG) print "COMBINATOR: any descendant\n";
+ $inCombinator = TRUE;
+ $this->handler->anyDescendant();
+ }
+ else {
+ if ($this->DEBUG) print "COMBINATOR: no combinator found.\n";
+ }
+ }
+
+ /**
+ * Check if the token is a combinator.
+ */
+ private function isCombinator($tok) {
+ $combinators = array(Token::plus, Token::rangle, Token::comma, Token::tilde);
+ return in_array($tok, $combinators);
+ }
+
+ /**
+ * Handle a simple selector.
+ */
+ private function simpleSelectors() {
+ if ($this->DEBUG) print "SIMPLE SELECTOR\n";
+ $this->allElements();
+ $this->elementName();
+ $this->elementClass();
+ $this->elementID();
+ $this->pseudoClass();
+ $this->attribute();
+ }
+
+ /**
+ * Handles CSS ID selectors.
+ * This will call EventHandler::elementID().
+ */
+ private function elementID() {
+ if ($this->DEBUG) print "ELEMENT ID\n";
+ if ($this->scanner->token == Token::octo) {
+ $this->scanner->nextToken();
+ if ($this->scanner->token !== Token::char) {
+ throw new ParseException("Expected string after #");
+ }
+ $id = $this->scanner->getNameString();
+ $this->handler->elementID($id);
+ }
+ }
+
+ /**
+ * Handles CSS class selectors.
+ * This will call the EventHandler::elementClass() method.
+ */
+ private function elementClass() {
+ if ($this->DEBUG) print "ELEMENT CLASS\n";
+ if ($this->scanner->token == Token::dot) {
+ $this->scanner->nextToken();
+ $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error.
+ $cssClass = $this->scanner->getNameString();
+ $this->handler->elementClass($cssClass);
+ }
+ }
+
+ /**
+ * Handle a pseudo-class and pseudo-element.
+ *
+ * CSS 3 selectors support separate pseudo-elements, using :: instead
+ * of : for separator. This is now supported, and calls the pseudoElement
+ * handler, EventHandler::pseudoElement().
+ *
+ * This will call EventHandler::pseudoClass() when a
+ * pseudo-class is parsed.
+ */
+ private function pseudoClass($restricted = FALSE) {
+ if ($this->DEBUG) print "PSEUDO-CLASS\n";
+ if ($this->scanner->token == Token::colon) {
+
+ // Check for CSS 3 pseudo element:
+ $isPseudoElement = FALSE;
+ if ($this->scanner->nextToken() === Token::colon) {
+ $isPseudoElement = TRUE;
+ $this->scanner->nextToken();
+ }
+
+ $name = $this->scanner->getNameString();
+ if ($restricted && $name == 'not') {
+ throw new ParseException("The 'not' pseudo-class is illegal in this context.");
+ }
+
+ $value = NULL;
+ if ($this->scanner->token == Token::lparen) {
+ if ($isPseudoElement) {
+ throw new ParseException("Illegal left paren. Pseudo-Element cannot have arguments.");
+ }
+ $value = $this->pseudoClassValue();
+ }
+
+ // FIXME: This should throw errors when pseudo element has values.
+ if ($isPseudoElement) {
+ if ($restricted) {
+ throw new ParseException("Pseudo-Elements are illegal in this context.");
+ }
+ $this->handler->pseudoElement($name);
+ $this->consumeWhitespace();
+
+ // Per the spec, pseudo-elements must be the last items in a selector, so we
+ // check to make sure that we are either at the end of the stream or that a
+ // new selector is starting. Only one pseudo-element is allowed per selector.
+ if ($this->scanner->token !== FALSE && $this->scanner->token !== Token::comma) {
+ throw new ParseException("A Pseudo-Element must be the last item in a selector.");
+ }
+ }
+ else {
+ $this->handler->pseudoClass($name, $value);
+ }
+ }
+ }
+
+ /**
+ * Get the value of a pseudo-classes.
+ *
+ * @return string
+ * Returns the value found from a pseudo-class.
+ *
+ * @todo Pseudoclasses can be passed pseudo-elements and
+ * other pseudo-classes as values, which means :pseudo(::pseudo)
+ * is legal.
+ */
+ private function pseudoClassValue() {
+ if ($this->scanner->token == Token::lparen) {
+ $buf = '';
+
+ // For now, just leave pseudoClass value vague.
+ /*
+ // We have to peek to see if next char is a colon because
+ // pseudo-classes and pseudo-elements are legal strings here.
+ print $this->scanner->peek();
+ if ($this->scanner->peek() == ':') {
+ print "Is pseudo\n";
+ $this->scanner->nextToken();
+
+ // Pseudo class
+ if ($this->scanner->token == Token::colon) {
+ $buf .= ':';
+ $this->scanner->nextToken();
+ // Pseudo element
+ if ($this->scanner->token == Token::colon) {
+ $buf .= ':';
+ $this->scanner->nextToken();
+ }
+ // Ident
+ $buf .= $this->scanner->getNameString();
+ }
+ }
+ else {
+ print "fetching string.\n";
+ $buf .= $this->scanner->getQuotedString();
+ if ($this->scanner->token != Token::rparen) {
+ $this->throwError(Token::rparen, $this->scanner->token);
+ }
+ $this->scanner->nextToken();
+ }
+ return $buf;
+ */
+ //$buf .= $this->scanner->getQuotedString();
+ $buf .= $this->scanner->getPseudoClassString();
+ return $buf;
+ }
+ }
+
+ /**
+ * Handle element names.
+ * This will call the EventHandler::elementName().
+ *
+ * This handles:
+ * <code>
+ * name (EventHandler::element())
+ * |name (EventHandler::element())
+ * ns|name (EventHandler::elementNS())
+ * ns|* (EventHandler::elementNS())
+ * </code>
+ */
+ private function elementName() {
+ if ($this->DEBUG) print "ELEMENT NAME\n";
+ if ($this->scanner->token === Token::pipe) {
+ // We have '|name', which is equiv to 'name'
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+ $elementName = $this->scanner->getNameString();
+ $this->handler->element($elementName);
+ }
+ elseif ($this->scanner->token === Token::char) {
+ $elementName = $this->scanner->getNameString();
+ if ($this->scanner->token == Token::pipe) {
+ // Get ns|name
+ $elementNS = $elementName;
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+ if ($this->scanner->token === Token::star) {
+ // We have ns|*
+ $this->handler->anyElementInNS($elementNS);
+ $this->scanner->nextToken();
+ }
+ elseif ($this->scanner->token !== Token::char) {
+ $this->throwError(Token::char, $this->scanner->token);
+ }
+ else {
+ $elementName = $this->scanner->getNameString();
+ // We have ns|name
+ $this->handler->elementNS($elementName, $elementNS);
+ }
+
+ }
+ else {
+ $this->handler->element($elementName);
+ }
+ }
+ }
+
+ /**
+ * Check for all elements designators. Due to the new CSS 3 namespace
+ * support, this is slightly more complicated, now, as it handles
+ * the *|name and *|* cases as well as *.
+ *
+ * Calls EventHandler::anyElement() or EventHandler::elementName().
+ */
+ private function allElements() {
+ if ($this->scanner->token === Token::star) {
+ $this->scanner->nextToken();
+ if ($this->scanner->token === Token::pipe) {
+ $this->scanner->nextToken();
+ if ($this->scanner->token === Token::star) {
+ // We got *|*. According to spec, this requires
+ // that the element has a namespace, so we pass it on
+ // to the handler:
+ $this->scanner->nextToken();
+ $this->handler->anyElementInNS('*');
+ }
+ else {
+ // We got *|name, which means the name MUST be in a namespce,
+ // so we pass this off to elementNameNS().
+ $name = $this->scanner->getNameString();
+ $this->handler->elementNS($name, '*');
+ }
+ }
+ else {
+ $this->handler->anyElement();
+ }
+ }
+ }
+
+ /**
+ * Handler an attribute.
+ * An attribute can be in one of two forms:
+ * <code>[attrName]</code>
+ * or
+ * <code>[attrName="AttrValue"]</code>
+ *
+ * This may call the following event handlers: EventHandler::attribute().
+ */
+ private function attribute() {
+ if($this->scanner->token == Token::lsquare) {
+ $attrVal = $op = $ns = NULL;
+
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+
+ if ($this->scanner->token === Token::at) {
+ if ($this->strict) {
+ throw new ParseException('The @ is illegal in attributes.');
+ }
+ else {
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+ }
+ }
+
+ if ($this->scanner->token === Token::star) {
+ // Global namespace... requires that attr be prefixed,
+ // so we pass this on to a namespace handler.
+ $ns = '*';
+ $this->scanner->nextToken();
+ }
+ if ($this->scanner->token === Token::pipe) {
+ // Skip this. It's a global namespace.
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+ }
+
+ $attrName = $this->scanner->getNameString();
+ $this->consumeWhitespace();
+
+ // Check for namespace attribute: ns|attr. We have to peek() to make
+ // sure that we haven't hit the |= operator, which looks the same.
+ if ($this->scanner->token === Token::pipe && $this->scanner->peek() !== '=') {
+ // We have a namespaced attribute.
+ $ns = $attrName;
+ $this->scanner->nextToken();
+ $attrName = $this->scanner->getNameString();
+ $this->consumeWhitespace();
+ }
+
+ // Note: We require that operators do not have spaces
+ // between characters, e.g. ~= , not ~ =.
+
+ // Get the operator:
+ switch ($this->scanner->token) {
+ case Token::eq:
+ $this->consumeWhitespace();
+ $op = EventHandler::isExactly;
+ break;
+ case Token::tilde:
+ if ($this->scanner->nextToken() !== Token::eq) {
+ $this->throwError(Token::eq, $this->scanner->token);
+ }
+ $op = EventHandler::containsWithSpace;
+ break;
+ case Token::pipe:
+ if ($this->scanner->nextToken() !== Token::eq) {
+ $this->throwError(Token::eq, $this->scanner->token);
+ }
+ $op = EventHandler::containsWithHyphen;
+ break;
+ case Token::star:
+ if ($this->scanner->nextToken() !== Token::eq) {
+ $this->throwError(Token::eq, $this->scanner->token);
+ }
+ $op = EventHandler::containsInString;
+ break;
+ case Token::dollar;
+ if ($this->scanner->nextToken() !== Token::eq) {
+ $this->throwError(Token::eq, $this->scanner->token);
+ }
+ $op = EventHandler::endsWith;
+ break;
+ case Token::carat:
+ if ($this->scanner->nextToken() !== Token::eq) {
+ $this->throwError(Token::eq, $this->scanner->token);
+ }
+ $op = EventHandler::beginsWith;
+ break;
+ }
+
+ if (isset($op)) {
+ // Consume '=' and go on.
+ $this->scanner->nextToken();
+ $this->consumeWhitespace();
+
+ // So... here we have a problem. The grammer suggests that the
+ // value here is String1 or String2, both of which are enclosed
+ // in quotes of some sort, and both of which allow lots of special
+ // characters. But the spec itself includes examples like this:
+ // [lang=fr]
+ // So some bareword support is assumed. To get around this, we assume
+ // that bare words follow the NAME rules, while quoted strings follow
+ // the String1/String2 rules.
+
+ if ($this->scanner->token === Token::quote || $this->scanner->token === Token::squote) {
+ $attrVal = $this->scanner->getQuotedString();
+ }
+ else {
+ $attrVal = $this->scanner->getNameString();
+ }
+
+ if ($this->DEBUG) {
+ print "ATTR: $attrVal AND OP: $op\n";
+ }
+ }
+
+ $this->consumeWhitespace();
+
+ if ($this->scanner->token != Token::rsquare) {
+ $this->throwError(Token::rsquare, $this->scanner->token);
+ }
+
+ if (isset($ns)) {
+ $this->handler->attributeNS($attrName, $ns, $attrVal, $op);
+ }
+ elseif (isset($attrVal)) {
+ $this->handler->attribute($attrName, $attrVal, $op);
+ }
+ else {
+ $this->handler->attribute($attrName);
+ }
+ $this->scanner->nextToken();
+ }
+ }
+
+ /**
+ * Utility for throwing a consistantly-formatted parse error.
+ */
+ private function throwError($expected, $got) {
+ $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got));
+ throw new ParseException($filter);
+ }
+
+}
+
diff --git a/lib/querypath/src/QueryPath/CSS/QueryPathEventHandler.php b/lib/querypath/src/QueryPath/CSS/QueryPathEventHandler.php
new file mode 100644
index 0000000..2dcfd57
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/QueryPathEventHandler.php
@@ -0,0 +1,1424 @@
+<?php
+/** @file
+ * This file contains a full implementation of the EventHandler interface.
+ *
+ * The tools in this package initiate a CSS selector parsing routine and then
+ * handle all of the callbacks.
+ *
+ * The implementation provided herein adheres to the CSS 3 Selector specification
+ * with the following caveats:
+ *
+ * - The negation (:not()) and containment (:has()) pseudo-classes allow *full*
+ * selectors and not just simple selectors.
+ * - There are a variety of additional pseudo-classes supported by this
+ * implementation that are not part of the spec. Most of the jQuery
+ * pseudo-classes are supported. The :x-root pseudo-class is also supported.
+ * - Pseudo-classes that require a User Agent to function have been disabled.
+ * Thus there is no :hover pseudo-class.
+ * - All pseudo-elements require the double-colon (::) notation. This breaks
+ * backward compatibility with the 2.1 spec, but it makes visible the issue
+ * that pseudo-elements cannot be effectively used with most of the present
+ * library. They return <b>stdClass objects with a text property</b> (QP > 1.3)
+ * instead of elements.
+ * - The pseudo-classes first-of-type, nth-of-type and last-of-type may or may
+ * not conform to the specification. The spec is unclear.
+ * - pseudo-class filters of the form -an+b do not function as described in the
+ * specification. However, they do behave the same way here as they do in
+ * jQuery.
+ * - This library DOES provide XML namespace aware tools. Selectors can use
+ * namespaces to increase specificity.
+ * - This library does nothing with the CSS 3 Selector specificity rating. Of
+ * course specificity is preserved (to the best of our abilities), but there
+ * is no calculation done.
+ *
+ * For detailed examples of how the code works and what selectors are supported,
+ * see the CssEventTests file, which contains the unit tests used for
+ * testing this implementation.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license MIT
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * Handler that tracks progress of a query through a DOM.
+ *
+ * The main idea is that we keep a copy of the tree, and then use an
+ * array to keep track of matches. To handle a list of selectors (using
+ * the comma separator), we have to track both the currently progressing
+ * match and the previously matched elements.
+ *
+ * To use this handler:
+ * @code
+ * $filter = '#id'; // Some CSS selector
+ * $handler = new QueryPathEventHandler(DOMNode $dom);
+ * $parser = new Parser();
+ * $parser->parse($filter, $handler);
+ * $matches = $handler->getMatches();
+ * @endcode
+ *
+ * $matches will be an array of zero or more DOMElement objects.
+ *
+ * @ingroup querypath_css
+ */
+class QueryPathEventHandler implements EventHandler, Traverser {
+ protected $dom = NULL; // Always points to the top level.
+ protected $matches = NULL; // The matches
+ protected $alreadyMatched = NULL; // Matches found before current selector.
+ protected $findAnyElement = TRUE;
+
+
+ /**
+ * Create a new event handler.
+ */
+ public function __construct($dom) {
+ $this->alreadyMatched = new \SplObjectStorage();
+ $matches = new \SplObjectStorage();
+
+ // Array of DOMElements
+ if (is_array($dom) || $dom instanceof \SplObjectStorage) {
+ //$matches = array();
+ foreach($dom as $item) {
+ if ($item instanceof \DOMNode && $item->nodeType == XML_ELEMENT_NODE) {
+ //$matches[] = $item;
+ $matches->attach($item);
+ }
+ }
+ //$this->dom = count($matches) > 0 ? $matches[0] : NULL;
+ if ($matches->count() > 0) {
+ $matches->rewind();
+ $this->dom = $matches->current();
+ }
+ else {
+ //throw new Exception("Setting DOM to Null");
+ $this->dom = NULL;
+ }
+ $this->matches = $matches;
+ }
+ // DOM Document -- we get the root element.
+ elseif ($dom instanceof \DOMDocument) {
+ $this->dom = $dom->documentElement;
+ $matches->attach($dom->documentElement);
+ }
+ // DOM Element -- we use this directly
+ elseif ($dom instanceof \DOMElement) {
+ $this->dom = $dom;
+ $matches->attach($dom);
+ }
+ // NodeList -- We turn this into an array
+ elseif ($dom instanceof \DOMNodeList) {
+ $a = array(); // Not sure why we are doing this....
+ foreach ($dom as $item) {
+ if ($item->nodeType == XML_ELEMENT_NODE) {
+ $matches->attach($item);
+ $a[] = $item;
+ }
+ }
+ $this->dom = $a;
+ }
+ // FIXME: Handle SimpleXML!
+ // Uh-oh... we don't support anything else.
+ else {
+ throw new \QueryPath\Exception("Unhandled type: " . get_class($dom));
+ }
+ $this->matches = $matches;
+ }
+
+ /**
+ * Generic finding method.
+ *
+ * This is the primary searching method used throughout QueryPath.
+ *
+ * @param string $filter
+ * A valid CSS 3 filter.
+ * @return QueryPathEventHandler
+ * Returns itself.
+ */
+ public function find($filter) {
+ $parser = new Parser($filter, $this);
+ $parser->parse();
+ return $this;
+ }
+
+ /**
+ * Get the elements that match the evaluated selector.
+ *
+ * This should be called after the filter has been parsed.
+ *
+ * @return array
+ * The matched items. This is almost always an array of
+ * {@link DOMElement} objects. It is always an instance of
+ * {@link DOMNode} objects.
+ */
+ public function getMatches() {
+ //$result = array_merge($this->alreadyMatched, $this->matches);
+ $result = new \SplObjectStorage();
+ foreach($this->alreadyMatched as $m) $result->attach($m);
+ foreach($this->matches as $m) $result->attach($m);
+ return $result;
+ }
+
+ public function matches() {
+ return $this->getMatches();
+ }
+
+ /**
+ * Find any element with the ID that matches $id.
+ *
+ * If this finds an ID, it will immediately quit. Essentially, it doesn't
+ * enforce ID uniqueness, but it assumes it.
+ *
+ * @param $id
+ * String ID for an element.
+ */
+ public function elementID($id) {
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ // Check if any of the current items has the desired ID.
+ if ($item->hasAttribute('id') && $item->getAttribute('id') === $id) {
+ $found->attach($item);
+ break;
+ }
+ }
+ $this->matches = $found;
+ $this->findAnyElement = FALSE;
+ }
+
+ // Inherited
+ public function element($name) {
+ $matches = $this->candidateList();
+ $this->findAnyElement = FALSE;
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ // Should the existing item be included?
+ // In some cases (e.g. element is root element)
+ // it definitely should. But what about other cases?
+ if ($item->tagName == $name) {
+ $found->attach($item);
+ }
+ // Search for matching kids.
+ //$nl = $item->getElementsByTagName($name);
+ //$found = array_merge($found, $this->nodeListToArray($nl));
+ }
+
+ $this->matches = $found;
+ }
+
+ // Inherited
+ public function elementNS($lname, $namespace = NULL) {
+ $this->findAnyElement = FALSE;
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ // Looking up NS URI only works if the XMLNS attributes are declared
+ // at a level equal to or above the searching doc. Normalizing a doc
+ // should fix this, but it doesn't. So we have to use a fallback
+ // detection scheme which basically searches by lname and then
+ // does a post hoc check on the tagname.
+
+ //$nsuri = $item->lookupNamespaceURI($namespace);
+ $nsuri = $this->dom->lookupNamespaceURI($namespace);
+
+ // XXX: Presumably the base item needs to be checked. Spec isn't
+ // too clear, but there are three possibilities:
+ // - base should always be checked (what we do here)
+ // - base should never be checked (only children)
+ // - base should only be checked if it is the root node
+ if ($item instanceof \DOMNode
+ && $item->namespaceURI == $nsuri
+ && $lname == $item->localName) {
+ $found->attach($item);
+ }
+
+ if (!empty($nsuri)) {
+ $nl = $item->getElementsByTagNameNS($nsuri, $lname);
+ // If something is found, merge them:
+ //if (!empty($nl)) $found = array_merge($found, $this->nodeListToArray($nl));
+ if (!empty($nl)) $this->attachNodeList($nl, $found);
+ }
+ else {
+ //$nl = $item->getElementsByTagName($namespace . ':' . $lname);
+ $nl = $item->getElementsByTagName($lname);
+ $tagname = $namespace . ':' . $lname;
+ $nsmatches = array();
+ foreach ($nl as $node) {
+ if ($node->tagName == $tagname) {
+ //$nsmatches[] = $node;
+ $found->attach($node);
+ }
+ }
+ // If something is found, merge them:
+ //if (!empty($nsmatches)) $found = array_merge($found, $nsmatches);
+ }
+ }
+ $this->matches = $found;
+ }
+
+ public function anyElement() {
+ $found = new \SplObjectStorage();
+ //$this->findAnyElement = TRUE;
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ $found->attach($item); // Add self
+ // See issue #20 or section 6.2 of this:
+ // http://www.w3.org/TR/2009/PR-css3-selectors-20091215/#universal-selector
+ //$nl = $item->getElementsByTagName('*');
+ //$this->attachNodeList($nl, $found);
+ }
+
+ $this->matches = $found;
+ $this->findAnyElement = FALSE;
+ }
+ public function anyElementInNS($ns) {
+ //$this->findAnyElement = TRUE;
+ $nsuri = $this->dom->lookupNamespaceURI($ns);
+ $found = new \SplObjectStorage();
+ if (!empty($nsuri)) {
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ if ($item instanceOf \DOMNode && $nsuri == $item->namespaceURI) {
+ $found->attach($item);
+ }
+ }
+ }
+ $this->matches = $found;//UniqueElementList::get($found);
+ $this->findAnyElement = FALSE;
+ }
+ public function elementClass($name) {
+
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ if ($item->hasAttribute('class')) {
+ $classes = explode(' ', $item->getAttribute('class'));
+ if (in_array($name, $classes)) $found->attach($item);
+ }
+ }
+
+ $this->matches = $found;//UniqueElementList::get($found);
+ $this->findAnyElement = FALSE;
+ }
+
+ public function attribute($name, $value = NULL, $operation = EventHandler::isExactly) {
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ if ($item->hasAttribute($name)) {
+ if (isset($value)) {
+ // If a value exists, then we need a match.
+ if($this->attrValMatches($value, $item->getAttribute($name), $operation)) {
+ $found->attach($item);
+ }
+ }
+ else {
+ // If no value exists, then we consider it a match.
+ $found->attach($item);
+ }
+ }
+ }
+ $this->matches = $found; //UniqueElementList::get($found);
+ $this->findAnyElement = FALSE;
+ }
+
+ /**
+ * Helper function to find all elements with exact matches.
+ *
+ * @deprecated All use cases seem to be covered by attribute().
+ */
+ protected function searchForAttr($name, $value = NULL) {
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $candidate) {
+ if ($candidate->hasAttribute($name)) {
+ // If value is required, match that, too.
+ if (isset($value) && $value == $candidate->getAttribute($name)) {
+ $found->attach($candidate);
+ }
+ // Otherwise, it's a match on name alone.
+ else {
+ $found->attach($candidate);
+ }
+ }
+ }
+
+ $this->matches = $found;
+ }
+
+ public function attributeNS($lname, $ns, $value = NULL, $operation = EventHandler::isExactly) {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ if (count($matches) == 0) {
+ $this->matches = $found;
+ return;
+ }
+
+ // Get the namespace URI for the given label.
+ //$uri = $matches[0]->lookupNamespaceURI($ns);
+ $matches->rewind();
+ $e = $matches->current();
+ $uri = $e->lookupNamespaceURI($ns);
+
+ foreach ($matches as $item) {
+ //foreach ($item->attributes as $attr) {
+ // print "$attr->prefix:$attr->localName ($attr->namespaceURI), Value: $attr->nodeValue\n";
+ //}
+ if ($item->hasAttributeNS($uri, $lname)) {
+ if (isset($value)) {
+ if ($this->attrValMatches($value, $item->getAttributeNS($uri, $lname), $operation)) {
+ $found->attach($item);
+ }
+ }
+ else {
+ $found->attach($item);
+ }
+ }
+ }
+ $this->matches = $found;
+ $this->findAnyElement = FALSE;
+ }
+
+ /**
+ * This also supports the following nonstandard pseudo classes:
+ * - :x-reset/:x-root (reset to the main item passed into the constructor. Less drastic than :root)
+ * - :odd/:even (shorthand for :nth-child(odd)/:nth-child(even))
+ */
+ public function pseudoClass($name, $value = NULL) {
+ $name = strtolower($name);
+ // Need to handle known pseudoclasses.
+ switch($name) {
+ case 'visited':
+ case 'hover':
+ case 'active':
+ case 'focus':
+ case 'animated': // Last 3 are from jQuery
+ case 'visible':
+ case 'hidden':
+ // These require a UA, which we don't have.
+ case 'target':
+ // This requires a location URL, which we don't have.
+ $this->matches = new \SplObjectStorage();
+ break;
+ case 'indeterminate':
+ // The assumption is that there is a UA and the format is HTML.
+ // I don't know if this should is useful without a UA.
+ throw new NotImplementedException(":indeterminate is not implemented.");
+ break;
+ case 'lang':
+ // No value = exception.
+ if (!isset($value)) {
+ throw new NotImplementedException("No handler for lang pseudoclass without value.");
+ }
+ $this->lang($value);
+ break;
+ case 'link':
+ $this->searchForAttr('href');
+ break;
+ case 'root':
+ $found = new \SplObjectStorage();
+ if (empty($this->dom)) {
+ $this->matches = $found;
+ }
+ elseif (is_array($this->dom)) {
+ $found->attach($this->dom[0]->ownerDocument->documentElement);
+ $this->matches = $found;
+ }
+ elseif ($this->dom instanceof \DOMNode) {
+ $found->attach($this->dom->ownerDocument->documentElement);
+ $this->matches = $found;
+ }
+ elseif ($this->dom instanceof \DOMNodeList && $this->dom->length > 0) {
+ $found->attach($this->dom->item(0)->ownerDocument->documentElement);
+ $this->matches = $found;
+ }
+ else {
+ // Hopefully we never get here:
+ $found->attach($this->dom);
+ $this->matches = $found;
+ }
+ break;
+
+ // NON-STANDARD extensions for reseting to the "top" items set in
+ // the constructor.
+ case 'x-root':
+ case 'x-reset':
+ $this->matches = new \SplObjectStorage();
+ $this->matches->attach($this->dom);
+ break;
+
+ // NON-STANDARD extensions for simple support of even and odd. These
+ // are supported by jQuery, FF, and other user agents.
+ case 'even':
+ $this->nthChild(2, 0);
+ break;
+ case 'odd':
+ $this->nthChild(2, 1);
+ break;
+
+ // Standard child-checking items.
+ case 'nth-child':
+ list($aVal, $bVal) = $this->parseAnB($value);
+ $this->nthChild($aVal, $bVal);
+ break;
+ case 'nth-last-child':
+ list($aVal, $bVal) = $this->parseAnB($value);
+ $this->nthLastChild($aVal, $bVal);
+ break;
+ case 'nth-of-type':
+ list($aVal, $bVal) = $this->parseAnB($value);
+ $this->nthOfTypeChild($aVal, $bVal, FALSE);
+ break;
+ case 'nth-last-of-type':
+ list($aVal, $bVal) = $this->parseAnB($value);
+ $this->nthLastOfTypeChild($aVal, $bVal);
+ break;
+ case 'first-child':
+ $this->nthChild(0, 1);
+ break;
+ case 'last-child':
+ $this->nthLastChild(0, 1);
+ break;
+ case 'first-of-type':
+ $this->firstOfType();
+ break;
+ case 'last-of-type':
+ $this->lastOfType();
+ break;
+ case 'only-child':
+ $this->onlyChild();
+ break;
+ case 'only-of-type':
+ $this->onlyOfType();
+ break;
+ case 'empty':
+ $this->emptyElement();
+ break;
+ case 'not':
+ if (empty($value)) {
+ throw new ParseException(":not() requires a value.");
+ }
+ $this->not($value);
+ break;
+ // Additional pseudo-classes defined in jQuery:
+ case 'lt':
+ case 'gt':
+ case 'nth':
+ case 'eq':
+ case 'first':
+ case 'last':
+ //case 'even':
+ //case 'odd':
+ $this->getByPosition($name, $value);
+ break;
+ case 'parent':
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $match) {
+ if (!empty($match->firstChild)) {
+ $found->attach($match);
+ }
+ }
+ $this->matches = $found;
+ break;
+
+ case 'enabled':
+ case 'disabled':
+ case 'checked':
+ $this->attribute($name);
+ break;
+ case 'text':
+ case 'radio':
+ case 'checkbox':
+ case 'file':
+ case 'password':
+ case 'submit':
+ case 'image':
+ case 'reset':
+ case 'button':
+ $this->attribute('type', $name);
+ break;
+
+ case 'header':
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ $tag = $item->tagName;
+ $f = strtolower(substr($tag, 0, 1));
+ if ($f == 'h' && strlen($tag) == 2 && ctype_digit(substr($tag, 1, 1))) {
+ $found->attach($item);
+ }
+ }
+ $this->matches = $found;
+ break;
+ case 'has':
+ $this->has($value);
+ break;
+ // Contains == text matches.
+ // In QP 2.1, this was changed.
+ case 'contains':
+ $value = $this->removeQuotes($value);
+
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ if (strpos($item->textContent, $value) !== FALSE) {
+ $found->attach($item);
+ }
+ }
+ $this->matches = $found;
+ break;
+
+ // Since QP 2.1
+ case 'contains-exactly':
+ $value = $this->removeQuotes($value);
+
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ if ($item->textContent == $value) {
+ $found->attach($item);
+ }
+ }
+ $this->matches = $found;
+ break;
+ default:
+ throw new ParseException("Unknown Pseudo-Class: " . $name);
+ }
+ $this->findAnyElement = FALSE;
+ }
+
+ /**
+ * Remove leading and trailing quotes.
+ */
+ private function removeQuotes($str) {
+ $f = substr($str, 0, 1);
+ $l = substr($str, -1);
+ if ($f === $l && ($f == '"' || $f == "'")) {
+ $str = substr($str, 1, -1);
+ }
+ return $str;
+ }
+
+ /**
+ * Pseudo-class handler for a variety of jQuery pseudo-classes.
+ * Handles lt, gt, eq, nth, first, last pseudo-classes.
+ */
+ private function getByPosition($operator, $pos) {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ if ($matches->count() == 0) {
+ return;
+ }
+
+ switch ($operator) {
+ case 'nth':
+ case 'eq':
+ if ($matches->count() >= $pos) {
+ //$found[] = $matches[$pos -1];
+ foreach ($matches as $match) {
+ // CSS is 1-based, so we pre-increment.
+ if ($matches->key() + 1 == $pos) {
+ $found->attach($match);
+ break;
+ }
+ }
+ }
+ break;
+ case 'first':
+ if ($matches->count() > 0) {
+ $matches->rewind(); // This is necessary to init.
+ $found->attach($matches->current());
+ }
+ break;
+ case 'last':
+ if ($matches->count() > 0) {
+
+ // Spin through iterator.
+ foreach ($matches as $item) {};
+
+ $found->attach($item);
+ }
+ break;
+ // case 'even':
+ // for ($i = 1; $i <= count($matches); ++$i) {
+ // if ($i % 2 == 0) {
+ // $found[] = $matches[$i];
+ // }
+ // }
+ // break;
+ // case 'odd':
+ // for ($i = 1; $i <= count($matches); ++$i) {
+ // if ($i % 2 == 0) {
+ // $found[] = $matches[$i];
+ // }
+ // }
+ // break;
+ case 'lt':
+ $i = 0;
+ foreach ($matches as $item) {
+ if (++$i < $pos) {
+ $found->attach($item);
+ }
+ }
+ break;
+ case 'gt':
+ $i = 0;
+ foreach ($matches as $item) {
+ if (++$i > $pos) {
+ $found->attach($item);
+ }
+ }
+ break;
+ }
+
+ $this->matches = $found;
+ }
+
+ /**
+ * Parse an an+b rule for CSS pseudo-classes.
+ * @param $rule
+ * Some rule in the an+b format.
+ * @return
+ * Array (list($aVal, $bVal)) of the two values.
+ * @throws ParseException
+ * If the rule does not follow conventions.
+ */
+ protected function parseAnB($rule) {
+ if ($rule == 'even') {
+ return array(2, 0);
+ }
+ elseif ($rule == 'odd') {
+ return array(2, 1);
+ }
+ elseif ($rule == 'n') {
+ return array(1, 0);
+ }
+ elseif (is_numeric($rule)) {
+ return array(0, (int)$rule);
+ }
+
+ $rule = explode('n', $rule);
+ if (count($rule) == 0) {
+ throw new ParseException("nth-child value is invalid.");
+ }
+
+ // Each of these is legal: 1, -1, and -. '-' is shorthand for -1.
+ $aVal = trim($rule[0]);
+ $aVal = ($aVal == '-') ? -1 : (int)$aVal;
+
+ $bVal = !empty($rule[1]) ? (int)trim($rule[1]) : 0;
+ return array($aVal, $bVal);
+ }
+
+ /**
+ * Pseudo-class handler for nth-child and all related pseudo-classes.
+ *
+ * @param int $groupSize
+ * The size of the group (in an+b, this is a).
+ * @param int $elementInGroup
+ * The offset in a group. (in an+b this is b).
+ * @param boolean $lastChild
+ * Whether counting should begin with the last child. By default, this is false.
+ * Pseudo-classes that start with the last-child can set this to true.
+ */
+ protected function nthChild($groupSize, $elementInGroup, $lastChild = FALSE) {
+ // EXPERIMENTAL: New in Quark. This should be substantially faster
+ // than the old (jQuery-ish) version. It still has E_STRICT violations
+ // though.
+ $parents = new \SplObjectStorage();
+ $matches = new \SplObjectStorage();
+
+ $i = 0;
+ foreach ($this->matches as $item) {
+ $parent = $item->parentNode;
+
+ // Build up an array of all of children of this parent, and store the
+ // index of each element for reference later. We only need to do this
+ // once per parent, though.
+ if (!$parents->contains($parent)) {
+
+ $c = 0;
+ foreach ($parent->childNodes as $child) {
+ // We only want nodes, and if this call is preceded by an element
+ // selector, we only want to match elements with the same tag name.
+ // !!! This last part is a grey area in the CSS 3 Selector spec. It seems
+ // necessary to make the implementation match the examples in the spec. However,
+ // jQuery 1.2 does not do this.
+ if ($child->nodeType == XML_ELEMENT_NODE && ($this->findAnyElement || $child->tagName == $item->tagName)) {
+ // This may break E_STRICT.
+ $child->nodeIndex = ++$c;
+ }
+ }
+ // This may break E_STRICT.
+ $parent->numElements = $c;
+ $parents->attach($parent);
+ }
+
+ // If we are looking for the last child, we count from the end of a list.
+ // Note that we add 1 because CSS indices begin at 1, not 0.
+ if ($lastChild) {
+ $indexToMatch = $item->parentNode->numElements - $item->nodeIndex + 1;
+ }
+ // Otherwise we count from the beginning of the list.
+ else {
+ $indexToMatch = $item->nodeIndex;
+ }
+
+ // If group size is 0, then we return element at the right index.
+ if ($groupSize == 0) {
+ if ($indexToMatch == $elementInGroup)
+ $matches->attach($item);
+ }
+ // If group size != 0, then we grab nth element from group offset by
+ // element in group.
+ else {
+ if (($indexToMatch - $elementInGroup) % $groupSize == 0
+ && ($indexToMatch - $elementInGroup) / $groupSize >= 0) {
+ $matches->attach($item);
+ }
+ }
+
+ // Iterate.
+ ++$i;
+ }
+ $this->matches = $matches;
+ }
+
+ /**
+ * Reverse a set of matches.
+ *
+ * This is now necessary because internal matches are no longer represented
+ * as arrays.
+ * @since QueryPath 2.0
+ *//*
+ private function reverseMatches() {
+ // Reverse the candidate list. There must be a better way of doing
+ // this.
+ $arr = array();
+ foreach ($this->matches as $m) array_unshift($arr, $m);
+
+ $this->found = new \SplObjectStorage();
+ foreach ($arr as $item) $this->found->attach($item);
+ }*/
+
+ /**
+ * Pseudo-class handler for :nth-last-child and related pseudo-classes.
+ */
+ protected function nthLastChild($groupSize, $elementInGroup) {
+ // New in Quark.
+ $this->nthChild($groupSize, $elementInGroup, TRUE);
+ }
+
+ /**
+ * Get a list of peer elements.
+ * If $requireSameTag is TRUE, then only peer elements with the same
+ * tagname as the given element will be returned.
+ *
+ * @param $element
+ * A DomElement.
+ * @param $requireSameTag
+ * Boolean flag indicating whether all matches should have the same
+ * element name (tagName) as $element.
+ * @return
+ * Array of peer elements.
+ *//*
+ protected function listPeerElements($element, $requireSameTag = FALSE) {
+ $peers = array();
+ $parent = $element->parentNode;
+ foreach ($parent->childNodes as $node) {
+ if ($node->nodeType == XML_ELEMENT_NODE) {
+ if ($requireSameTag) {
+ // Need to make sure that the tag matches:
+ if ($element->tagName == $node->tagName) {
+ $peers[] = $node;
+ }
+ }
+ else {
+ $peers[] = $node;
+ }
+ }
+ }
+ return $peers;
+ }
+ */
+ /**
+ * Get the nth child (by index) from matching candidates.
+ *
+ * This is used by pseudo-class handlers.
+ */
+ /*
+ protected function childAtIndex($index, $tagName = NULL) {
+ $restrictToElement = !$this->findAnyElement;
+ $matches = $this->candidateList();
+ $defaultTagName = $tagName;
+
+ // XXX: Added in Quark: I believe this should return an empty
+ // match set if no child was found tat the index.
+ $this->matches = new \SplObjectStorage();
+
+ foreach ($matches as $item) {
+ $parent = $item->parentNode;
+
+ // If a default tag name is supplied, we always use it.
+ if (!empty($defaultTagName)) {
+ $tagName = $defaultTagName;
+ }
+ // If we are inside of an element selector, we use the
+ // tag name of the given elements.
+ elseif ($restrictToElement) {
+ $tagName = $item->tagName;
+ }
+ // Otherwise, we skip the tag name match.
+ else {
+ $tagName = NULL;
+ }
+
+ // Loop through all children looking for matches.
+ $i = 0;
+ foreach ($parent->childNodes as $child) {
+ if ($child->nodeType !== XML_ELEMENT_NODE) {
+ break; // Skip non-elements
+ }
+
+ // If type is set, then we do type comparison
+ if (!empty($tagName)) {
+ // Check whether tag name matches the type.
+ if ($child->tagName == $tagName) {
+ // See if this is the index we are looking for.
+ if ($i == $index) {
+ //$this->matches = new \SplObjectStorage();
+ $this->matches->attach($child);
+ return;
+ }
+ // If it's not the one we are looking for, increment.
+ ++$i;
+ }
+ }
+ // We don't care about type. Any tagName will match.
+ else {
+ if ($i == $index) {
+ $this->matches->attach($child);
+ return;
+ }
+ ++$i;
+ }
+ } // End foreach
+ }
+
+ }*/
+
+ /**
+ * Pseudo-class handler for nth-of-type-child.
+ * Not implemented.
+ */
+ protected function nthOfTypeChild($groupSize, $elementInGroup, $lastChild) {
+ // EXPERIMENTAL: New in Quark. This should be substantially faster
+ // than the old (jQuery-ish) version. It still has E_STRICT violations
+ // though.
+ $parents = new \SplObjectStorage();
+ $matches = new \SplObjectStorage();
+
+ $i = 0;
+ foreach ($this->matches as $item) {
+ $parent = $item->parentNode;
+
+ // Build up an array of all of children of this parent, and store the
+ // index of each element for reference later. We only need to do this
+ // once per parent, though.
+ if (!$parents->contains($parent)) {
+
+ $c = 0;
+ foreach ($parent->childNodes as $child) {
+ // This doesn't totally make sense, since the CSS 3 spec does not require that
+ // this pseudo-class be adjoined to an element (e.g. ' :nth-of-type' is allowed).
+ if ($child->nodeType == XML_ELEMENT_NODE && $child->tagName == $item->tagName) {
+ // This may break E_STRICT.
+ $child->nodeIndex = ++$c;
+ }
+ }
+ // This may break E_STRICT.
+ $parent->numElements = $c;
+ $parents->attach($parent);
+ }
+
+ // If we are looking for the last child, we count from the end of a list.
+ // Note that we add 1 because CSS indices begin at 1, not 0.
+ if ($lastChild) {
+ $indexToMatch = $item->parentNode->numElements - $item->nodeIndex + 1;
+ }
+ // Otherwise we count from the beginning of the list.
+ else {
+ $indexToMatch = $item->nodeIndex;
+ }
+
+ // If group size is 0, then we return element at the right index.
+ if ($groupSize == 0) {
+ if ($indexToMatch == $elementInGroup)
+ $matches->attach($item);
+ }
+ // If group size != 0, then we grab nth element from group offset by
+ // element in group.
+ else {
+ if (($indexToMatch - $elementInGroup) % $groupSize == 0
+ && ($indexToMatch - $elementInGroup) / $groupSize >= 0) {
+ $matches->attach($item);
+ }
+ }
+
+ // Iterate.
+ ++$i;
+ }
+ $this->matches = $matches;
+ }
+
+ /**
+ * Pseudo-class handler for nth-last-of-type-child.
+ * Not implemented.
+ */
+ protected function nthLastOfTypeChild($groupSize, $elementInGroup) {
+ $this->nthOfTypeChild($groupSize, $elementInGroup, TRUE);
+ }
+
+ /**
+ * Pseudo-class handler for :lang
+ */
+ protected function lang($value) {
+ // TODO: This checks for cases where an explicit language is
+ // set. The spec seems to indicate that an element should inherit
+ // language from the parent... but this is unclear.
+ $operator = (strpos($value, '-') !== FALSE) ? self::isExactly : self::containsWithHyphen;
+
+ $orig = $this->matches;
+ $origDepth = $this->findAnyElement;
+
+ // Do first pass: attributes in default namespace
+ $this->attribute('lang', $value, $operator);
+ $lang = $this->matches; // Temp array for merging.
+
+ // Reset
+ $this->matches = $orig;
+ $this->findAnyElement = $origDepth;
+
+ // Do second pass: attributes in 'xml' namespace.
+ $this->attributeNS('lang', 'xml', $value, $operator);
+
+
+ // Merge results.
+ // FIXME: Note that we lose natural ordering in
+ // the document because we search for xml:lang separately
+ // from lang.
+ foreach ($this->matches as $added) $lang->attach($added);
+ $this->matches = $lang;
+ }
+
+ /**
+ * Pseudo-class handler for :not(filter).
+ *
+ * This does not follow the specification in the following way: The CSS 3
+ * selector spec says the value of not() must be a simple selector. This
+ * function allows complex selectors.
+ *
+ * @param string $filter
+ * A CSS selector.
+ */
+ protected function not($filter) {
+ $matches = $this->candidateList();
+ //$found = array();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ $handler = new QueryPathEventHandler($item);
+ $not_these = $handler->find($filter)->getMatches();
+ if ($not_these->count() == 0) {
+ $found->attach($item);
+ }
+ }
+ // No need to check for unique elements, since the list
+ // we began from already had no duplicates.
+ $this->matches = $found;
+ }
+
+ /**
+ * Pseudo-class handler for :has(filter).
+ * This can also be used as a general filtering routine.
+ */
+ public function has($filter) {
+ $matches = $this->candidateList();
+ //$found = array();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ $handler = new QueryPathEventHandler($item);
+ $these = $handler->find($filter)->getMatches();
+ if (count($these) > 0) {
+ $found->attach($item);
+ }
+ }
+ $this->matches = $found;
+ return $this;
+ }
+
+ /**
+ * Pseudo-class handler for :first-of-type.
+ */
+ protected function firstOfType() {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ $type = $item->tagName;
+ $parent = $item->parentNode;
+ foreach ($parent->childNodes as $kid) {
+ if ($kid->nodeType == XML_ELEMENT_NODE && $kid->tagName == $type) {
+ if (!$found->contains($kid)) {
+ $found->attach($kid);
+ }
+ break;
+ }
+ }
+ }
+ $this->matches = $found;
+ }
+
+ /**
+ * Pseudo-class handler for :last-of-type.
+ */
+ protected function lastOfType() {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ $type = $item->tagName;
+ $parent = $item->parentNode;
+ for ($i = $parent->childNodes->length - 1; $i >= 0; --$i) {
+ $kid = $parent->childNodes->item($i);
+ if ($kid->nodeType == XML_ELEMENT_NODE && $kid->tagName == $type) {
+ if (!$found->contains($kid)) {
+ $found->attach($kid);
+ }
+ break;
+ }
+ }
+ }
+ $this->matches = $found;
+ }
+
+ /**
+ * Pseudo-class handler for :only-child.
+ */
+ protected function onlyChild() {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach($matches as $item) {
+ $parent = $item->parentNode;
+ $kids = array();
+ foreach($parent->childNodes as $kid) {
+ if ($kid->nodeType == XML_ELEMENT_NODE) {
+ $kids[] = $kid;
+ }
+ }
+ // There should be only one child element, and
+ // it should be the one being tested.
+ if (count($kids) == 1 && $kids[0] === $item) {
+ $found->attach($kids[0]);
+ }
+ }
+ $this->matches = $found;
+ }
+
+ /**
+ * Pseudo-class handler for :empty.
+ */
+ protected function emptyElement() {
+ $found = new \SplObjectStorage();
+ $matches = $this->candidateList();
+ foreach ($matches as $item) {
+ $empty = TRUE;
+ foreach($item->childNodes as $kid) {
+ // From the spec: Elements and Text nodes are the only ones to
+ // affect emptiness.
+ if ($kid->nodeType == XML_ELEMENT_NODE || $kid->nodeType == XML_TEXT_NODE) {
+ $empty = FALSE;
+ break;
+ }
+ }
+ if ($empty) {
+ $found->attach($item);
+ }
+ }
+ $this->matches = $found;
+ }
+
+ /**
+ * Pseudo-class handler for :only-of-type.
+ */
+ protected function onlyOfType() {
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ if (!$item->parentNode) {
+ $this->matches = new \SplObjectStorage();
+ }
+ $parent = $item->parentNode;
+ $onlyOfType = TRUE;
+
+ // See if any peers are of the same type
+ foreach($parent->childNodes as $kid) {
+ if ($kid->nodeType == XML_ELEMENT_NODE
+ && $kid->tagName == $item->tagName
+ && $kid !== $item) {
+ //$this->matches = new \SplObjectStorage();
+ $onlyOfType = FALSE;
+ break;
+ }
+ }
+
+ // If no others were found, attach this one.
+ if ($onlyOfType) $found->attach($item);
+ }
+ $this->matches = $found;
+ }
+
+ /**
+ * Check for attr value matches based on an operation.
+ */
+ protected function attrValMatches($needle, $haystack, $operation) {
+
+ if (strlen($haystack) < strlen($needle)) return FALSE;
+
+ // According to the spec:
+ // "The case-sensitivity of attribute names in selectors depends on the document language."
+ // (6.3.2)
+ // To which I say, "huh?". We assume case sensitivity.
+ switch ($operation) {
+ case EventHandler::isExactly:
+ return $needle == $haystack;
+ case EventHandler::containsWithSpace:
+ return in_array($needle, explode(' ', $haystack));
+ case EventHandler::containsWithHyphen:
+ return in_array($needle, explode('-', $haystack));
+ case EventHandler::containsInString:
+ return strpos($haystack, $needle) !== FALSE;
+ case EventHandler::beginsWith:
+ return strpos($haystack, $needle) === 0;
+ case EventHandler::endsWith:
+ //return strrpos($haystack, $needle) === strlen($needle) - 1;
+ return preg_match('/' . $needle . '$/', $haystack) == 1;
+ }
+ return FALSE; // Shouldn't be able to get here.
+ }
+
+ /**
+ * As the spec mentions, these must be at the end of a selector or
+ * else they will cause errors. Most selectors return elements. Pseudo-elements
+ * do not.
+ */
+ public function pseudoElement($name) {
+ // process the pseudoElement
+ switch ($name) {
+ // XXX: Should this return an array -- first line of
+ // each of the matched elements?
+ case 'first-line':
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ $o = new \stdClass();
+ foreach ($matches as $item) {
+ $str = $item->textContent;
+ $lines = explode("\n", $str);
+ if (!empty($lines)) {
+ $line = trim($lines[0]);
+ if (!empty($line)) {
+ $o->textContent = $line;
+ $found->attach($o);//trim($lines[0]);
+ }
+ }
+ }
+ $this->matches = $found;
+ break;
+ // XXX: Should this return an array -- first letter of each
+ // of the matched elements?
+ case 'first-letter':
+ $matches = $this->candidateList();
+ $found = new \SplObjectStorage();
+ $o = new \stdClass();
+ foreach ($matches as $item) {
+ $str = $item->textContent;
+ if (!empty($str)) {
+ $str = substr($str,0, 1);
+ $o->textContent = $str;
+ $found->attach($o);
+ }
+ }
+ $this->matches = $found;
+ break;
+ case 'before':
+ case 'after':
+ // There is nothing in a DOM to return for the before and after
+ // selectors.
+ case 'selection':
+ // With no user agent, we don't have a concept of user selection.
+ throw new NotImplementedException("The $name pseudo-element is not implemented.");
+ break;
+ }
+ $this->findAnyElement = FALSE;
+ }
+ public function directDescendant() {
+ $this->findAnyElement = FALSE;
+
+ $kids = new \SplObjectStorage();
+ foreach ($this->matches as $item) {
+ $kidsNL = $item->childNodes;
+ foreach ($kidsNL as $kidNode) {
+ if ($kidNode->nodeType == XML_ELEMENT_NODE) {
+ $kids->attach($kidNode);
+ }
+ }
+ }
+ $this->matches = $kids;
+ }
+ /**
+ * For an element to be adjacent to another, it must be THE NEXT NODE
+ * in the node list. So if an element is surrounded by pcdata, there are
+ * no adjacent nodes. E.g. in <a/>FOO<b/>, the a and b elements are not
+ * adjacent.
+ *
+ * In a strict DOM parser, line breaks and empty spaces are nodes. That means
+ * nodes like this will not be adjacent: <test/> <test/>. The space between
+ * them makes them non-adjacent. If this is not the desired behavior, pass
+ * in the appropriate flags to your parser. Example:
+ * <code>
+ * $doc = new DomDocument();
+ * $doc->loadXML('<test/> <test/>', LIBXML_NOBLANKS);
+ * </code>
+ */
+ public function adjacent() {
+ $this->findAnyElement = FALSE;
+ // List of nodes that are immediately adjacent to the current one.
+ //$found = array();
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $item) {
+ while (isset($item->nextSibling)) {
+ if (isset($item->nextSibling) && $item->nextSibling->nodeType === XML_ELEMENT_NODE) {
+ $found->attach($item->nextSibling);
+ break;
+ }
+ $item = $item->nextSibling;
+ }
+ }
+ $this->matches = $found;
+ }
+
+ public function anotherSelector() {
+ $this->findAnyElement = FALSE;
+ // Copy old matches into buffer.
+ if ($this->matches->count() > 0) {
+ //$this->alreadyMatched = array_merge($this->alreadyMatched, $this->matches);
+ foreach ($this->matches as $item) $this->alreadyMatched->attach($item);
+ }
+
+ // Start over at the top of the tree.
+ $this->findAnyElement = TRUE; // Reset depth flag.
+ $this->matches = new \SplObjectStorage();
+ $this->matches->attach($this->dom);
+ }
+
+ /**
+ * Get all nodes that are siblings to currently selected nodes.
+ *
+ * If two passed in items are siblings of each other, neither will
+ * be included in the list of siblings. Their status as being candidates
+ * excludes them from being considered siblings.
+ */
+ public function sibling() {
+ $this->findAnyElement = FALSE;
+ // Get the nodes at the same level.
+
+ if ($this->matches->count() > 0) {
+ $sibs = new \SplObjectStorage();
+ foreach ($this->matches as $item) {
+ /*$candidates = $item->parentNode->childNodes;
+ foreach ($candidates as $candidate) {
+ if ($candidate->nodeType === XML_ELEMENT_NODE && $candidate !== $item) {
+ $sibs->attach($candidate);
+ }
+ }
+ */
+ while ($item->nextSibling != NULL) {
+ $item = $item->nextSibling;
+ if ($item->nodeType === XML_ELEMENT_NODE) $sibs->attach($item);
+ }
+ }
+ $this->matches = $sibs;
+ }
+ }
+
+ /**
+ * Get any descendant.
+ */
+ public function anyDescendant() {
+ // Get children:
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $item) {
+ $kids = $item->getElementsByTagName('*');
+ //$found = array_merge($found, $this->nodeListToArray($kids));
+ $this->attachNodeList($kids, $found);
+ }
+ $this->matches = $found;
+
+ // Set depth flag:
+ $this->findAnyElement = TRUE;
+ }
+
+ /**
+ * Determine what candidates are in the current scope.
+ *
+ * This is a utility method that gets the list of elements
+ * that should be evaluated in the context. If $this->findAnyElement
+ * is TRUE, this will return a list of every element that appears in
+ * the subtree of $this->matches. Otherwise, it will just return
+ * $this->matches.
+ */
+ private function candidateList() {
+ if ($this->findAnyElement) {
+ return $this->getAllCandidates($this->matches);
+ }
+ return $this->matches;
+ }
+
+ /**
+ * Get a list of all of the candidate elements.
+ *
+ * This is used when $this->findAnyElement is TRUE.
+ * @param $elements
+ * A list of current elements (usually $this->matches).
+ *
+ * @return
+ * A list of all candidate elements.
+ */
+ private function getAllCandidates($elements) {
+ $found = new \SplObjectStorage();
+ foreach ($elements as $item) {
+ $found->attach($item); // put self in
+ $nl = $item->getElementsByTagName('*');
+ //foreach ($nl as $node) $found[] = $node;
+ $this->attachNodeList($nl, $found);
+ }
+ return $found;
+ }
+ /*
+ public function nodeListToArray($nodeList) {
+ $array = array();
+ foreach ($nodeList as $node) {
+ if ($node->nodeType == XML_ELEMENT_NODE) {
+ $array[] = $node;
+ }
+ }
+ return $array;
+ }
+ */
+
+ /**
+ * Attach all nodes in a node list to the given \SplObjectStorage.
+ */
+ public function attachNodeList(\DOMNodeList $nodeList, \SplObjectStorage $splos) {
+ foreach ($nodeList as $item) $splos->attach($item);
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/Scanner.php b/lib/querypath/src/QueryPath/CSS/Scanner.php
new file mode 100644
index 0000000..3513a0b
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/Scanner.php
@@ -0,0 +1,306 @@
+<?php
+/** @file
+ * The scanner.
+ */
+namespace QueryPath\CSS;
+/**
+ * Scanner for CSS selector parsing.
+ *
+ * This provides a simple scanner for traversing an input stream.
+ *
+ * @ingroup querypath_css
+ */
+final class Scanner {
+ var $is = NULL;
+ public $value = NULL;
+ public $token = NULL;
+
+ var $recurse = FALSE;
+ var $it = 0;
+
+ /**
+ * Given a new input stream, tokenize the CSS selector string.
+ * @see InputStream
+ * @param InputStream $in
+ * An input stream to be scanned.
+ */
+ public function __construct(InputStream $in) {
+ $this->is = $in;
+ }
+
+ /**
+ * Return the position of the reader in the string.
+ */
+ public function position() {
+ return $this->is->position;
+ }
+
+ /**
+ * See the next char without removing it from the stack.
+ *
+ * @return char
+ * Returns the next character on the stack.
+ */
+ public function peek() {
+ return $this->is->peek();
+ }
+
+ /**
+ * Get the next token in the input stream.
+ *
+ * This sets the current token to the value of the next token in
+ * the stream.
+ *
+ * @return int
+ * Returns an int value corresponding to one of the Token constants,
+ * or FALSE if the end of the string is reached. (Remember to use
+ * strong equality checking on FALSE, since 0 is a valid token id.)
+ */
+ public function nextToken() {
+ $tok = -1;
+ ++$this->it;
+ if ($this->is->isEmpty()) {
+ if ($this->recurse) {
+ throw new \QueryPath\Exception("Recursion error detected at iteration " . $this->it . '.');
+ exit();
+ }
+ //print "{$this->it}: All done\n";
+ $this->recurse = TRUE;
+ $this->token = FALSE;
+ return FALSE;
+ }
+ $ch = $this->is->consume();
+ //print __FUNCTION__ . " Testing $ch.\n";
+ if (ctype_space($ch)) {
+ $this->value = ' '; // Collapse all WS to a space.
+ $this->token = $tok = Token::white;
+ //$ch = $this->is->consume();
+ return $tok;
+ }
+
+ if (ctype_alnum($ch) || $ch == '-' || $ch == '_') {
+ // It's a character
+ $this->value = $ch; //strtolower($ch);
+ $this->token = $tok = Token::char;
+ return $tok;
+ }
+
+ $this->value = $ch;
+
+ switch($ch) {
+ case '*':
+ $tok = Token::star;
+ break;
+ case chr(ord('>')):
+ $tok = Token::rangle;
+ break;
+ case '.':
+ $tok = Token::dot;
+ break;
+ case '#':
+ $tok = Token::octo;
+ break;
+ case '[':
+ $tok = Token::lsquare;
+ break;
+ case ']':
+ $tok = Token::rsquare;
+ break;
+ case ':':
+ $tok = Token::colon;
+ break;
+ case '(':
+ $tok = Token::lparen;
+ break;
+ case ')':
+ $tok = Token::rparen;
+ break;
+ case '+':
+ $tok = Token::plus;
+ break;
+ case '~':
+ $tok = Token::tilde;
+ break;
+ case '=':
+ $tok = Token::eq;
+ break;
+ case '|':
+ $tok = Token::pipe;
+ break;
+ case ',':
+ $tok = Token::comma;
+ break;
+ case chr(34):
+ $tok = Token::quote;
+ break;
+ case "'":
+ $tok = Token::squote;
+ break;
+ case '\\':
+ $tok = Token::bslash;
+ break;
+ case '^':
+ $tok = Token::carat;
+ break;
+ case '$':
+ $tok = Token::dollar;
+ break;
+ case '@':
+ $tok = Token::at;
+ break;
+ }
+
+
+ // Catch all characters that are legal within strings.
+ if ($tok == -1) {
+ // TODO: This should be UTF-8 compatible, but PHP doesn't
+ // have a native UTF-8 string. Should we use external
+ // mbstring library?
+
+ $ord = ord($ch);
+ // Characters in this pool are legal for use inside of
+ // certain strings. Extended ASCII is used here, though I
+ // Don't know if these are really legal.
+ if (($ord >= 32 && $ord <= 126) || ($ord >= 128 && $ord <= 255)) {
+ $tok = Token::stringLegal;
+ }
+ else {
+ throw new ParseException('Illegal character found in stream: ' . $ord);
+ }
+ }
+
+ $this->token = $tok;
+ return $tok;
+ }
+
+ /**
+ * Get a name string from the input stream.
+ * A name string must be composed of
+ * only characters defined in Token:char: -_a-zA-Z0-9
+ */
+ public function getNameString() {
+ $buf = '';
+ while ($this->token === Token::char) {
+ $buf .= $this->value;
+ $this->nextToken();
+ //print '_';
+ }
+ return $buf;
+ }
+
+ /**
+ * This gets a string with any legal 'string' characters.
+ * See CSS Selectors specification, section 11, for the
+ * definition of string.
+ *
+ * This will check for string1, string2, and the case where a
+ * string is unquoted (Oddly absent from the "official" grammar,
+ * though such strings are present as examples in the spec.)
+ *
+ * Note:
+ * Though the grammar supplied by CSS 3 Selectors section 11 does not
+ * address the contents of a pseudo-class value, the spec itself indicates
+ * that a pseudo-class value is a "value between parenthesis" [6.6]. The
+ * examples given use URLs among other things, making them closer to the
+ * definition of 'string' than to 'name'. So we handle them here as strings.
+ */
+ public function getQuotedString() {
+ if ($this->token == Token::quote || $this->token == Token::squote || $this->token == Token::lparen) {
+ $end = ($this->token == Token::lparen) ? Token::rparen : $this->token;
+ $buf = '';
+ $escape = FALSE;
+
+ $this->nextToken(); // Skip the opening quote/paren
+
+ // The second conjunct is probably not necessary.
+ while ($this->token !== FALSE && $this->token > -1) {
+ //print "Char: $this->value \n";
+ if ($this->token == Token::bslash && !$escape) {
+ // XXX: The backslash (\) is removed here.
+ // Turn on escaping.
+ //$buf .= $this->value;
+ $escape = TRUE;
+ }
+ elseif ($escape) {
+ // Turn off escaping
+ $buf .= $this->value;
+ $escape = FALSE;
+ }
+ elseif ($this->token === $end) {
+ // At end of string; skip token and break.
+ $this->nextToken();
+ break;
+ }
+ else {
+ // Append char.
+ $buf .= $this->value;
+ }
+ $this->nextToken();
+ }
+ return $buf;
+ }
+ }
+
+ // Get the contents inside of a pseudoClass().
+ public function getPseudoClassString() {
+ if ($this->token == Token::quote || $this->token == Token::squote || $this->token == Token::lparen) {
+ $end = ($this->token == Token::lparen) ? Token::rparen : $this->token;
+ $buf = '';
+ $escape = FALSE;
+
+ $this->nextToken(); // Skip the opening quote/paren
+
+ // The second conjunct is probably not necessary.
+ while ($this->token !== FALSE && $this->token > -1) {
+ //print "Char: $this->value \n";
+ if ($this->token == Token::bslash && !$escape) {
+ // XXX: The backslash (\) is removed here.
+ // Turn on escaping.
+ //$buf .= $this->value;
+ $escape = TRUE;
+ }
+ elseif ($escape) {
+ // Turn off escaping
+ $buf .= $this->value;
+ $escape = FALSE;
+ }
+ // Allow nested pseudoclasses.
+ elseif ($this->token == Token::lparen) {
+ $buf .= "(";
+ $buf .= $this->getPseudoClassString();
+ $buf .= ")";
+ }
+ elseif ($this->token === $end) {
+ // At end of string; skip token and break.
+ $this->nextToken();
+ break;
+ }
+ else {
+ // Append char.
+ $buf .= $this->value;
+ }
+ $this->nextToken();
+ }
+ return $buf;
+ }
+ }
+
+ /**
+ * Get a string from the input stream.
+ * This is a convenience function for getting a string of
+ * characters that are either alphanumber or whitespace. See
+ * the Token::white and Token::char definitions.
+ *
+ * @deprecated This is not used anywhere in QueryPath.
+ *//*
+ public function getStringPlusWhitespace() {
+ $buf = '';
+ if($this->token === FALSE) {return '';}
+ while ($this->token === Token::char || $this->token == Token::white) {
+ $buf .= $this->value;
+ $this->nextToken();
+ }
+ return $buf;
+ }*/
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/Selector.php b/lib/querypath/src/QueryPath/CSS/Selector.php
new file mode 100644
index 0000000..4b538bd
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/Selector.php
@@ -0,0 +1,144 @@
+<?php
+/** @file
+ * A selector.
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * A CSS Selector.
+ *
+ * A CSS selector is made up of one or more Simple Selectors
+ * (SimpleSelector).
+ *
+ * @attention
+ * The Selector data structure is a LIFO (Last in, First out). This is
+ * because CSS selectors are best processed "bottom up". Thus, when
+ * iterating over 'a>b>c', the iterator will produce:
+ * - c
+ * - b
+ * - a
+ * It is assumed, therefore, that any suitable querying engine will
+ * traverse from the bottom (`c`) back up.
+ *
+ * @b Usage
+ *
+ * This class is an event handler. It can be plugged into an Parser and
+ * receive the events the Parser generates.
+ *
+ * This class is also an iterator. Once the parser has completed, the
+ * captured selectors can be iterated over.
+ *
+ * @code
+ * <?php
+ * $selectorList = new \QueryPath\CSS\Selector();
+ * $parser = new \QueryPath\CSS\Parser($selector, $selectorList);
+ *
+ * $parser->parse();
+ *
+ * foreach ($selectorList as $simpleSelector) {
+ * // Do something with the SimpleSelector.
+ * print_r($simpleSelector);
+ * }
+ * ?>
+ * @endode
+ *
+ *
+ * @since QueryPath 3.0.0
+ */
+class Selector implements EventHandler, \IteratorAggregate, \Countable {
+ protected $selectors = array();
+ protected $currSelector;
+ protected $selectorGroups = array();
+ protected $groupIndex = 0;
+
+ public function __construct() {
+ $this->currSelector = new SimpleSelector();
+
+ $this->selectors[$this->groupIndex][] = $this->currSelector;
+ }
+
+ public function getIterator() {
+ return new \ArrayIterator($this->selectors);
+ }
+
+ /**
+ * Get the array of SimpleSelector objects.
+ *
+ * Normally, one iterates over a Selector. However, if it is
+ * necessary to get the selector array and manipulate it, this
+ * method can be used.
+ */
+ public function toArray() {
+ return $this->selectors;
+ }
+
+ public function count() {
+ return count($this->selectors);
+ }
+
+ public function elementID($id) {
+ $this->currSelector->id = $id;
+ }
+ public function element($name) {
+ $this->currSelector->element = $name;
+ }
+ public function elementNS($name, $namespace = NULL) {
+ $this->currSelector->ns = $namespace;
+ $this->currSelector->element = $name;
+ }
+ public function anyElement() {
+ $this->currSelector->element = '*';
+ }
+ public function anyElementInNS($ns) {
+ $this->currSelector->ns = $ns;
+ $this->currSelector->element = '*';
+ }
+ public function elementClass($name) {
+ $this->currSelector->classes[] = $name;
+ }
+ public function attribute($name, $value = NULL, $operation = EventHandler::isExactly) {
+ $this->currSelector->attributes[] = array(
+ 'name' => $name,
+ 'value' => $value,
+ 'op' => $operation,
+ );
+ }
+ public function attributeNS($name, $ns, $value = NULL, $operation = EventHandler::isExactly) {
+ $this->currSelector->attributes[] = array(
+ 'name' => $name,
+ 'value' => $value,
+ 'op' => $operation,
+ 'ns' => $ns,
+ );
+ }
+ public function pseudoClass($name, $value = NULL) {
+ $this->currSelector->pseudoClasses[] = array('name' => $name, 'value' => $value);
+ }
+ public function pseudoElement($name) {
+ $this->currSelector->pseudoElements[] = $name;
+ }
+ public function combinator($combinatorName) {
+ $this->currSelector->combinator = $combinatorName;
+ $this->currSelector = new SimpleSelector();
+ array_unshift($this->selectors[$this->groupIndex], $this->currSelector);
+ //$this->selectors[]= $this->currSelector;
+ }
+ public function directDescendant() {
+ $this->combinator(SimpleSelector::directDescendant);
+ }
+ public function adjacent() {
+ $this->combinator(SimpleSelector::adjacent);
+ }
+ public function anotherSelector() {
+ $this->groupIndex++;
+ $this->currSelector = new SimpleSelector();
+ $this->selectors[$this->groupIndex] = array($this->currSelector);
+ }
+ public function sibling() {
+ $this->combinator(SimpleSelector::sibling);
+ }
+ public function anyDescendant() {
+ $this->combinator(SimpleSelector::anyDescendant);
+ }
+}
diff --git a/lib/querypath/src/QueryPath/CSS/SimpleSelector.php b/lib/querypath/src/QueryPath/CSS/SimpleSelector.php
new file mode 100644
index 0000000..3fcc796
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/SimpleSelector.php
@@ -0,0 +1,138 @@
+<?php
+/** @file
+ *
+ * A simple selector.
+ *
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * Models a simple selector.
+ *
+ * CSS Selectors are composed of one or more simple selectors, where
+ * each simple selector may have any of the following components:
+ *
+ * - An element name (or wildcard *)
+ * - An ID (#foo)
+ * - One or more classes (.foo.bar)
+ * - One or more attribute matchers ([foo=bar])
+ * - One or more pseudo-classes (:foo)
+ * - One or more pseudo-elements (::first)
+ *
+ * For performance reasons, this object has been kept as sparse as
+ * possible.
+ *
+ * @since QueryPath 3.x
+ * @author M Butcher
+ *
+ */
+class SimpleSelector {
+
+ const adjacent = 1;
+ const directDescendant = 2;
+ const anotherSelector = 4;
+ const sibling = 8;
+ const anyDescendant = 16;
+
+ public $element;
+ public $ns;
+ public $id;
+ public $classes = array();
+ public $attributes = array();
+ public $pseudoClasses = array();
+ public $pseudoElements = array();
+ public $combinator;
+
+ public static function attributeOperator($code) {
+ switch($code) {
+ case EventHandler::containsWithSpace:
+ return '~=';
+ case EventHandler::containsWithHyphen:
+ return '|=';
+ case EventHandler::containsInString:
+ return '*=';
+ case EventHandler::beginsWith:
+ return '^=';
+ case EventHandler::endsWith:
+ return '$=';
+ default:
+ return '=';
+ }
+ }
+
+ public static function combinatorOperator($code) {
+ switch ($code) {
+ case self::adjacent:
+ return '+';
+ case self::directDescendant:
+ return '>';
+ case self::sibling:
+ return '~';
+ case self::anotherSelector:
+ return ', ';
+ case self::anyDescendant:
+ return ' ';
+ }
+ }
+
+ public function __construct() {
+ }
+
+ public function notEmpty() {
+ return !empty($element)
+ && !empty($id)
+ && !empty($classes)
+ && !empty($combinator)
+ && !empty($attributes)
+ && !empty($pseudoClasses)
+ && !empty($pseudoElements)
+ ;
+ }
+
+ public function __tostring() {
+ $buffer = array();
+ try {
+
+ if (!empty($this->ns)) {
+ $buffer[] = $this->ns; $buffer[] = '|';
+ }
+ if (!empty($this->element)) $buffer[] = $this->element;
+ if (!empty($this->id)) $buffer[] = '#' . $this->id;
+ if (!empty($this->attributes)) {
+ foreach ($this->attributes as $attr) {
+ $buffer[] = '[';
+ if(!empty($attr['ns'])) $buffer[] = $attr['ns'] . '|';
+ $buffer[] = $attr['name'];
+ if (!empty($attr['value'])) {
+ $buffer[] = self::attributeOperator($attr['op']);
+ $buffer[] = $attr['value'];
+ }
+ $buffer[] = ']';
+ }
+ }
+ if (!empty($this->pseudoClasses)) {
+ foreach ($this->pseudoClasses as $ps) {
+ $buffer[] = ':' . $ps['name'];
+ if (isset($ps['value'])) {
+ $buffer[] = '(' . $ps['value'] . ')';
+ }
+ }
+ }
+ foreach ($this->pseudoElements as $pe) {
+ $buffer[] = '::' . $pe;
+ }
+
+ if (!empty($this->combinator)) {
+ $buffer[] = self::combinatorOperator($this->combinator);
+ }
+
+ }
+ catch (\Exception $e) {
+ return $e->getMessage();
+ }
+
+ return implode('', $buffer);
+ }
+
+}
diff --git a/lib/querypath/src/QueryPath/CSS/Token.php b/lib/querypath/src/QueryPath/CSS/Token.php
new file mode 100644
index 0000000..3c31ef4
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/Token.php
@@ -0,0 +1,60 @@
+<?php
+/** @file
+ * Parser tokens.
+ */
+namespace QueryPath\CSS;
+/**
+ * Tokens for CSS.
+ * This class defines the recognized tokens for the parser, and also
+ * provides utility functions for error reporting.
+ *
+ * @ingroup querypath_css
+ */
+final class Token {
+ const char = 0;
+ const star = 1;
+ const rangle = 2;
+ const dot = 3;
+ const octo = 4;
+ const rsquare = 5;
+ const lsquare = 6;
+ const colon = 7;
+ const rparen = 8;
+ const lparen = 9;
+ const plus = 10;
+ const tilde = 11;
+ const eq = 12;
+ const pipe = 13;
+ const comma = 14;
+ const white = 15;
+ const quote = 16;
+ const squote = 17;
+ const bslash = 18;
+ const carat = 19;
+ const dollar = 20;
+ const at = 21; // This is not in the spec. Apparently, old broken CSS uses it.
+
+ // In legal range for string.
+ const stringLegal = 99;
+
+ /**
+ * Get a name for a given constant. Used for error handling.
+ */
+ static function name($const_int) {
+ $a = array('character', 'star', 'right angle bracket',
+ 'dot', 'octothorp', 'right square bracket', 'left square bracket',
+ 'colon', 'right parenthesis', 'left parenthesis', 'plus', 'tilde',
+ 'equals', 'vertical bar', 'comma', 'space', 'quote', 'single quote',
+ 'backslash', 'carat', 'dollar', 'at');
+ if (isset($a[$const_int]) && is_numeric($const_int)) {
+ return $a[$const_int];
+ }
+ elseif ($const_int == 99) {
+ return 'a legal non-alphanumeric character';
+ }
+ elseif ($const_int == FALSE) {
+ return 'end of file';
+ }
+ return sprintf('illegal character (%s)', $const_int);
+ }
+}
diff --git a/lib/querypath/src/QueryPath/CSS/Traverser.php b/lib/querypath/src/QueryPath/CSS/Traverser.php
new file mode 100644
index 0000000..6d16d63
--- /dev/null
+++ b/lib/querypath/src/QueryPath/CSS/Traverser.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * @file
+ * The main Traverser interface.
+ */
+
+namespace QueryPath\CSS;
+
+/**
+ * An object capable of walking (and searching) a datastructure.
+ */
+interface Traverser {
+ /**
+ * Process a CSS selector and find matches.
+ *
+ * This specifies a query to be run by the Traverser. A given
+ * Traverser may, in practice, delay the finding until some later time
+ * but must return the found results when getMatches() is called.
+ *
+ * @param string $selector
+ * A selector. Typically this is a CSS 3 Selector.
+ * @return \Traverser
+ * The Traverser that can return matches.
+ */
+ public function find($selector);
+ /**
+ * Get the results of a find() operation.
+ *
+ * Return an array of matching items.
+ *
+ * @return array
+ * An array of matched values. The specific data type in the matches
+ * will differ depending on the data type searched, but in the core
+ * QueryPath implementation, this will be an array of DOMNode
+ * objects.
+ */
+ public function matches();
+}
diff --git a/lib/querypath/src/QueryPath/DOMQuery.php b/lib/querypath/src/QueryPath/DOMQuery.php
new file mode 100644
index 0000000..15ab893
--- /dev/null
+++ b/lib/querypath/src/QueryPath/DOMQuery.php
@@ -0,0 +1,3993 @@
+<?php
+/**
+ * @file
+ * This houses the class formerly called QueryPath.
+ *
+ * As of QueryPath 3.0.0, the class was renamed QueryPath::DOMQuery. This
+ * was done for a few reasons:
+ * - The library has been refactored, and it made more sense to call the top
+ * level class QueryPath. This is not the top level class.
+ * - There have been requests for a JSONQuery class, which would be the
+ * natural complement of DOMQuery.
+ */
+
+namespace QueryPath;
+
+use \QueryPath\CSS\QueryPathEventHandler;
+use \QueryPath;
+use \Masterminds\HTML5;
+
+
+/**
+ * The DOMQuery object is the primary tool in this library.
+ *
+ * To create a new DOMQuery, use QueryPath::with() or qp() function.
+ *
+ * If you are new to these documents, start at the QueryPath.php page.
+ * There you will find a quick guide to the tools contained in this project.
+ *
+ * A note on serialization: DOMQuery uses DOM classes internally, and those
+ * do not serialize well at all. In addition, DOMQuery may contain many
+ * extensions, and there is no guarantee that extensions can serialize. The
+ * moral of the story: Don't serialize DOMQuery.
+ *
+ * @see qp()
+ * @see QueryPath.php
+ * @ingroup querypath_core
+ */
+class DOMQuery implements \QueryPath\Query, \IteratorAggregate, \Countable {
+
+ /**
+ * Default parser flags.
+ *
+ * These are flags that will be used if no global or local flags override them.
+ * @since 2.0
+ */
+ const DEFAULT_PARSER_FLAGS = NULL;
+
+ const JS_CSS_ESCAPE_CDATA = '\\1';
+ const JS_CSS_ESCAPE_CDATA_CCOMMENT = '/* \\1 */';
+ const JS_CSS_ESCAPE_CDATA_DOUBLESLASH = '// \\1';
+ const JS_CSS_ESCAPE_NONE = '';
+
+ //const IGNORE_ERRORS = 1544; //E_NOTICE | E_USER_WARNING | E_USER_NOTICE;
+ private $errTypes = 771; //E_ERROR; | E_USER_ERROR;
+
+ /**
+ * The base DOMDocument.
+ */
+ protected $document = NULL;
+ private $options = array(
+ 'parser_flags' => NULL,
+ 'omit_xml_declaration' => FALSE,
+ 'replace_entities' => FALSE,
+ 'exception_level' => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING
+ 'ignore_parser_warnings' => FALSE,
+ 'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT,
+ );
+ /**
+ * The array of matches.
+ */
+ protected $matches = array();
+ /**
+ * The last array of matches.
+ */
+ protected $last = array(); // Last set of matches.
+ private $ext = array(); // Extensions array.
+
+ /**
+ * The number of current matches.
+ *
+ * @see count()
+ */
+ public $length = 0;
+
+ /**
+ * Constructor.
+ *
+ * Typically, a new DOMQuery is created by QueryPath::with(), QueryPath::withHTML(),
+ * qp(), or htmlqp().
+ *
+ * @param mixed $document
+ * A document-like object.
+ * @param string $string
+ * A CSS 3 Selector
+ * @param array $options
+ * An associative array of options.
+ * @see qp()
+ */
+ public function __construct($document = NULL, $string = NULL, $options = array()) {
+ $string = trim($string);
+ $this->options = $options + Options::get() + $this->options;
+
+ $parser_flags = isset($options['parser_flags']) ? $options['parser_flags'] : self::DEFAULT_PARSER_FLAGS;
+ if (!empty($this->options['ignore_parser_warnings'])) {
+ // Don't convert parser warnings into exceptions.
+ $this->errTypes = 257; //E_ERROR | E_USER_ERROR;
+ }
+ elseif (isset($this->options['exception_level'])) {
+ // Set the error level at which exceptions will be thrown. By default,
+ // QueryPath will throw exceptions for
+ // E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING.
+ $this->errTypes = $this->options['exception_level'];
+ }
+
+ // Empty: Just create an empty QP.
+ if (empty($document)) {
+ $this->document = isset($this->options['encoding']) ? new \DOMDocument('1.0', $this->options['encoding']) : new \DOMDocument();
+ $this->setMatches(new \SplObjectStorage());
+ }
+ // Figure out if document is DOM, HTML/XML, or a filename
+ elseif (is_object($document)) {
+
+ // This is the most frequent object type.
+ if ($document instanceof \SplObjectStorage) {
+ $this->matches = $document;
+ if ($document->count() != 0) {
+ $first = $this->getFirstMatch();
+ if (!empty($first->ownerDocument)) {
+ $this->document = $first->ownerDocument;
+ }
+ }
+ }
+ elseif ($document instanceof DOMQuery) {
+ //$this->matches = $document->get(NULL, TRUE);
+ $this->setMatches($document->get(NULL, TRUE));
+ if ($this->matches->count() > 0)
+ $this->document = $this->getFirstMatch()->ownerDocument;
+ }
+ elseif ($document instanceof \DOMDocument) {
+ $this->document = $document;
+ //$this->matches = $this->matches($document->documentElement);
+ $this->setMatches($document->documentElement);
+ }
+ elseif ($document instanceof \DOMNode) {
+ $this->document = $document->ownerDocument;
+ //$this->matches = array($document);
+ $this->setMatches($document);
+ }
+ elseif ($document instanceof \Masterminds\HTML5) {
+ $this->document = $document;
+ $this->setMatches($document->documentElement);
+ }
+ elseif ($document instanceof \SimpleXMLElement) {
+ $import = dom_import_simplexml($document);
+ $this->document = $import->ownerDocument;
+ //$this->matches = array($import);
+ $this->setMatches($import);
+ }
+ else {
+ throw new \QueryPath\Exception('Unsupported class type: ' . get_class($document));
+ }
+ }
+ elseif (is_array($document)) {
+ //trigger_error('Detected deprecated array support', E_USER_NOTICE);
+ if (!empty($document) && $document[0] instanceof \DOMNode) {
+ $found = new \SplObjectStorage();
+ foreach ($document as $item) $found->attach($item);
+ //$this->matches = $found;
+ $this->setMatches($found);
+ $this->document = $this->getFirstMatch()->ownerDocument;
+ }
+ }
+ elseif ($this->isXMLish($document)) {
+ // $document is a string with XML
+ $this->document = $this->parseXMLString($document);
+ $this->setMatches($this->document->documentElement);
+ }
+ else {
+
+ // $document is a filename
+ $context = empty($options['context']) ? NULL : $options['context'];
+ $this->document = $this->parseXMLFile($document, $parser_flags, $context);
+ $this->setMatches($this->document->documentElement);
+ }
+
+ // Globally set the output option.
+ if (isset($this->options['format_output']) && $this->options['format_output'] == FALSE) {
+ $this->document->formatOutput = FALSE;
+ }
+ else {
+ $this->document->formatOutput = TRUE;
+ }
+
+ // Do a find if the second param was set.
+ if (isset($string) && strlen($string) > 0) {
+ // We don't issue a find because that creates a new DOMQuery.
+ //$this->find($string);
+
+ $query = new \QueryPath\CSS\DOMTraverser($this->matches);
+ $query->find($string);
+ $this->setMatches($query->matches());
+ }
+ }
+
+
+ /**
+ * Get the effective options for the current DOMQuery object.
+ *
+ * This returns an associative array of all of the options as set
+ * for the current DOMQuery object. This includes default options,
+ * options directly passed in via {@link qp()} or the constructor,
+ * an options set in the QueryPath::Options object.
+ *
+ * The order of merging options is this:
+ * - Options passed in using qp() are highest priority, and will
+ * override other options.
+ * - Options set with QueryPath::Options will override default options,
+ * but can be overridden by options passed into qp().
+ * - Default options will be used when no overrides are present.
+ *
+ * This function will return the options currently used, with the above option
+ * overriding having been calculated already.
+ *
+ * @return array
+ * An associative array of options, calculated from defaults and overridden
+ * options.
+ * @see qp()
+ * @see QueryPath::Options::set()
+ * @see QueryPath::Options::merge()
+ * @since 2.0
+ */
+ public function getOptions() {
+ return $this->options;
+ }
+
+ /**
+ * Select the root element of the document.
+ *
+ * This sets the current match to the document's root element. For
+ * practical purposes, this is the same as:
+ * @code
+ * qp($someDoc)->find(':root');
+ * @endcode
+ * However, since it doesn't invoke a parser, it has less overhead. It also
+ * works in cases where the QueryPath has been reduced to zero elements (a
+ * case that is not handled by find(':root') because there is no element
+ * whose root can be found).
+ *
+ * @param string $selector
+ * A selector. If this is supplied, QueryPath will navigate to the
+ * document root and then run the query. (Added in QueryPath 2.0 Beta 2)
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, wrapping the root element (document element)
+ * for the current document.
+ */
+ public function top($selector = NULL) {
+ //$this->setMatches($this->document->documentElement);
+ //return !empty($selector) ? $this->find($selector) : $this;
+ return $this->inst($this->document->documentElement, $selector, $this->options);
+ }
+
+ /**
+ * Given a CSS Selector, find matching items.
+ *
+ * @param string $selector
+ * CSS 3 Selector
+ * @return \QueryPath\DOMQuery
+ * @see filter()
+ * @see is()
+ * @todo If a find() returns zero matches, then a subsequent find() will
+ * also return zero matches, even if that find has a selector like :root.
+ * The reason for this is that the {@link QueryPathEventHandler} does
+ * not set the root of the document tree if it cannot find any elements
+ * from which to determine what the root is. The workaround is to use
+ * {@link top()} to select the root element again.
+ */
+ public function find($selector) {
+
+ //$query = new QueryPathEventHandler($this->matches);
+ $query = new \QueryPath\CSS\DOMTraverser($this->matches);
+ $query->find($selector);
+ //$this->setMatches($query->matches());
+ //return $this;
+ return $this->inst($query->matches(), NULL , $this->options);
+ }
+ public function findInPlace($selector) {
+ $query = new \QueryPath\CSS\DOMTraverser($this->matches);
+ $query->find($selector);
+ $this->setMatches($query->matches());
+ return $this;
+ }
+
+ /**
+ * Execute an XPath query and store the results in the QueryPath.
+ *
+ * Most methods in this class support CSS 3 Selectors. Sometimes, though,
+ * XPath provides a finer-grained query language. Use this to execute
+ * XPath queries.
+ *
+ * Beware, though. DOMQuery works best on DOM Elements, but an XPath
+ * query can return other nodes, strings, and values. These may not work with
+ * other QueryPath functions (though you will be able to access the
+ * values with {@link get()}).
+ *
+ * @param string $query
+ * An XPath query.
+ * @param array $options
+ * Currently supported options are:
+ * - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
+ * in conjunction with 'namespace_uri'
+ * - 'namespace_uri': The URI to be used as the default namespace URI. Used
+ * with 'namespace_prefix'
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery object wrapping the results of the query.
+ * @see find()
+ * @author M Butcher
+ * @author Xavier Prud'homme
+ */
+ public function xpath($query, $options = array()) {
+ $xpath = new \DOMXPath($this->document);
+
+ // Register a default namespace.
+ if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
+ $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
+ }
+
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $item) {
+ $nl = $xpath->query($query, $item);
+ if ($nl->length > 0) {
+ for ($i = 0; $i < $nl->length; ++$i) $found->attach($nl->item($i));
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ //$this->setMatches($found);
+ //return $this;
+ }
+
+ /**
+ * Get the number of elements currently wrapped by this object.
+ *
+ * Note that there is no length property on this object.
+ *
+ * @return int
+ * Number of items in the object.
+ * @deprecated QueryPath now implements Countable, so use count().
+ */
+ public function size() {
+ return $this->matches->count();
+ }
+
+ /**
+ * Get the number of elements currently wrapped by this object.
+ *
+ * Since DOMQuery is Countable, the PHP count() function can also
+ * be used on a DOMQuery.
+ *
+ * @code
+ * <?php
+ * count(qp($xml, 'div'));
+ * ?>
+ * @endcode
+ *
+ * @return int
+ * The number of matches in the DOMQuery.
+ */
+ public function count() {
+ return $this->matches->count();
+ }
+
+ /**
+ * Get one or all elements from this object.
+ *
+ * When called with no paramaters, this returns all objects wrapped by
+ * the DOMQuery. Typically, these are DOMElement objects (unless you have
+ * used map(), xpath(), or other methods that can select
+ * non-elements).
+ *
+ * When called with an index, it will return the item in the DOMQuery with
+ * that index number.
+ *
+ * Calling this method does not change the DOMQuery (e.g. it is
+ * non-destructive).
+ *
+ * You can use qp()->get() to iterate over all elements matched. You can
+ * also iterate over qp() itself (DOMQuery implementations must be Traversable).
+ * In the later case, though, each item
+ * will be wrapped in a DOMQuery object. To learn more about iterating
+ * in QueryPath, see {@link examples/techniques.php}.
+ *
+ * @param int $index
+ * If specified, then only this index value will be returned. If this
+ * index is out of bounds, a NULL will be returned.
+ * @param boolean $asObject
+ * If this is TRUE, an SplObjectStorage object will be returned
+ * instead of an array. This is the preferred method for extensions to use.
+ * @return mixed
+ * If an index is passed, one element will be returned. If no index is
+ * present, an array of all matches will be returned.
+ * @see eq()
+ * @see SplObjectStorage
+ */
+ public function get($index = NULL, $asObject = FALSE) {
+ if (isset($index)) {
+ return ($this->size() > $index) ? $this->getNthMatch($index) : NULL;
+ }
+ // Retain support for legacy.
+ if (!$asObject) {
+ $matches = array();
+ foreach ($this->matches as $m) $matches[] = $m;
+ return $matches;
+ }
+ return $this->matches;
+ }
+
+ /**
+ * Get the namespace of the current element.
+ *
+ * If QP is currently pointed to a list of elements, this will get the
+ * namespace of the first element.
+ */
+ public function ns() {
+ return $this->get(0)->namespaceURI;
+ }
+
+ /**
+ * Get the DOMDocument that we currently work with.
+ *
+ * This returns the current DOMDocument. Any changes made to this document will be
+ * accessible to DOMQuery, as both will share access to the same object.
+ *
+ * @return DOMDocument
+ */
+ public function document() {
+ return $this->document;
+ }
+
+ /**
+ * On an XML document, load all XIncludes.
+ *
+ * @return \QueryPath\DOMQuery
+ */
+ public function xinclude() {
+ $this->document->xinclude();
+ return $this;
+ }
+
+ /**
+ * Get all current elements wrapped in an array.
+ * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
+ * with no parameters.
+ *
+ * @return array
+ * An array of DOMNodes (typically DOMElements).
+ */
+ public function toArray() {
+ return $this->get();
+ }
+ /**
+ * Get/set an attribute.
+ * - If no parameters are specified, this returns an associative array of all
+ * name/value pairs.
+ * - If both $name and $value are set, then this will set the attribute name/value
+ * pair for all items in this object.
+ * - If $name is set, and is an array, then
+ * all attributes in the array will be set for all items in this object.
+ * - If $name is a string and is set, then the attribute value will be returned.
+ *
+ * When an attribute value is retrieved, only the attribute value of the FIRST
+ * match is returned.
+ *
+ * @param mixed $name
+ * The name of the attribute or an associative array of name/value pairs.
+ * @param string $value
+ * A value (used only when setting an individual property).
+ * @return mixed
+ * If this was a setter request, return the DOMQuery object. If this was
+ * an access request (getter), return the string value.
+ * @see removeAttr()
+ * @see tag()
+ * @see hasAttr()
+ * @see hasClass()
+ */
+ public function attr($name = NULL, $value = NULL) {
+
+ // Default case: Return all attributes as an assoc array.
+ if (is_null($name)) {
+ if ($this->matches->count() == 0) return NULL;
+ $ele = $this->getFirstMatch();
+ $buffer = array();
+
+ // This does not appear to be part of the DOM
+ // spec. Nor is it documented. But it works.
+ foreach ($ele->attributes as $name => $attrNode) {
+ $buffer[$name] = $attrNode->value;
+ }
+ return $buffer;
+ }
+
+ // multi-setter
+ if (is_array($name)) {
+ foreach ($name as $k => $v) {
+ foreach ($this->matches as $m) $m->setAttribute($k, $v);
+ }
+ return $this;
+ }
+ // setter
+ if (isset($value)) {
+ foreach ($this->matches as $m) $m->setAttribute($name, $value);
+ return $this;
+ }
+
+ //getter
+ if ($this->matches->count() == 0) return NULL;
+
+ // Special node type handler:
+ if ($name == 'nodeType') {
+ return $this->getFirstMatch()->nodeType;
+ }
+
+ // Always return first match's attr.
+ return $this->getFirstMatch()->getAttribute($name);
+ }
+ /**
+ * Check to see if the given attribute is present.
+ *
+ * This returns TRUE if <em>all</em> selected items have the attribute, or
+ * FALSE if at least one item does not have the attribute.
+ *
+ * @param string $attrName
+ * The attribute name.
+ * @return boolean
+ * TRUE if all matches have the attribute, FALSE otherwise.
+ * @since 2.0
+ * @see attr()
+ * @see hasClass()
+ */
+ public function hasAttr($attrName) {
+ foreach ($this->matches as $match) {
+ if (!$match->hasAttribute($attrName)) return FALSE;
+ }
+ return TRUE;
+ }
+
+ /**
+ * Set/get a CSS value for the current element(s).
+ * This sets the CSS value for each element in the DOMQuery object.
+ * It does this by setting (or getting) the style attribute (without a namespace).
+ *
+ * For example, consider this code:
+ * @code
+ * <?php
+ * qp(HTML_STUB, 'body')->css('background-color','red')->html();
+ * ?>
+ * @endcode
+ * This will return the following HTML:
+ * @code
+ * <body style="background-color: red"/>
+ * @endcode
+ *
+ * If no parameters are passed into this function, then the current style
+ * element will be returned unparsed. Example:
+ * @code
+ * <?php
+ * qp(HTML_STUB, 'body')->css('background-color','red')->css();
+ * ?>
+ * @endcode
+ * This will return the following:
+ * @code
+ * background-color: red
+ * @endcode
+ *
+ * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
+ * (In previous versions of QueryPath, a call to css() overwrite the existing style
+ * values).
+ *
+ * @param mixed $name
+ * If this is a string, it will be used as a CSS name. If it is an array,
+ * this will assume it is an array of name/value pairs of CSS rules. It will
+ * apply all rules to all elements in the set.
+ * @param string $value
+ * The value to set. This is only set if $name is a string.
+ * @return \QueryPath\DOMQuery
+ */
+ public function css($name = NULL, $value = '') {
+ if (empty($name)) {
+ return $this->attr('style');
+ }
+
+ // Get any existing CSS.
+ $css = array();
+ foreach ($this->matches as $match) {
+ $style = $match->getAttribute('style');
+ if (!empty($style)) {
+ // XXX: Is this sufficient?
+ $style_array = explode(';', $style);
+ foreach ($style_array as $item) {
+ $item = trim($item);
+
+ // Skip empty attributes.
+ if (strlen($item) == 0) continue;
+
+ list($css_att, $css_val) = explode(':',$item, 2);
+ $css[$css_att] = trim($css_val);
+ }
+ }
+ }
+
+ if (is_array($name)) {
+ // Use array_merge instead of + to preserve order.
+ $css = array_merge($css, $name);
+ }
+ else {
+ $css[$name] = $value;
+ }
+
+ // Collapse CSS into a string.
+ $format = '%s: %s;';
+ $css_string = '';
+ foreach ($css as $n => $v) {
+ $css_string .= sprintf($format, $n, trim($v));
+ }
+
+ $this->attr('style', $css_string);
+ return $this;
+ }
+
+ /**
+ * Insert or retrieve a Data URL.
+ *
+ * When called with just $attr, it will fetch the result, attempt to decode it, and
+ * return an array with the MIME type and the application data.
+ *
+ * When called with both $attr and $data, it will inject the data into all selected elements
+ * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
+ * the given PNG image into the selected elements.
+ *
+ * The current implementation only knows how to encode and decode Base 64 data.
+ *
+ * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
+ *
+ * @param string $attr
+ * The name of the attribute.
+ * @param mixed $data
+ * The contents to inject as the data. The value can be any one of the following:
+ * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
+ * MUST BE A FULL URL, not a relative path.
+ * - A string of data: If this is given, then the subsystem will encode the string.
+ * - A stream or file handle: If this is given, the stream's contents will be encoded
+ * and inserted as data.
+ * (Note that we make the assumption here that you would never want to set data to be
+ * a URL. If this is an incorrect assumption, file a bug.)
+ * @param string $mime
+ * The MIME type of the document.
+ * @param resource $context
+ * A valid context. Use this only if you need to pass a stream context. This is only necessary
+ * if $data is a URL. (See {@link stream_context_create()}).
+ * @return \QueryPath\DOMQuery|string
+ * If this is called as a setter, this will return a DOMQuery object. Otherwise, it
+ * will attempt to fetch data out of the attribute and return that.
+ * @see http://en.wikipedia.org/wiki/Data:_URL
+ * @see attr()
+ * @since 2.1
+ */
+ public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL) {
+ if (is_null($data)) {
+ // Attempt to fetch the data
+ $data = $this->attr($attr);
+ if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
+ return;
+ }
+
+ // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
+ $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
+ $matches = array();
+ preg_match($regex, $data, $matches);
+
+ if (!empty($matches)) {
+ $result = array(
+ 'mime' => $matches[1] . '/' . $matches[2],
+ 'data' => base64_decode($matches[3]),
+ );
+ return $result;
+ }
+ }
+ else {
+ $attVal = \QueryPath::encodeDataURL($data, $mime, $context);
+ return $this->attr($attr, $attVal);
+ }
+ }
+
+ /**
+ * Remove the named attribute from all elements in the current DOMQuery.
+ *
+ * This will remove any attribute with the given name. It will do this on each
+ * item currently wrapped by DOMQuery.
+ *
+ * As is the case in jQuery, this operation is not considered destructive.
+ *
+ * @param string $name
+ * Name of the parameter to remove.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the same elements.
+ * @see attr()
+ */
+ public function removeAttr($name) {
+ foreach ($this->matches as $m) {
+ //if ($m->hasAttribute($name))
+ $m->removeAttribute($name);
+ }
+ return $this;
+ }
+ /**
+ * Reduce the matched set to just one.
+ *
+ * This will take a matched set and reduce it to just one item -- the item
+ * at the index specified. This is a destructive operation, and can be undone
+ * with {@link end()}.
+ *
+ * @param $index
+ * The index of the element to keep. The rest will be
+ * discarded.
+ * @return \QueryPath\DOMQuery
+ * @see get()
+ * @see is()
+ * @see end()
+ */
+ public function eq($index) {
+ return $this->inst($this->getNthMatch($index), NULL, $this->options);
+ // XXX: Might there be a more efficient way of doing this?
+ //$this->setMatches($this->getNthMatch($index));
+ //return $this;
+ }
+ /**
+ * Given a selector, this checks to see if the current set has one or more matches.
+ *
+ * Unlike jQuery's version, this supports full selectors (not just simple ones).
+ *
+ * @param string $selector
+ * The selector to search for. As of QueryPath 2.1.1, this also supports passing a
+ * DOMNode object.
+ * @return boolean
+ * TRUE if one or more elements match. FALSE if no match is found.
+ * @see get()
+ * @see eq()
+ */
+ public function is($selector) {
+
+ if (is_object($selector)) {
+ if ($selector instanceof \DOMNode) {
+ return count($this->matches) == 1 && $selector->isSameNode($this->get(0));
+ }
+ elseif ($selector instanceof \Traversable) {
+ if (count($selector) != count($this->matches)) {
+ return FALSE;
+ }
+ // Without $seen, there is an edge case here if $selector contains the same object
+ // more than once, but the counts are equal. For example, [a, a, a, a] will
+ // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
+ $seen = new \SplObjectStorage();
+ foreach ($selector as $item) {
+ if (!$this->matches->contains($item) || $seen->contains($item)) {
+ return FALSE;
+ }
+ $seen->attach($item);
+ }
+ return TRUE;
+ }
+ throw new \QueryPath\Exception('Cannot compare an object to a DOMQuery.');
+ return FALSE;
+ }
+
+ // Testing based on Issue #70.
+ //fprintf(STDOUT, __FUNCTION__ .' found %d', $this->find($selector)->count());
+ return $this->branch($selector)->count() > 0;
+
+ // Old version:
+ //foreach ($this->matches as $m) {
+ //$q = new \QueryPath\CSS\QueryPathEventHandler($m);
+ //if ($q->find($selector)->getMatches()->count()) {
+ //return TRUE;
+ //}
+ //}
+ //return FALSE;
+ }
+ /**
+ * Filter a list down to only elements that match the selector.
+ * Use this, for example, to find all elements with a class, or with
+ * certain children.
+ *
+ * @param string $selector
+ * The selector to use as a filter.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery with non-matching items filtered out.
+ * @see filterLambda()
+ * @see filterCallback()
+ * @see map()
+ * @see find()
+ * @see is()
+ */
+ public function filter($selector) {
+
+ $found = new \SplObjectStorage();
+ $tmp = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ $tmp->attach($m);
+ // Seems like this should be right... but it fails unit
+ // tests. Need to compare to jQuery.
+ // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m);
+ $query = new \QueryPath\CSS\DOMTraverser($tmp);
+ $query->find($selector);
+ if (count($query->matches())) {
+ $found->attach($m);
+ }
+ $tmp->detach($m);
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Sort the contents of the QueryPath object.
+ *
+ * By default, this does not change the order of the elements in the
+ * DOM. Instead, it just sorts the internal list. However, if TRUE
+ * is passed in as the second parameter then QueryPath will re-order
+ * the DOM, too.
+ *
+ * @attention
+ * DOM re-ordering is done by finding the location of the original first
+ * item in the list, and then placing the sorted list at that location.
+ *
+ * The argument $compartor is a callback, such as a function name or a
+ * closure. The callback receives two DOMNode objects, which you can use
+ * as DOMNodes, or wrap in QueryPath objects.
+ *
+ * A simple callback:
+ * @code
+ * <?php
+ * $comp = function (\DOMNode $a, \DOMNode $b) {
+ * if ($a->textContent == $b->textContent) {
+ * return 0;
+ * }
+ * return $a->textContent > $b->textContent ? 1 : -1;
+ * };
+ * $qp = QueryPath::with($xml, $selector)->sort($comp);
+ * ?>
+ * @endcode
+ *
+ * The above sorts the matches into lexical order using the text of each node.
+ * If you would prefer to work with QueryPath objects instead of DOMNode
+ * objects, you may prefer something like this:
+ *
+ * @code
+ * <?php
+ * $comp = function (\DOMNode $a, \DOMNode $b) {
+ * $qpa = qp($a);
+ * $qpb = qp($b);
+ *
+ * if ($qpa->text() == $qpb->text()) {
+ * return 0;
+ * }
+ * return $qpa->text()> $qpb->text()? 1 : -1;
+ * };
+ *
+ * $qp = QueryPath::with($xml, $selector)->sort($comp);
+ * ?>
+ * @endcode
+ *
+ * @param callback $comparator
+ * A callback. This will be called during sorting to compare two DOMNode
+ * objects.
+ * @param boolean $modifyDOM
+ * If this is TRUE, the sorted results will be inserted back into
+ * the DOM at the position of the original first element.
+ * @return \QueryPath\DOMQuery
+ * This object.
+ */
+ public function sort($comparator, $modifyDOM = FALSE) {
+ // Sort as an array.
+ $list = iterator_to_array($this->matches);
+
+ if (empty($list)) {
+ return $this;
+ }
+
+ $oldFirst = $list[0];
+
+ usort($list, $comparator);
+
+ // Copy back into SplObjectStorage.
+ $found = new \SplObjectStorage();
+ foreach ($list as $node) {
+ $found->attach($node);
+ }
+ //$this->setMatches($found);
+
+
+ // Do DOM modifications only if necessary.
+ if ($modifyDOM) {
+ $placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_');
+ $placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst);
+ $len = count($list);
+ for ($i = 0; $i < $len; ++$i) {
+ $node = $list[$i];
+ $node = $node->parentNode->removeChild($node);
+ $placeholder->parentNode->insertBefore($node, $placeholder);
+ }
+ $placeholder->parentNode->removeChild($placeholder);
+ }
+
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Filter based on a lambda function.
+ *
+ * The function string will be executed as if it were the body of a
+ * function. It is passed two arguments:
+ * - $index: The index of the item.
+ * - $item: The current Element.
+ * If the function returns boolean FALSE, the item will be removed from
+ * the list of elements. Otherwise it will be kept.
+ *
+ * Example:
+ * @code
+ * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
+ * @endcode
+ *
+ * The above would filter down the list to only an item whose ID is
+ * 'text'.
+ *
+ * @param string $fn
+ * Inline lambda function in a string.
+ * @return \QueryPath\DOMQuery
+ * @see filter()
+ * @see map()
+ * @see mapLambda()
+ * @see filterCallback()
+ */
+ public function filterLambda($fn) {
+ $function = create_function('$index, $item', $fn);
+ $found = new \SplObjectStorage();
+ $i = 0;
+ foreach ($this->matches as $item)
+ if ($function($i++, $item) !== FALSE) $found->attach($item);
+
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Use regular expressions to filter based on the text content of matched elements.
+ *
+ * Only items that match the given regular expression will be kept. All others will
+ * be removed.
+ *
+ * The regular expression is run against the <i>text content</i> (the PCDATA) of the
+ * elements. This is a way of filtering elements based on their content.
+ *
+ * Example:
+ * @code
+ * <?xml version="1.0"?>
+ * <div>Hello <i>World</i></div>
+ * @endcode
+ *
+ * @code
+ * <?php
+ * // This will be 1.
+ * qp($xml, 'div')->filterPreg('/World/')->size();
+ * ?>
+ * @endcode
+ *
+ * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
+ * @codeHello World@endcode.
+ *
+ * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
+ *
+ * @param string $regex
+ * A regular expression.
+ * @return \QueryPath\DOMQuery
+ * @see filter()
+ * @see filterCallback()
+ * @see preg_match()
+ */
+ public function filterPreg($regex) {
+
+ $found = new \SplObjectStorage();
+
+ foreach ($this->matches as $item) {
+ if (preg_match($regex, $item->textContent) > 0) {
+ $found->attach($item);
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Filter based on a callback function.
+ *
+ * A callback may be any of the following:
+ * - a function: 'my_func'.
+ * - an object/method combo: $obj, 'myMethod'
+ * - a class/method combo: 'MyClass', 'myMethod'
+ * Note that classes are passed in strings. Objects are not.
+ *
+ * Each callback is passed to arguments:
+ * - $index: The index position of the object in the array.
+ * - $item: The item to be operated upon.
+ *
+ * If the callback function returns FALSE, the item will be removed from the
+ * set of matches. Otherwise the item will be considered a match and left alone.
+ *
+ * @param callback $callback.
+ * A callback either as a string (function) or an array (object, method OR
+ * classname, method).
+ * @return \QueryPath\DOMQuery
+ * Query path object augmented according to the function.
+ * @see filter()
+ * @see filterLambda()
+ * @see map()
+ * @see is()
+ * @see find()
+ */
+ public function filterCallback($callback) {
+ $found = new \SplObjectStorage();
+ $i = 0;
+ if (is_callable($callback)) {
+ foreach($this->matches as $item)
+ if (call_user_func($callback, $i++, $item) !== FALSE) $found->attach($item);
+ }
+ else {
+ throw new \QueryPath\Exception('The specified callback is not callable.');
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Filter a list to contain only items that do NOT match.
+ *
+ * @param string $selector
+ * A selector to use as a negation filter. If the filter is matched, the
+ * element will be removed from the list.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with matching items filtered out.
+ * @see find()
+ */
+ public function not($selector) {
+ $found = new \SplObjectStorage();
+ if ($selector instanceof \DOMElement) {
+ foreach ($this->matches as $m) if ($m !== $selector) $found->attach($m);
+ }
+ elseif (is_array($selector)) {
+ foreach ($this->matches as $m) {
+ if (!in_array($m, $selector, TRUE)) $found->attach($m);
+ }
+ }
+ elseif ($selector instanceof \SplObjectStorage) {
+ foreach ($this->matches as $m) if ($selector->contains($m)) $found->attach($m);
+ }
+ else {
+ foreach ($this->matches as $m) if (!QueryPath::with($m, NULL, $this->options)->is($selector)) $found->attach($m);
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get an item's index.
+ *
+ * Given a DOMElement, get the index from the matches. This is the
+ * converse of {@link get()}.
+ *
+ * @param DOMElement $subject
+ * The item to match.
+ *
+ * @return mixed
+ * The index as an integer (if found), or boolean FALSE. Since 0 is a
+ * valid index, you should use strong equality (===) to test..
+ * @see get()
+ * @see is()
+ */
+ public function index($subject) {
+
+ $i = 0;
+ foreach ($this->matches as $m) {
+ if ($m === $subject) {
+ return $i;
+ }
+ ++$i;
+ }
+ return FALSE;
+ }
+ /**
+ * Run a function on each item in a set.
+ *
+ * The mapping callback can return anything. Whatever it returns will be
+ * stored as a match in the set, though. This means that afer a map call,
+ * there is no guarantee that the elements in the set will behave correctly
+ * with other DOMQuery functions.
+ *
+ * Callback rules:
+ * - If the callback returns NULL, the item will be removed from the array.
+ * - If the callback returns an array, the entire array will be stored in
+ * the results.
+ * - If the callback returns anything else, it will be appended to the array
+ * of matches.
+ *
+ * @param callback $callback
+ * The function or callback to use. The callback will be passed two params:
+ * - $index: The index position in the list of items wrapped by this object.
+ * - $item: The current item.
+ *
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object wrapping a list of whatever values were returned
+ * by each run of the callback.
+ *
+ * @see DOMQuery::get()
+ * @see filter()
+ * @see find()
+ */
+ public function map($callback) {
+ $found = new \SplObjectStorage();
+
+ if (is_callable($callback)) {
+ $i = 0;
+ foreach ($this->matches as $item) {
+ $c = call_user_func($callback, $i, $item);
+ if (isset($c)) {
+ if (is_array($c) || $c instanceof \Iterable) {
+ foreach ($c as $retval) {
+ if (!is_object($retval)) {
+ $tmp = new \stdClass();
+ $tmp->textContent = $retval;
+ $retval = $tmp;
+ }
+ $found->attach($retval);
+ }
+ }
+ else {
+ if (!is_object($c)) {
+ $tmp = new \stdClass();
+ $tmp->textContent = $c;
+ $c = $tmp;
+ }
+ $found->attach($c);
+ }
+ }
+ ++$i;
+ }
+ }
+ else {
+ throw new \QueryPath\Exception('Callback is not callable.');
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Narrow the items in this object down to only a slice of the starting items.
+ *
+ * @param integer $start
+ * Where in the list of matches to begin the slice.
+ * @param integer $length
+ * The number of items to include in the slice. If nothing is specified, the
+ * all remaining matches (from $start onward) will be included in the sliced
+ * list.
+ * @return \QueryPath\DOMQuery
+ * @see array_slice()
+ */
+ public function slice($start, $length = 0) {
+ $end = $length;
+ $found = new \SplObjectStorage();
+ if ($start >= $this->size()) {
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ $i = $j = 0;
+ foreach ($this->matches as $m) {
+ if ($i >= $start) {
+ if ($end > 0 && $j >= $end) {
+ break;
+ }
+ $found->attach($m);
+ ++$j;
+ }
+ ++$i;
+ }
+
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Run a callback on each item in the list of items.
+ *
+ * Rules of the callback:
+ * - A callback is passed two variables: $index and $item. (There is no
+ * special treatment of $this, as there is in jQuery.)
+ * - You will want to pass $item by reference if it is not an
+ * object (DOMNodes are all objects).
+ * - A callback that returns FALSE will stop execution of the each() loop. This
+ * works like break in a standard loop.
+ * - A TRUE return value from the callback is analogous to a continue statement.
+ * - All other return values are ignored.
+ *
+ * @param callback $callback
+ * The callback to run.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery.
+ * @see eachLambda()
+ * @see filter()
+ * @see map()
+ */
+ public function each($callback) {
+ if (is_callable($callback)) {
+ $i = 0;
+ foreach ($this->matches as $item) {
+ if (call_user_func($callback, $i, $item) === FALSE) return $this;
+ ++$i;
+ }
+ }
+ else {
+ throw new \QueryPath\Exception('Callback is not callable.');
+ }
+ return $this;
+ }
+ /**
+ * An each() iterator that takes a lambda function.
+ *
+ * @deprecated
+ * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
+ * method is not necessary and should be avoided.
+ * @param string $lambda
+ * The lambda function. This will be passed ($index, &$item).
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object.
+ * @see each()
+ * @see filterLambda()
+ * @see filterCallback()
+ * @see map()
+ */
+ public function eachLambda($lambda) {
+ $index = 0;
+ foreach ($this->matches as $item) {
+ $fn = create_function('$index, &$item', $lambda);
+ if ($fn($index, $item) === FALSE) return $this;
+ ++$index;
+ }
+ return $this;
+ }
+ /**
+ * Insert the given markup as the last child.
+ *
+ * The markup will be inserted into each match in the set.
+ *
+ * The same element cannot be inserted multiple times into a document. DOM
+ * documents do not allow a single object to be inserted multiple times
+ * into the DOM. To insert the same XML repeatedly, we must first clone
+ * the object. This has one practical implication: Once you have inserted
+ * an element into the object, you cannot further manipulate the original
+ * element and expect the changes to be replciated in the appended object.
+ * (They are not the same -- there is no shared reference.) Instead, you
+ * will need to retrieve the appended object and operate on that.
+ *
+ * @param mixed $data
+ * This can be either a string (the usual case), or a DOM Element.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object.
+ * @see appendTo()
+ * @see prepend()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function append($data) {
+ $data = $this->prepareInsert($data);
+ if (isset($data)) {
+ if (empty($this->document->documentElement) && $this->matches->count() == 0) {
+ // Then we assume we are writing to the doc root
+ $this->document->appendChild($data);
+ $found = new \SplObjectStorage();
+ $found->attach($this->document->documentElement);
+ $this->setMatches($found);
+ }
+ else {
+ // You can only append in item once. So in cases where we
+ // need to append multiple times, we have to clone the node.
+ foreach ($this->matches as $m) {
+ // DOMDocumentFragments are even more troublesome, as they don't
+ // always clone correctly. So we have to clone their children.
+ if ($data instanceof \DOMDocumentFragment) {
+ foreach ($data->childNodes as $n)
+ $m->appendChild($n->cloneNode(TRUE));
+ }
+ else {
+ // Otherwise a standard clone will do.
+ $m->appendChild($data->cloneNode(TRUE));
+ }
+
+ }
+ }
+
+ }
+ return $this;
+ }
+ /**
+ * Append the current elements to the destination passed into the function.
+ *
+ * This cycles through all of the current matches and appends them to
+ * the context given in $destination. If a selector is provided then the
+ * $destination is queried (using that selector) prior to the data being
+ * appended. The data is then appended to the found items.
+ *
+ * @param DOMQuery $dest
+ * A DOMQuery object that will be appended to.
+ * @return \QueryPath\DOMQuery
+ * The original DOMQuery, unaltered. Only the destination DOMQuery will
+ * be modified.
+ * @see append()
+ * @see prependTo()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function appendTo(DOMQuery $dest) {
+ foreach ($this->matches as $m) $dest->append($m);
+ return $this;
+ }
+ /**
+ * Insert the given markup as the first child.
+ *
+ * The markup will be inserted into each match in the set.
+ *
+ * @param mixed $data
+ * This can be either a string (the usual case), or a DOM Element.
+ * @return \QueryPath\DOMQuery
+ * @see append()
+ * @see before()
+ * @see after()
+ * @see prependTo()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function prepend($data) {
+ $data = $this->prepareInsert($data);
+ if (isset($data)) {
+ foreach ($this->matches as $m) {
+ $ins = $data->cloneNode(TRUE);
+ if ($m->hasChildNodes())
+ $m->insertBefore($ins, $m->childNodes->item(0));
+ else
+ $m->appendChild($ins);
+ }
+ }
+ return $this;
+ }
+ /**
+ * Take all nodes in the current object and prepend them to the children nodes of
+ * each matched node in the passed-in DOMQuery object.
+ *
+ * This will iterate through each item in the current DOMQuery object and
+ * add each item to the beginning of the children of each element in the
+ * passed-in DOMQuery object.
+ *
+ * @see insertBefore()
+ * @see insertAfter()
+ * @see prepend()
+ * @see appendTo()
+ * @param DOMQuery $dest
+ * The destination DOMQuery object.
+ * @return \QueryPath\DOMQuery
+ * The original DOMQuery, unmodified. NOT the destination DOMQuery.
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function prependTo(DOMQuery $dest) {
+ foreach ($this->matches as $m) $dest->prepend($m);
+ return $this;
+ }
+
+ /**
+ * Insert the given data before each element in the current set of matches.
+ *
+ * This will take the give data (XML or HTML) and put it before each of the items that
+ * the DOMQuery object currently contains. Contrast this with after().
+ *
+ * @param mixed $data
+ * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
+ * or the other usual suspects. (See {@link qp()}).
+ * @return \QueryPath\DOMQuery
+ * Returns the DOMQuery with the new modifications. The list of elements currently
+ * selected will remain the same.
+ * @see insertBefore()
+ * @see after()
+ * @see append()
+ * @see prepend()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function before($data) {
+ $data = $this->prepareInsert($data);
+ foreach ($this->matches as $m) {
+ $ins = $data->cloneNode(TRUE);
+ $m->parentNode->insertBefore($ins, $m);
+ }
+
+ return $this;
+ }
+ /**
+ * Insert the current elements into the destination document.
+ * The items are inserted before each element in the given DOMQuery document.
+ * That is, they will be siblings with the current elements.
+ *
+ * @param DOMQuery $dest
+ * Destination DOMQuery document.
+ * @return \QueryPath\DOMQuery
+ * The current DOMQuery object, unaltered. Only the destination DOMQuery
+ * object is altered.
+ * @see before()
+ * @see insertAfter()
+ * @see appendTo()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function insertBefore(DOMQuery $dest) {
+ foreach ($this->matches as $m) $dest->before($m);
+ return $this;
+ }
+ /**
+ * Insert the contents of the current DOMQuery after the nodes in the
+ * destination DOMQuery object.
+ *
+ * @param DOMQuery $dest
+ * Destination object where the current elements will be deposited.
+ * @return \QueryPath\DOMQuery
+ * The present DOMQuery, unaltered. Only the destination object is altered.
+ * @see after()
+ * @see insertBefore()
+ * @see append()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function insertAfter(DOMQuery $dest) {
+ foreach ($this->matches as $m) $dest->after($m);
+ return $this;
+ }
+ /**
+ * Insert the given data after each element in the current DOMQuery object.
+ *
+ * This inserts the element as a peer to the currently matched elements.
+ * Contrast this with {@link append()}, which inserts the data as children
+ * of matched elements.
+ *
+ * @param mixed $data
+ * The data to be appended.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object (with the items inserted).
+ * @see before()
+ * @see append()
+ * @throws QueryPath::Exception
+ * Thrown if $data is an unsupported object type.
+ */
+ public function after($data) {
+ if (empty($data)) {
+ return $this;
+ }
+ $data = $this->prepareInsert($data);
+ foreach ($this->matches as $m) {
+ $ins = $data->cloneNode(TRUE);
+ if (isset($m->nextSibling))
+ $m->parentNode->insertBefore($ins, $m->nextSibling);
+ else
+ $m->parentNode->appendChild($ins);
+ }
+ return $this;
+ }
+ /**
+ * Replace the existing element(s) in the list with a new one.
+ *
+ * @param mixed $new
+ * A DOMElement or XML in a string. This will replace all elements
+ * currently wrapped in the DOMQuery object.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object wrapping <b>the items that were removed</b>.
+ * This remains consistent with the jQuery API.
+ * @see append()
+ * @see prepend()
+ * @see before()
+ * @see after()
+ * @see remove()
+ * @see replaceAll()
+ */
+ public function replaceWith($new) {
+ $data = $this->prepareInsert($new);
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ $parent = $m->parentNode;
+ $parent->insertBefore($data->cloneNode(TRUE), $m);
+ $found->attach($parent->removeChild($m));
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Remove the parent element from the selected node or nodes.
+ *
+ * This takes the given list of nodes and "unwraps" them, moving them out of their parent
+ * node, and then deleting the parent node.
+ *
+ * For example, consider this:
+ *
+ * @code
+ * <root><wrapper><content/></wrapper></root>
+ * @endcode
+ *
+ * Now we can run this code:
+ * @code
+ * qp($xml, 'content')->unwrap();
+ * @endcode
+ *
+ * This will result in:
+ *
+ * @code
+ * <root><content/></root>
+ * @endcode
+ * This is the opposite of wrap().
+ *
+ * <b>The root element cannot be unwrapped.</b> It has no parents.
+ * If you attempt to use unwrap on a root element, this will throw a
+ * QueryPath::Exception. (You can, however, "Unwrap" a child that is
+ * a direct descendant of the root element. This will remove the root
+ * element, and replace the child as the root element. Be careful, though.
+ * You cannot set more than one child as a root element.)
+ *
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, with the same element(s) selected.
+ * @throws QueryPath::Exception
+ * An exception is thrown if one attempts to unwrap a root element.
+ * @see wrap()
+ * @since 2.1
+ * @author mbutcher
+ */
+ public function unwrap() {
+
+ // We do this in two loops in order to
+ // capture the case where two matches are
+ // under the same parent. Othwerwise we might
+ // remove a match before we can move it.
+ $parents = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+
+ // Cannot unwrap the root element.
+ if ($m->isSameNode($m->ownerDocument->documentElement)) {
+ throw new \QueryPath\Exception('Cannot unwrap the root element.');
+ }
+
+ // Move children to peer of parent.
+ $parent = $m->parentNode;
+ $old = $parent->removeChild($m);
+ $parent->parentNode->insertBefore($old, $parent);
+ $parents->attach($parent);
+ }
+
+ // Now that all the children are moved, we
+ // remove all of the parents.
+ foreach ($parents as $ele) {
+ $ele->parentNode->removeChild($ele);
+ }
+
+ return $this;
+ }
+ /**
+ * Wrap each element inside of the given markup.
+ *
+ * Markup is usually a string, but it can also be a DOMNode, a document
+ * fragment, a SimpleXMLElement, or another DOMNode object (in which case
+ * the first item in the list will be used.)
+ *
+ * @param mixed $markup
+ * Markup that will wrap each element in the current list.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the wrapping changes made.
+ * @see wrapAll()
+ * @see wrapInner()
+ */
+ public function wrap($markup) {
+ $data = $this->prepareInsert($markup);
+
+ // If the markup passed in is empty, we don't do any wrapping.
+ if (empty($data)) {
+ return $this;
+ }
+
+ foreach ($this->matches as $m) {
+ if ($data instanceof \DOMDocumentFragment) {
+ $copy = $data->firstChild->cloneNode(true);
+ } else {
+ $copy = $data->cloneNode(true);
+ }
+
+ // XXX: Should be able to avoid doing this over and over.
+ if ($copy->hasChildNodes()) {
+ $deepest = $this->deepestNode($copy);
+ // FIXME: Does this need a different data structure?
+ $bottom = $deepest[0];
+ }
+ else
+ $bottom = $copy;
+
+ $parent = $m->parentNode;
+ $parent->insertBefore($copy, $m);
+ $m = $parent->removeChild($m);
+ $bottom->appendChild($m);
+ //$parent->appendChild($copy);
+ }
+ return $this;
+ }
+ /**
+ * Wrap all elements inside of the given markup.
+ *
+ * So all elements will be grouped together under this single marked up
+ * item. This works by first determining the parent element of the first item
+ * in the list. It then moves all of the matching elements under the wrapper
+ * and inserts the wrapper where that first element was found. (This is in
+ * accordance with the way jQuery works.)
+ *
+ * Markup is usually XML in a string, but it can also be a DOMNode, a document
+ * fragment, a SimpleXMLElement, or another DOMNode object (in which case
+ * the first item in the list will be used.)
+ *
+ * @param string $markup
+ * Markup that will wrap all elements in the current list.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the wrapping changes made.
+ * @see wrap()
+ * @see wrapInner()
+ */
+ public function wrapAll($markup) {
+ if ($this->matches->count() == 0) return;
+
+ $data = $this->prepareInsert($markup);
+
+ if (empty($data)) {
+ return $this;
+ }
+
+ if ($data instanceof \DOMDocumentFragment) {
+ $data = $data->firstChild->cloneNode(true);
+ } else {
+ $data = $data->cloneNode(true);
+ }
+
+ if ($data->hasChildNodes()) {
+ $deepest = $this->deepestNode($data);
+ // FIXME: Does this need fixing?
+ $bottom = $deepest[0];
+ }
+ else
+ $bottom = $data;
+
+ $first = $this->getFirstMatch();
+ $parent = $first->parentNode;
+ $parent->insertBefore($data, $first);
+ foreach ($this->matches as $m) {
+ $bottom->appendChild($m->parentNode->removeChild($m));
+ }
+ return $this;
+ }
+ /**
+ * Wrap the child elements of each item in the list with the given markup.
+ *
+ * Markup is usually a string, but it can also be a DOMNode, a document
+ * fragment, a SimpleXMLElement, or another DOMNode object (in which case
+ * the first item in the list will be used.)
+ *
+ * @param string $markup
+ * Markup that will wrap children of each element in the current list.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the wrapping changes made.
+ * @see wrap()
+ * @see wrapAll()
+ */
+ public function wrapInner($markup) {
+ $data = $this->prepareInsert($markup);
+
+ // No data? Short circuit.
+ if (empty($data)) return $this;
+
+ foreach ($this->matches as $m) {
+ if ($data instanceof \DOMDocumentFragment) {
+ $wrapper = $data->firstChild->cloneNode(true);
+ } else {
+ $wrapper = $data->cloneNode(true);
+ }
+
+ if ($wrapper->hasChildNodes()) {
+ $deepest = $this->deepestNode($wrapper);
+ // FIXME: ???
+ $bottom = $deepest[0];
+ }
+ else
+ $bottom = $wrapper;
+
+ if ($m->hasChildNodes()) {
+ while($m->firstChild) {
+ $kid = $m->removeChild($m->firstChild);
+ $bottom->appendChild($kid);
+ }
+ }
+
+ $m->appendChild($wrapper);
+ }
+ return $this;
+ }
+ /**
+ * Reduce the set of matches to the deepest child node in the tree.
+ *
+ * This loops through the matches and looks for the deepest child node of all of
+ * the matches. "Deepest", here, is relative to the nodes in the list. It is
+ * calculated as the distance from the starting node to the most distant child
+ * node. In other words, it is not necessarily the farthest node from the root
+ * element, but the farthest note from the matched element.
+ *
+ * In the case where there are multiple nodes at the same depth, all of the
+ * nodes at that depth will be included.
+ *
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery wrapping the single deepest node.
+ */
+ public function deepest() {
+ $deepest = 0;
+ $winner = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ $local_deepest = 0;
+ $local_ele = $this->deepestNode($m, 0, NULL, $local_deepest);
+
+ // Replace with the new deepest.
+ if ($local_deepest > $deepest) {
+ $winner = new \SplObjectStorage();
+ foreach ($local_ele as $lele) $winner->attach($lele);
+ $deepest = $local_deepest;
+ }
+ // Augument with other equally deep elements.
+ elseif ($local_deepest == $deepest) {
+ foreach ($local_ele as $lele)
+ $winner->attach($lele);
+ }
+ }
+ return $this->inst($winner, NULL, $this->options);
+ }
+
+ /**
+ * A depth-checking function. Typically, it only needs to be
+ * invoked with the first parameter. The rest are used for recursion.
+ * @see deepest();
+ * @param DOMNode $ele
+ * The element.
+ * @param int $depth
+ * The depth guage
+ * @param mixed $current
+ * The current set.
+ * @param DOMNode $deepest
+ * A reference to the current deepest node.
+ * @return array
+ * Returns an array of DOM nodes.
+ */
+ protected function deepestNode(\DOMNode $ele, $depth = 0, $current = NULL, &$deepest = NULL) {
+ // FIXME: Should this use SplObjectStorage?
+ if (!isset($current)) $current = array($ele);
+ if (!isset($deepest)) $deepest = $depth;
+ if ($ele->hasChildNodes()) {
+ foreach ($ele->childNodes as $child) {
+ if ($child->nodeType === XML_ELEMENT_NODE) {
+ $current = $this->deepestNode($child, $depth + 1, $current, $deepest);
+ }
+ }
+ }
+ elseif ($depth > $deepest) {
+ $current = array($ele);
+ $deepest = $depth;
+ }
+ elseif ($depth === $deepest) {
+ $current[] = $ele;
+ }
+ return $current;
+ }
+
+ /**
+ * Prepare an item for insertion into a DOM.
+ *
+ * This handles a variety of boilerplate tasks that need doing before an
+ * indeterminate object can be inserted into a DOM tree.
+ * - If item is a string, this is converted into a document fragment and returned.
+ * - If item is a DOMQuery, then all items are retrieved and converted into
+ * a document fragment and returned.
+ * - If the item is a DOMNode, it is imported into the current DOM if necessary.
+ * - If the item is a SimpleXMLElement, it is converted into a DOM node and then
+ * imported.
+ *
+ * @param mixed $item
+ * Item to prepare for insert.
+ * @return mixed
+ * Returns the prepared item.
+ * @throws QueryPath::Exception
+ * Thrown if the object passed in is not of a supprted object type.
+ */
+ protected function prepareInsert($item) {
+ if(empty($item)) {
+ return;
+ }
+ elseif (is_string($item)) {
+ // If configured to do so, replace all entities.
+ if ($this->options['replace_entities']) {
+ $item = \QueryPath\Entities::replaceAllEntities($item);
+ }
+
+ $frag = $this->document->createDocumentFragment();
+ try {
+ set_error_handler(array('\QueryPath\ParseException', 'initializeFromError'), $this->errTypes);
+ $frag->appendXML($item);
+ }
+ // Simulate a finally block.
+ catch (Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+ return $frag;
+ }
+ elseif ($item instanceof DOMQuery) {
+ if ($item->size() == 0)
+ return;
+
+ $frag = $this->document->createDocumentFragment();
+ foreach ($item->matches as $m) {
+ $frag->appendXML($item->document->saveXML($m));
+ }
+ return $frag;
+ }
+ elseif ($item instanceof \DOMNode) {
+ if ($item->ownerDocument !== $this->document) {
+ // Deep clone this and attach it to this document
+ $item = $this->document->importNode($item, TRUE);
+ }
+ return $item;
+ }
+ elseif ($item instanceof \SimpleXMLElement) {
+ $element = dom_import_simplexml($item);
+ return $this->document->importNode($element, TRUE);
+ }
+ // What should we do here?
+ //var_dump($item);
+ throw new \QueryPath\Exception("Cannot prepare item of unsupported type: " . gettype($item));
+ }
+ /**
+ * The tag name of the first element in the list.
+ *
+ * This returns the tag name of the first element in the list of matches. If
+ * the list is empty, an empty string will be used.
+ *
+ * @see replaceAll()
+ * @see replaceWith()
+ * @return string
+ * The tag name of the first element in the list.
+ */
+ public function tag() {
+ return ($this->size() > 0) ? $this->getFirstMatch()->tagName : '';
+ }
+ /**
+ * Remove any items from the list if they match the selector.
+ *
+ * In other words, each item that matches the selector will be remove
+ * from the DOM document. The returned DOMQuery wraps the list of
+ * removed elements.
+ *
+ * If no selector is specified, this will remove all current matches from
+ * the document.
+ *
+ * @param string $selector
+ * A CSS Selector.
+ * @return \QueryPath\DOMQuery
+ * The Query path wrapping a list of removed items.
+ * @see replaceAll()
+ * @see replaceWith()
+ * @see removeChildren()
+ */
+ public function remove($selector = NULL) {
+ if(!empty($selector)) {
+ // Do a non-destructive find.
+ $query = new QueryPathEventHandler($this->matches);
+ $query->find($selector);
+ $matches = $query->getMatches();
+ }
+ else {
+ $matches = $this->matches;
+ }
+
+ $found = new \SplObjectStorage();
+ foreach ($matches as $item) {
+ // The item returned is (according to docs) different from
+ // the one passed in, so we have to re-store it.
+ $found->attach($item->parentNode->removeChild($item));
+ }
+
+ // Return a clone DOMQuery with just the removed items. If
+ // no items are found, this will return an empty DOMQuery.
+ return count($found) == 0 ? new static() : new static($found);
+ }
+ /**
+ * This replaces everything that matches the selector with the first value
+ * in the current list.
+ *
+ * This is the reverse of replaceWith.
+ *
+ * Unlike jQuery, DOMQuery cannot assume a default document. Consequently,
+ * you must specify the intended destination document. If it is omitted, the
+ * present document is assumed to be tthe document. However, that can result
+ * in undefined behavior if the selector and the replacement are not sufficiently
+ * distinct.
+ *
+ * @param string $selector
+ * The selector.
+ * @param DOMDocument $document
+ * The destination document.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery wrapping the modified document.
+ * @deprecated Due to the fact that this is not a particularly friendly method,
+ * and that it can be easily replicated using {@see replaceWith()}, it is to be
+ * considered deprecated.
+ * @see remove()
+ * @see replaceWith()
+ */
+ public function replaceAll($selector, \DOMDocument $document) {
+ $replacement = $this->size() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
+
+ $c = new QueryPathEventHandler($document);
+ $c->find($selector);
+ $temp = $c->getMatches();
+ foreach ($temp as $item) {
+ $node = $replacement->cloneNode();
+ $node = $document->importNode($node);
+ $item->parentNode->replaceChild($node, $item);
+ }
+ return QueryPath::with($document, NULL, $this->options);
+ }
+ /**
+ * Add more elements to the current set of matches.
+ *
+ * This begins the new query at the top of the DOM again. The results found
+ * when running this selector are then merged into the existing results. In
+ * this way, you can add additional elements to the existing set.
+ *
+ * @param string $selector
+ * A valid selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the newly added elements.
+ * @see append()
+ * @see after()
+ * @see andSelf()
+ * @see end()
+ */
+ public function add($selector) {
+
+ // This is destructive, so we need to set $last:
+ $this->last = $this->matches;
+
+ foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) {
+ $this->matches->attach($item);
+ }
+ return $this;
+ }
+ /**
+ * Revert to the previous set of matches.
+ *
+ * <b>DEPRECATED</b> Do not use.
+ *
+ * This will revert back to the last set of matches (before the last
+ * "destructive" set of operations). This undoes any change made to the set of
+ * matched objects. Functions like find() and filter() change the
+ * list of matched objects. The end() function will revert back to the last set of
+ * matched items.
+ *
+ * Note that functions that modify the document, but do not change the list of
+ * matched objects, are not "destructive". Thus, calling append('something')->end()
+ * will not undo the append() call.
+ *
+ * Only one level of changes is stored. Reverting beyond that will result in
+ * an empty set of matches. Example:
+ *
+ * @code
+ * // The line below returns the same thing as qp(document, 'p');
+ * qp(document, 'p')->find('div')->end();
+ * // This returns an empty array:
+ * qp(document, 'p')->end();
+ * // This returns an empty array:
+ * qp(document, 'p')->find('div')->find('span')->end()->end();
+ * @endcode
+ *
+ * The last one returns an empty array because only one level of changes is stored.
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object reflecting the list of matches prior to the last destructive
+ * operation.
+ * @see andSelf()
+ * @see add()
+ * @deprecated This function will be removed.
+ */
+ public function end() {
+ // Note that this does not use setMatches because it must set the previous
+ // set of matches to empty array.
+ $this->matches = $this->last;
+ $this->last = new \SplObjectStorage();
+ return $this;
+ }
+ /**
+ * Combine the current and previous set of matched objects.
+ *
+ * Example:
+ *
+ * @code
+ * qp(document, 'p')->find('div')->andSelf();
+ * @endcode
+ *
+ * The code above will contain a list of all p elements and all div elements that
+ * are beneath p elements.
+ *
+ * @see end();
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object with the results of the last two "destructive" operations.
+ * @see add()
+ * @see end()
+ */
+ public function andSelf() {
+ // This is destructive, so we need to set $last:
+ $last = $this->matches;
+
+ foreach ($this->last as $item) $this->matches->attach($item);
+
+ $this->last = $last;
+ return $this;
+ }
+ /**
+ * Remove all child nodes.
+ *
+ * This is equivalent to jQuery's empty() function. (However, empty() is a
+ * PHP built-in, and cannot be used as a method name.)
+ *
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the child nodes removed.
+ * @see replaceWith()
+ * @see replaceAll()
+ * @see remove()
+ */
+ public function removeChildren() {
+ foreach ($this->matches as $m) {
+ while($kid = $m->firstChild) {
+ $m->removeChild($kid);
+ }
+ }
+ return $this;
+ }
+ /**
+ * Get the children of the elements in the DOMQuery object.
+ *
+ * If a selector is provided, the list of children will be filtered through
+ * the selector.
+ *
+ * @param string $selector
+ * A valid selector.
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see removeChildren()
+ * @see parent()
+ * @see parents()
+ * @see next()
+ * @see prev()
+ */
+ public function children($selector = NULL) {
+ $found = new \SplObjectStorage();
+ $filter = strlen($selector) > 0;
+
+ if ($filter) {
+ $tmp = new \SplObjectStorage();
+ }
+ foreach ($this->matches as $m) {
+ foreach($m->childNodes as $c) {
+ if ($c->nodeType == XML_ELEMENT_NODE) {
+ // This is basically an optimized filter() just for children().
+ if ($filter) {
+ $tmp->attach($c);
+ $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $c);
+ $query->find($selector);
+ if (count($query->matches()) > 0) {
+ $found->attach($c);
+ }
+ $tmp->detach($c);
+
+ }
+ // No filter. Just attach it.
+ else {
+ $found->attach($c);
+ }
+ }
+ }
+ }
+ $new = $this->inst($found, NULL, $this->options);
+ return $new;
+ }
+ /**
+ * Get all child nodes (not just elements) of all items in the matched set.
+ *
+ * It gets only the immediate children, not all nodes in the subtree.
+ *
+ * This does not process iframes. Xinclude processing is dependent on the
+ * DOM implementation and configuration.
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object wrapping all child nodes for all elements in the
+ * DOMNode object.
+ * @see find()
+ * @see text()
+ * @see html()
+ * @see innerHTML()
+ * @see xml()
+ * @see innerXML()
+ */
+ public function contents() {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ if (empty($m->childNodes)) continue; // Issue #51
+ foreach ($m->childNodes as $c) {
+ $found->attach($c);
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get a list of siblings for elements currently wrapped by this object.
+ *
+ * This will compile a list of every sibling of every element in the
+ * current list of elements.
+ *
+ * Note that if two siblings are present in the DOMQuery object to begin with,
+ * then both will be returned in the matched set, since they are siblings of each
+ * other. In other words,if the matches contain a and b, and a and b are siblings of
+ * each other, than running siblings will return a set that contains
+ * both a and b.
+ *
+ * @param string $selector
+ * If the optional selector is provided, siblings will be filtered through
+ * this expression.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery containing the matched siblings.
+ * @see contents()
+ * @see children()
+ * @see parent()
+ * @see parents()
+ */
+ public function siblings($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ $parent = $m->parentNode;
+ foreach ($parent->childNodes as $n) {
+ if ($n->nodeType == XML_ELEMENT_NODE && $n !== $m) {
+ $found->attach($n);
+ }
+ }
+ }
+ if (empty($selector)) {
+ return $this->inst($found, NULL, $this->options);
+ }
+ else {
+ return $this->inst($found, NULL, $this->options)->filter($selector);
+ }
+ }
+ /**
+ * Find the closest element matching the selector.
+ *
+ * This finds the closest match in the ancestry chain. It first checks the
+ * present element. If the present element does not match, this traverses up
+ * the ancestry chain (e.g. checks each parent) looking for an item that matches.
+ *
+ * It is provided for jQuery 1.3 compatibility.
+ * @param string $selector
+ * A CSS Selector to match.
+ * @return \QueryPath\DOMQuery
+ * The set of matches.
+ * @since 2.0
+ */
+ public function closest($selector) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ $found->attach($m);
+ }
+ else {
+ while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
+ $m = $m->parentNode;
+ // Is there any case where parent node is not an element?
+ if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ $found->attach($m);
+ break;
+ }
+ }
+ }
+
+ }
+ // XXX: Should this be an in-place modification?
+ return $this->inst($found, NULL, $this->options);
+ //$this->setMatches($found);
+ //return $this;
+ }
+ /**
+ * Get the immediate parent of each element in the DOMQuery.
+ *
+ * If a selector is passed, this will return the nearest matching parent for
+ * each element in the DOMQuery.
+ *
+ * @param string $selector
+ * A valid CSS3 selector.
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object wrapping the matching parents.
+ * @see children()
+ * @see siblings()
+ * @see parents()
+ */
+ public function parent($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
+ $m = $m->parentNode;
+ // Is there any case where parent node is not an element?
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ $found->attach($m);
+ break;
+ }
+ }
+ else {
+ $found->attach($m);
+ break;
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get all ancestors of each element in the DOMQuery.
+ *
+ * If a selector is present, only matching ancestors will be retrieved.
+ *
+ * @see parent()
+ * @param string $selector
+ * A valid CSS 3 Selector.
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object containing the matching ancestors.
+ * @see siblings()
+ * @see children()
+ */
+ public function parents($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
+ $m = $m->parentNode;
+ // Is there any case where parent node is not an element?
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0)
+ $found->attach($m);
+ }
+ else
+ $found->attach($m);
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Set or get the markup for an element.
+ *
+ * If $markup is set, then the giving markup will be injected into each
+ * item in the set. All other children of that node will be deleted, and this
+ * new code will be the only child or children. The markup MUST BE WELL FORMED.
+ *
+ * If no markup is given, this will return a string representing the child
+ * markup of the first node.
+ *
+ * <b>Important:</b> This differs from jQuery's html() function. This function
+ * returns <i>the current node</i> and all of its children. jQuery returns only
+ * the children. This means you do not need to do things like this:
+ * @code$qp->parent()->html()@endcode.
+ *
+ * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
+ *
+ * @param string $markup
+ * The text to insert.
+ * @return mixed
+ * A string if no markup was passed, or a DOMQuery if markup was passed.
+ * @see xml()
+ * @see text()
+ * @see contents()
+ */
+ public function html($markup = NULL) {
+ if (isset($markup)) {
+
+ if ($this->options['replace_entities']) {
+ $markup = \QueryPath\Entities::replaceAllEntities($markup);
+ }
+
+ // Parse the HTML and insert it into the DOM
+ //$doc = DOMDocument::loadHTML($markup);
+ $doc = $this->document->createDocumentFragment();
+ $doc->appendXML($markup);
+ $this->removeChildren();
+ $this->append($doc);
+ return $this;
+ }
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+
+ // Added by eabrand.
+ if(!$first->ownerDocument->documentElement) {
+ return NULL;
+ }
+
+ if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
+ return $this->document->saveHTML();
+ }
+ // saveHTML cannot take a node and serialize it.
+ return $this->document->saveXML($first);
+ }
+
+ /**
+ * Write the QueryPath document to HTML5.
+ *
+ * See html()
+ */
+ function html5($markup = NULL) {
+ $html5 = new HTML5($this->options);
+
+ // append HTML to existing
+ if (isset($markup)) {
+
+ // Parse the HTML and insert it into the DOM
+ $doc = $html5->loadHTMLFragment($markup);
+ $this->removeChildren();
+ $this->append($doc);
+ return $this;
+ }
+
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+
+ // Added by eabrand.
+ if(!$first->ownerDocument->documentElement) {
+ return NULL;
+ }
+
+ if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
+ return $html5->saveHTML($this->document); //$this->document->saveHTML();
+ }
+ return $html5->saveHTML($first);
+ }
+
+ /**
+ * Fetch the HTML contents INSIDE of the first DOMQuery item.
+ *
+ * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
+ *
+ * This gets all children of the first match in DOMQuery.
+ *
+ * Consider this fragment:
+ * @code
+ * <div>
+ * test <p>foo</p> test
+ * </div>
+ * @endcode
+ *
+ * We can retrieve just the contents of this code by doing something like
+ * this:
+ * @code
+ * qp($xml, 'div')->innerHTML();
+ * @endcode
+ *
+ * This would return the following:
+ * @codetest <p>foo</p> test@endcode
+ *
+ * @return string
+ * Returns a string representation of the child nodes of the first
+ * matched element.
+ * @see html()
+ * @see innerXML()
+ * @see innerXHTML()
+ * @since 2.0
+ */
+ public function innerHTML() {
+ return $this->innerXML();
+ }
+
+ /**
+ * Fetch child (inner) nodes of the first match.
+ *
+ * This will return the children of the present match. For an example,
+ * see {@link innerHTML()}.
+ *
+ * @see innerHTML()
+ * @see innerXML()
+ * @return string
+ * Returns a string of XHTML that represents the children of the present
+ * node.
+ * @since 2.0
+ */
+ public function innerXHTML() {
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+ elseif (!$first->hasChildNodes()) {
+ return '';
+ }
+
+ $buffer = '';
+ foreach ($first->childNodes as $child) {
+ $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Fetch child (inner) nodes of the first match.
+ *
+ * This will return the children of the present match. For an example,
+ * see {@link innerHTML()}.
+ *
+ * @see innerHTML()
+ * @see innerXHTML()
+ * @return string
+ * Returns a string of XHTML that represents the children of the present
+ * node.
+ * @since 2.0
+ */
+ public function innerXML() {
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+ elseif (!$first->hasChildNodes()) {
+ return '';
+ }
+
+ $buffer = '';
+ foreach ($first->childNodes as $child) {
+ $buffer .= $this->document->saveXML($child);
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Get child elements as an HTML5 string.
+ *
+ * TODO: This is a very simple alteration of innerXML. Do we need better
+ * support?
+ */
+ public function innerHTML5() {
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+ elseif (!$first->hasChildNodes()) {
+ return '';
+ }
+
+ $html5 = new HTML5($this->options);
+ $buffer = '';
+ foreach ($first->childNodes as $child) {
+ $buffer .= $html5->saveHTML($child);
+ }
+
+ return $buffer;
+ }
+
+ /**
+ * Retrieve the text of each match and concatenate them with the given separator.
+ *
+ * This has the effect of looping through all children, retrieving their text
+ * content, and then concatenating the text with a separator.
+ *
+ * @param string $sep
+ * The string used to separate text items. The default is a comma followed by a
+ * space.
+ * @param boolean $filterEmpties
+ * If this is true, empty items will be ignored.
+ * @return string
+ * The text contents, concatenated together with the given separator between
+ * every pair of items.
+ * @see implode()
+ * @see text()
+ * @since 2.0
+ */
+ public function textImplode($sep = ', ', $filterEmpties = TRUE) {
+ $tmp = array();
+ foreach ($this->matches as $m) {
+ $txt = $m->textContent;
+ $trimmed = trim($txt);
+ // If filter empties out, then we only add items that have content.
+ if ($filterEmpties) {
+ if (strlen($trimmed) > 0) $tmp[] = $txt;
+ }
+ // Else add all content, even if it's empty.
+ else {
+ $tmp[] = $txt;
+ }
+ }
+ return implode($sep, $tmp);
+ }
+ /**
+ * Get the text contents from just child elements.
+ *
+ * This is a specialized variant of textImplode() that implodes text for just the
+ * child elements of the current element.
+ *
+ * @param string $separator
+ * The separator that will be inserted between found text content.
+ * @return string
+ * The concatenated values of all children.
+ */
+ function childrenText($separator = ' ') {
+ // Branch makes it non-destructive.
+ return $this->branch()->xpath('descendant::text()')->textImplode($separator);
+ }
+ /**
+ * Get or set the text contents of a node.
+ * @param string $text
+ * If this is not NULL, this value will be set as the text of the node. It
+ * will replace any existing content.
+ * @return mixed
+ * A DOMQuery if $text is set, or the text content if no text
+ * is passed in as a pram.
+ * @see html()
+ * @see xml()
+ * @see contents()
+ */
+ public function text($text = NULL) {
+ if (isset($text)) {
+ $this->removeChildren();
+ foreach ($this->matches as $m) $m->appendChild($this->document->createTextNode($text));
+ return $this;
+ }
+ // Returns all text as one string:
+ $buf = '';
+ foreach ($this->matches as $m) $buf .= $m->textContent;
+ return $buf;
+ }
+ /**
+ * Get or set the text before each selected item.
+ *
+ * If $text is passed in, the text is inserted before each currently selected item.
+ *
+ * If no text is given, this will return the concatenated text after each selected element.
+ *
+ * @code
+ * <?php
+ * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
+ *
+ * // This will return 'Foo'
+ * qp($xml, 'a')->textBefore();
+ *
+ * // This will insert 'Baz' right before <b/>.
+ * qp($xml, 'b')->textBefore('Baz');
+ * ?>
+ * @endcode
+ *
+ * @param string $text
+ * If this is set, it will be inserted before each node in the current set of
+ * selected items.
+ * @return mixed
+ * Returns the DOMQuery object if $text was set, and returns a string (possibly empty)
+ * if no param is passed.
+ */
+ public function textBefore($text = NULL) {
+ if (isset($text)) {
+ $textNode = $this->document->createTextNode($text);
+ return $this->before($textNode);
+ }
+ $buffer = '';
+ foreach ($this->matches as $m) {
+ $p = $m;
+ while (isset($p->previousSibling) && $p->previousSibling->nodeType == XML_TEXT_NODE) {
+ $p = $p->previousSibling;
+ $buffer .= $p->textContent;
+ }
+ }
+ return $buffer;
+ }
+
+ public function textAfter($text = NULL) {
+ if (isset($text)) {
+ $textNode = $this->document->createTextNode($text);
+ return $this->after($textNode);
+ }
+ $buffer = '';
+ foreach ($this->matches as $m) {
+ $n = $m;
+ while (isset($n->nextSibling) && $n->nextSibling->nodeType == XML_TEXT_NODE) {
+ $n = $n->nextSibling;
+ $buffer .= $n->textContent;
+ }
+ }
+ return $buffer;
+ }
+
+ /**
+ * Set or get the value of an element's 'value' attribute.
+ *
+ * The 'value' attribute is common in HTML form elements. This is a
+ * convenience function for accessing the values. Since this is not common
+ * task on the server side, this method may be removed in future releases. (It
+ * is currently provided for jQuery compatibility.)
+ *
+ * If a value is provided in the params, then the value will be set for all
+ * matches. If no params are given, then the value of the first matched element
+ * will be returned. This may be NULL.
+ *
+ * @deprecated Just use attr(). There's no reason to use this on the server.
+ * @see attr()
+ * @param string $value
+ * @return mixed
+ * Returns a DOMQuery if a string was passed in, and a string if no string
+ * was passed in. In the later case, an error will produce NULL.
+ */
+ public function val($value = NULL) {
+ if (isset($value)) {
+ $this->attr('value', $value);
+ return $this;
+ }
+ return $this->attr('value');
+ }
+ /**
+ * Set or get XHTML markup for an element or elements.
+ *
+ * This differs from {@link html()} in that it processes (and produces)
+ * strictly XML 1.0 compliant markup.
+ *
+ * Like {@link xml()} and {@link html()}, this functions as both a
+ * setter and a getter.
+ *
+ * This is a convenience function for fetching HTML in XML format.
+ * It does no processing of the markup (such as schema validation).
+ * @param string $markup
+ * A string containing XML data.
+ * @return mixed
+ * If markup is passed in, a DOMQuery is returned. If no markup is passed
+ * in, XML representing the first matched element is returned.
+ * @see html()
+ * @see innerXHTML()
+ */
+ public function xhtml($markup = NULL) {
+
+ // XXX: This is a minor reworking of the original xml() method.
+ // This should be refactored, probably.
+ // See http://github.com/technosophos/querypath/issues#issue/10
+
+ $omit_xml_decl = $this->options['omit_xml_declaration'];
+ if ($markup === TRUE) {
+ // Basically, we handle the special case where we don't
+ // want the XML declaration to be displayed.
+ $omit_xml_decl = TRUE;
+ }
+ elseif (isset($markup)) {
+ return $this->xml($markup);
+ }
+
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+
+ if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
+
+ // Has the unfortunate side-effect of stripping doctype.
+ //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
+ $text = $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
+ }
+ else {
+ $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
+ }
+
+ // Issue #47: Using the old trick for removing the XML tag also removed the
+ // doctype. So we remove it with a regex:
+ if ($omit_xml_decl) {
+ $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
+ }
+
+ // This is slightly lenient: It allows for cases where code incorrectly places content
+ // inside of these supposedly unary elements.
+ $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
+ $text = preg_replace($unary, '<\\1\\2 />', $text);
+
+ // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
+ // and HTML 4/5 compat
+ $cdata = '/(<!\[CDATA\[|\]\]>)/i';
+ $replace = $this->options['escape_xhtml_js_css_sections'];
+ $text = preg_replace($cdata, $replace, $text);
+
+ return $text;
+ }
+ /**
+ * Set or get the XML markup for an element or elements.
+ *
+ * Like {@link html()}, this functions in both a setter and a getter mode.
+ *
+ * In setter mode, the string passed in will be parsed and then appended to the
+ * elements wrapped by this DOMNode object.When in setter mode, this parses
+ * the XML using the DOMFragment parser. For that reason, an XML declaration
+ * is not necessary.
+ *
+ * In getter mode, the first element wrapped by this DOMNode object will be
+ * converted to an XML string and returned.
+ *
+ * @param string $markup
+ * A string containing XML data.
+ * @return mixed
+ * If markup is passed in, a DOMQuery is returned. If no markup is passed
+ * in, XML representing the first matched element is returned.
+ * @see xhtml()
+ * @see html()
+ * @see text()
+ * @see content()
+ * @see innerXML()
+ */
+ public function xml($markup = NULL) {
+ $omit_xml_decl = $this->options['omit_xml_declaration'];
+ if ($markup === TRUE) {
+ // Basically, we handle the special case where we don't
+ // want the XML declaration to be displayed.
+ $omit_xml_decl = TRUE;
+ }
+ elseif (isset($markup)) {
+ if ($this->options['replace_entities']) {
+ $markup = \QueryPath\Entities::replaceAllEntities($markup);
+ }
+ $doc = $this->document->createDocumentFragment();
+ $doc->appendXML($markup);
+ $this->removeChildren();
+ $this->append($doc);
+ return $this;
+ }
+ $length = $this->size();
+ if ($length == 0) {
+ return NULL;
+ }
+ // Only return the first item -- that's what JQ does.
+ $first = $this->getFirstMatch();
+
+ // Catch cases where first item is not a legit DOM object.
+ if (!($first instanceof \DOMNode)) {
+ return NULL;
+ }
+
+ if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
+
+ return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
+ }
+ return $this->document->saveXML($first);
+ }
+ /**
+ * Send the XML document to the client.
+ *
+ * Write the document to a file path, if given, or
+ * to stdout (usually the client).
+ *
+ * This prints the entire document.
+ *
+ * @param string $path
+ * The path to the file into which the XML should be written. if
+ * this is NULL, data will be written to STDOUT, which is usually
+ * sent to the remote browser.
+ * @param int $options
+ * (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, unmodified.
+ * @see xml()
+ * @see innerXML()
+ * @see writeXHTML()
+ * @throws Exception
+ * In the event that a file cannot be written, an Exception will be thrown.
+ */
+ public function writeXML($path = NULL, $options = NULL) {
+ if ($path == NULL) {
+ print $this->document->saveXML(NULL, $options);
+ }
+ else {
+ try {
+ set_error_handler(array('\QueryPath\IOException', 'initializeFromError'));
+ $this->document->save($path, $options);
+ }
+ catch (Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+ }
+ return $this;
+ }
+ /**
+ * Writes HTML to output.
+ *
+ * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
+ * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
+ *
+ * Write the document to stdout (usually the client) or to a file.
+ *
+ * @param string $path
+ * The path to the file into which the XML should be written. if
+ * this is NULL, data will be written to STDOUT, which is usually
+ * sent to the remote browser.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, unmodified.
+ * @see html()
+ * @see innerHTML()
+ * @throws Exception
+ * In the event that a file cannot be written, an Exception will be thrown.
+ */
+ public function writeHTML($path = NULL) {
+ if ($path == NULL) {
+ print $this->document->saveHTML();
+ }
+ else {
+ try {
+ set_error_handler(array('\QueryPath\ParseException', 'initializeFromError'));
+ $this->document->saveHTMLFile($path);
+ }
+ catch (Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+ }
+ return $this;
+ }
+
+ /**
+ * Write the document to HTML5.
+ *
+ * This works the same as the other write* functions, but it encodes the output
+ * as HTML5 with UTF-8.
+ * @see html5()
+ * @see innerHTML5()
+ * @throws Exception
+ * In the event that a file cannot be written, an Exception will be thrown.
+ */
+ public function writeHTML5($path = NULL) {
+ $html5 = new HTML5();
+ if ($path == NULL) {
+ // Print the document to stdout.
+ print $html5->saveHTML($this->document);
+ return;
+ }
+
+ $html5->save($this->document, $path);
+ }
+
+ /**
+ * Write an XHTML file to output.
+ *
+ * Typically, you should use this instead of {@link writeHTML()}.
+ *
+ * Currently, this functions identically to {@link toXML()} <i>except that</i>
+ * it always uses closing tags (e.g. always @code<script></script>@endcode,
+ * never @code<script/>@endcode). It will
+ * write the file as well-formed XML. No XHTML schema validation is done.
+ *
+ * @see writeXML()
+ * @see xml()
+ * @see writeHTML()
+ * @see innerXHTML()
+ * @see xhtml()
+ * @param string $path
+ * The filename of the file to write to.
+ * @return \QueryPath\DOMQuery
+ * Returns the DOMQuery, unmodified.
+ * @throws Exception
+ * In the event that the output file cannot be written, an exception is
+ * thrown.
+ * @since 2.0
+ */
+ public function writeXHTML($path = NULL) {
+ return $this->writeXML($path, LIBXML_NOEMPTYTAG);
+ }
+ /**
+ * Get the next sibling of each element in the DOMQuery.
+ *
+ * If a selector is provided, the next matching sibling will be returned.
+ *
+ * @param string $selector
+ * A CSS3 selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object.
+ * @see nextAll()
+ * @see prev()
+ * @see children()
+ * @see contents()
+ * @see parent()
+ * @see parents()
+ */
+ public function next($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->nextSibling)) {
+ $m = $m->nextSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ $found->attach($m);
+ break;
+ }
+ }
+ else {
+ $found->attach($m);
+ break;
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get all siblings after an element.
+ *
+ * For each element in the DOMQuery, get all siblings that appear after
+ * it. If a selector is passed in, then only siblings that match the
+ * selector will be included.
+ *
+ * @param string $selector
+ * A valid CSS 3 selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, now containing the matching siblings.
+ * @see next()
+ * @see prevAll()
+ * @see children()
+ * @see siblings()
+ */
+ public function nextAll($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->nextSibling)) {
+ $m = $m->nextSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ $found->attach($m);
+ }
+ }
+ else {
+ $found->attach($m);
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get the next sibling before each element in the DOMQuery.
+ *
+ * For each element in the DOMQuery, this retrieves the previous sibling
+ * (if any). If a selector is supplied, it retrieves the first matching
+ * sibling (if any is found).
+ *
+ * @param string $selector
+ * A valid CSS 3 selector.
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object, now containing any previous siblings that have been
+ * found.
+ * @see prevAll()
+ * @see next()
+ * @see siblings()
+ * @see children()
+ */
+ public function prev($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->previousSibling)) {
+ $m = $m->previousSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector)) {
+ $found->attach($m);
+ break;
+ }
+ }
+ else {
+ $found->attach($m);
+ break;
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Get the previous siblings for each element in the DOMQuery.
+ *
+ * For each element in the DOMQuery, get all previous siblings. If a
+ * selector is provided, only matching siblings will be retrieved.
+ *
+ * @param string $selector
+ * A valid CSS 3 selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, now wrapping previous sibling elements.
+ * @see prev()
+ * @see nextAll()
+ * @see siblings()
+ * @see contents()
+ * @see children()
+ */
+ public function prevAll($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->previousSibling)) {
+ $m = $m->previousSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector)) {
+ $found->attach($m);
+ }
+ }
+ else {
+ $found->attach($m);
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+ /**
+ * Add a class to all elements in the current DOMQuery.
+ *
+ * This searchers for a class attribute on each item wrapped by the current
+ * DOMNode object. If no attribute is found, a new one is added and its value
+ * is set to $class. If a class attribute is found, then the value is appended
+ * on to the end.
+ *
+ * @param string $class
+ * The name of the class.
+ * @return \QueryPath\DOMQuery
+ * Returns the DOMQuery object.
+ * @see css()
+ * @see attr()
+ * @see removeClass()
+ * @see hasClass()
+ */
+ public function addClass($class) {
+ foreach ($this->matches as $m) {
+ if ($m->hasAttribute('class')) {
+ $val = $m->getAttribute('class');
+ $m->setAttribute('class', $val . ' ' . $class);
+ }
+ else {
+ $m->setAttribute('class', $class);
+ }
+ }
+ return $this;
+ }
+ /**
+ * Remove the named class from any element in the DOMQuery that has it.
+ *
+ * This may result in the entire class attribute being removed. If there
+ * are other items in the class attribute, though, they will not be removed.
+ *
+ * Example:
+ * Consider this XML:
+ * @code
+ * <element class="first second"/>
+ * @endcode
+ *
+ * Executing this fragment of code will remove only the 'first' class:
+ * @code
+ * qp(document, 'element')->removeClass('first');
+ * @endcode
+ *
+ * The resulting XML will be:
+ * @code
+ * <element class="second"/>
+ * @endcode
+ *
+ * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
+ *
+ * @param string $class
+ * The class name to remove.
+ * @return \QueryPath\DOMQuery
+ * The modified DOMNode object.
+ * @see attr()
+ * @see addClass()
+ * @see hasClass()
+ */
+ public function removeClass($class = false) {
+ if (empty($class))
+ {
+ foreach ($this->matches as $m) {
+ $m->removeAttribute('class');
+ }
+ }else{
+ $to_remove = array_filter(explode(' ',$class));
+ foreach ($this->matches as $m) {
+ if ($m->hasAttribute('class')) {
+ $vals = array_filter(explode(' ', $m->getAttribute('class')));
+ $buf = array();
+ foreach ($vals as $v) {
+ if (!in_array($v, $to_remove))
+ $buf[] = $v;
+ }
+ if (empty($buf))
+ $m->removeAttribute('class');
+ else
+ $m->setAttribute('class', implode(' ', $buf));
+ }
+ }
+ }
+ return $this;
+ }
+
+ /**
+ * Returns TRUE if any of the elements in the DOMQuery have the specified class.
+ *
+ * @param string $class
+ * The name of the class.
+ * @return boolean
+ * TRUE if the class exists in one or more of the elements, FALSE otherwise.
+ * @see addClass()
+ * @see removeClass()
+ */
+ public function hasClass($class) {
+ foreach ($this->matches as $m) {
+ if ($m->hasAttribute('class')) {
+ $vals = explode(' ', $m->getAttribute('class'));
+ if (in_array($class, $vals)) return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Branch the base DOMQuery into another one with the same matches.
+ *
+ * This function makes a copy of the DOMQuery object, but keeps the new copy
+ * (initially) pointed at the same matches. This object can then be queried without
+ * changing the original DOMQuery. However, changes to the elements inside of this
+ * DOMQuery will show up in the DOMQuery from which it is branched.
+ *
+ * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
+ * the current DOMNode object and makes a copy of all of its matches. You continue
+ * to operate on the same DOMNode object, but the elements inside of the DOMQuery
+ * are copies of those before the call to cloneAll().
+ *
+ * This, on the other hand, copies <i>the DOMQuery</i>, but keeps valid
+ * references to the document and the wrapped elements. A new query branch is
+ * created, but any changes will be written back to the same document.
+ *
+ * In practice, this comes in handy when you want to do multiple queries on a part
+ * of the document, but then return to a previous set of matches. (see {@link QPTPL}
+ * for examples of this in practice).
+ *
+ * Example:
+ *
+ * @code
+ * <?php
+ * $qp = qp( QueryPath::HTML_STUB);
+ * $branch = $qp->branch();
+ * $branch->find('title')->text('Title');
+ * $qp->find('body')->text('This is the body')->writeHTML;
+ * ?>
+ * @endcode
+ *
+ * Notice that in the code, each of the DOMQuery objects is doing its own
+ * query. However, both are modifying the same document. The result of the above
+ * would look something like this:
+ *
+ * @code
+ * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+ * <html xmlns="http://www.w3.org/1999/xhtml">
+ * <head>
+ * <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
+ * <title>Title</title>
+ * </head>
+ * <body>This is the body</body>
+ * </html>
+ * @endcode
+ *
+ * Notice that while $qp and $banch were performing separate queries, they
+ * both modified the same document.
+ *
+ * In jQuery or a browser-based solution, you generally do not need a branching
+ * function because there is (implicitly) only one document. In QueryPath, there
+ * is no implicit document. Every document must be explicitly specified (and,
+ * in most cases, parsed -- which is costly). Branching makes it possible to
+ * work on one document with multiple DOMNode objects.
+ *
+ * @param string $selector
+ * If a selector is passed in, an additional {@link find()} will be executed
+ * on the branch before it is returned. (Added in QueryPath 2.0.)
+ * @return \QueryPath\DOMQuery
+ * A copy of the DOMQuery object that points to the same set of elements that
+ * the original DOMQuery was pointing to.
+ * @since 1.1
+ * @see cloneAll()
+ * @see find()
+ */
+ public function branch($selector = NULL) {
+ $temp = \QueryPath::with($this->matches, NULL, $this->options);
+ //if (isset($selector)) $temp->find($selector);
+ $temp->document = $this->document;
+ if (isset($selector)) $temp->findInPlace($selector);
+ return $temp;
+ }
+ protected function inst($matches, $selector, $options) {
+ /*
+ $temp = \QueryPath::with($matches, NULL, $options);
+ //if (isset($selector)) $temp->find($selector);
+ $temp->document = $this->document;
+ if (isset($selector)) $temp->findInPlace($selector);
+ return $temp;
+ */
+ // https://en.wikipedia.org/wiki/Dolly_(sheep)
+ $dolly = clone $this;
+ $dolly->setMatches($matches);
+ //var_dump($dolly); exit;
+ if (isset($selector)) $dolly->findInPlace($selector);
+ return $dolly;
+ }
+ /**
+ * Perform a deep clone of each node in the DOMQuery.
+ *
+ * @attention
+ * This is an in-place modification of the current QueryPath object.
+ *
+ * This does not clone the DOMQuery object, but instead clones the
+ * list of nodes wrapped by the DOMQuery. Every element is deeply
+ * cloned.
+ *
+ * This method is analogous to jQuery's clone() method.
+ *
+ * This is a destructive operation, which means that end() will revert
+ * the list back to the clone's original.
+ * @see qp()
+ * @return \QueryPath\DOMQuery
+ */
+ public function cloneAll() {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) $found->attach($m->cloneNode(TRUE));
+ //return $this->inst($found, NULL, $this->options);
+ $this->setMatches($found);
+ return $this;
+ }
+
+ /**
+ * Clone the DOMQuery.
+ *
+ * This makes a deep clone of the elements inside of the DOMQuery.
+ *
+ * This clones only the QueryPathImpl, not all of the decorators. The
+ * clone operator in PHP should handle the cloning of the decorators.
+ */
+ public function __clone() {
+ //XXX: Should we clone the document?
+
+ // Make sure we clone the kids.
+ $this->cloneAll();
+ }
+
+ /**
+ * Detach any items from the list if they match the selector.
+ *
+ * In other words, each item that matches the selector will be removed
+ * from the DOM document. The returned DOMQuery wraps the list of
+ * removed elements.
+ *
+ * If no selector is specified, this will remove all current matches from
+ * the document.
+ *
+ * @param string $selector
+ * A CSS Selector.
+ * @return \QueryPath\DOMQuery
+ * The Query path wrapping a list of removed items.
+ * @see replaceAll()
+ * @see replaceWith()
+ * @see removeChildren()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function detach($selector = NULL) {
+
+ if(!empty($selector))
+ $this->find($selector);
+
+ $found = new \SplObjectStorage();
+ $this->last = $this->matches;
+ foreach ($this->matches as $item) {
+ // The item returned is (according to docs) different from
+ // the one passed in, so we have to re-store it.
+ $found->attach($item->parentNode->removeChild($item));
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Attach any items from the list if they match the selector.
+ *
+ * If no selector is specified, this will remove all current matches from
+ * the document.
+ *
+ * @param DOMQuery $dest
+ * A DOMQuery Selector.
+ * @return \QueryPath\DOMQuery
+ * The Query path wrapping a list of removed items.
+ * @see replaceAll()
+ * @see replaceWith()
+ * @see removeChildren()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function attach(DOMQuery $dest) {
+ foreach ($this->last as $m) $dest->append($m);
+ return $this;
+ }
+
+ /**
+ * Reduce the elements matched by DOMQuery to only those which contain the given item.
+ *
+ * There are two ways in which this is different from jQuery's implementation:
+ * - We allow ANY DOMNode, not just DOMElements. That means this will work on
+ * processor instructions, text nodes, comments, etc.
+ * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
+ * and modifies the existing object. It does not create a brand new object.
+ *
+ * @param mixed $contained
+ * - If $contained is a CSS selector (e.g. '#foo'), this will test to see
+ * if the current DOMQuery has any elements that contain items that match
+ * the selector.
+ * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
+ * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the same document.)
+ * @since 2.1
+ * @author eabrand
+ * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
+ */
+ public function has($contained) {
+ /*
+ if (count($this->matches) == 0) {
+ return false;
+ }
+ */
+ $found = new \SplObjectStorage();
+
+ // If it's a selector, we just get all of the DOMNodes that match the selector.
+ $nodes = array();
+ if (is_string($contained)) {
+ // Get the list of nodes.
+ $nodes = $this->branch($contained)->get();
+ }
+ elseif ($contained instanceof \DOMNode) {
+ // Make a list with one node.
+ $nodes = array($contained);
+ }
+
+ // Now we go through each of the nodes that we are testing. We want to find
+ // ALL PARENTS that are in our existing DOMQuery matches. Those are the
+ // ones we add to our new matches.
+ foreach ($nodes as $original_node) {
+ $node = $original_node;
+ while (!empty($node)/* && $node != $node->ownerDocument*/) {
+ if ($this->matches->contains($node)) {
+ $found->attach($node);
+ }
+ $node = $node->parentNode;
+ }
+ }
+
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Empty everything within the specified element.
+ *
+ * A convenience function for removeChildren(). This is equivalent to jQuery's
+ * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
+ * function name.
+ *
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object with the newly emptied elements.
+ * @see removeChildren()
+ * @since 2.1
+ * @author eabrand
+ * @deprecated The removeChildren() function is the preferred method.
+ */
+ public function emptyElement() {
+ $this->removeChildren();
+ return $this;
+ }
+
+ /**
+ * Get the even elements, so counter-intuitively 1, 3, 5, etc.
+ *
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see removeChildren()
+ * @see parent()
+ * @see parents()
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function even() {
+ $found = new \SplObjectStorage();
+ $even = false;
+ foreach ($this->matches as $m) {
+ if ($even && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
+ $even = ($even) ? false : true;
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
+ *
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see removeChildren()
+ * @see parent()
+ * @see parents()
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function odd() {
+ $found = new \SplObjectStorage();
+ $odd = true;
+ foreach ($this->matches as $m) {
+ if ($odd && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
+ $odd = ($odd) ? false : true;
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the first matching element.
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function first() {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ if ($m->nodeType == XML_ELEMENT_NODE) {
+ $found->attach($m);
+ break;
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the first child of the matching element.
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function firstChild() {
+ // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
+ $found = new \SplObjectStorage();
+ $flag = false;
+ foreach ($this->matches as $m) {
+ foreach($m->childNodes as $c) {
+ if ($c->nodeType == XML_ELEMENT_NODE) {
+ $found->attach($c);
+ $flag = true;
+ break;
+ }
+ }
+ if($flag) break;
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the last matching element.
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function last() {
+ $found = new \SplObjectStorage();
+ $item = null;
+ foreach ($this->matches as $m) {
+ if ($m->nodeType == XML_ELEMENT_NODE) {
+ $item = $m;
+ }
+ }
+ if ($item) {
+ $found->attach($item);
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the last child of the matching element.
+ *
+ *
+ * @return \QueryPath\DOMQuery
+ * A DOMQuery wrapping all of the children.
+ * @see next()
+ * @see prev()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function lastChild() {
+ $found = new \SplObjectStorage();
+ $item = null;
+ foreach ($this->matches as $m) {
+ foreach($m->childNodes as $c) {
+ if ($c->nodeType == XML_ELEMENT_NODE) {
+ $item = $c;
+ }
+ }
+ if ($item) {
+ $found->attach($item);
+ $item = null;
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get all siblings after an element until the selector is reached.
+ *
+ * For each element in the DOMQuery, get all siblings that appear after
+ * it. If a selector is passed in, then only siblings that match the
+ * selector will be included.
+ *
+ * @param string $selector
+ * A valid CSS 3 selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, now containing the matching siblings.
+ * @see next()
+ * @see prevAll()
+ * @see children()
+ * @see siblings()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function nextUntil($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->nextSibling)) {
+ $m = $m->nextSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
+ break;
+ }
+ else {
+ $found->attach($m);
+ }
+ }
+ else {
+ $found->attach($m);
+ }
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get the previous siblings for each element in the DOMQuery
+ * until the selector is reached.
+ *
+ * For each element in the DOMQuery, get all previous siblings. If a
+ * selector is provided, only matching siblings will be retrieved.
+ *
+ * @param string $selector
+ * A valid CSS 3 selector.
+ * @return \QueryPath\DOMQuery
+ * The DOMQuery object, now wrapping previous sibling elements.
+ * @see prev()
+ * @see nextAll()
+ * @see siblings()
+ * @see contents()
+ * @see children()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function prevUntil($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while (isset($m->previousSibling)) {
+ $m = $m->previousSibling;
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector) && QueryPath::with($m, NULL, $this->options)->is($selector))
+ break;
+ else
+ $found->attach($m);
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /**
+ * Get all ancestors of each element in the DOMQuery until the selector is reached.
+ *
+ * If a selector is present, only matching ancestors will be retrieved.
+ *
+ * @see parent()
+ * @param string $selector
+ * A valid CSS 3 Selector.
+ * @return \QueryPath\DOMQuery
+ * A DOMNode object containing the matching ancestors.
+ * @see siblings()
+ * @see children()
+ * @since 2.1
+ * @author eabrand
+ */
+ public function parentsUntil($selector = NULL) {
+ $found = new \SplObjectStorage();
+ foreach ($this->matches as $m) {
+ while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
+ $m = $m->parentNode;
+ // Is there any case where parent node is not an element?
+ if ($m->nodeType === XML_ELEMENT_NODE) {
+ if (!empty($selector)) {
+ if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0)
+ break;
+ else
+ $found->attach($m);
+ }
+ else
+ $found->attach($m);
+ }
+ }
+ }
+ return $this->inst($found, NULL, $this->options);
+ }
+
+ /////// INTERNAL FUNCTIONS ////////
+
+
+ /**
+ * Determine whether a given string looks like XML or not.
+ *
+ * Basically, this scans a portion of the supplied string, checking to see
+ * if it has a tag-like structure. It is possible to "confuse" this, which
+ * may subsequently result in parse errors, but in the vast majority of
+ * cases, this method serves as a valid inicator of whether or not the
+ * content looks like XML.
+ *
+ * Things that are intentional excluded:
+ * - plain text with no markup.
+ * - strings that look like filesystem paths.
+ *
+ * Subclasses SHOULD NOT OVERRIDE THIS. Altering it may be altering
+ * core assumptions about how things work. Instead, classes should
+ * override the constructor and pass in only one of the parsed types
+ * that this class expects.
+ */
+ protected function isXMLish($string) {
+ return (strpos($string, '<') !== FALSE && strpos($string, '>') !== FALSE);
+ }
+
+ private function parseXMLString($string, $flags = NULL) {
+
+ $document = new \DOMDocument('1.0');
+ $lead = strtolower(substr($string, 0, 5)); // <?xml
+ try {
+ set_error_handler(array('\QueryPath\ParseException', 'initializeFromError'), $this->errTypes);
+
+ if (isset($this->options['convert_to_encoding'])) {
+ // Is there another way to do this?
+
+ $from_enc = isset($this->options['convert_from_encoding']) ? $this->options['convert_from_encoding'] : 'auto';
+ $to_enc = $this->options['convert_to_encoding'];
+
+ if (function_exists('mb_convert_encoding')) {
+ $string = mb_convert_encoding($string, $to_enc, $from_enc);
+ }
+
+ }
+
+ // This is to avoid cases where low ascii digits have slipped into HTML.
+ // AFAIK, it should not adversly effect UTF-8 documents.
+ if (!empty($this->options['strip_low_ascii'])) {
+ $string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
+ }
+
+ // Allow users to override parser settings.
+ if (empty($this->options['use_parser'])) {
+ $useParser = '';
+ }
+ else {
+ $useParser = strtolower($this->options['use_parser']);
+ }
+
+ // If HTML parser is requested, we use it.
+ if ($useParser == 'html') {
+ $document->loadHTML($string);
+ }
+ // Parse as XML if it looks like XML, or if XML parser is requested.
+ elseif ($lead == '<?xml' || $useParser == 'xml') {
+ if ($this->options['replace_entities']) {
+ $string = \QueryPath\Entities::replaceAllEntities($string);
+ }
+ $document->loadXML($string, $flags);
+ }
+ // In all other cases, we try the HTML parser.
+ else {
+ $document->loadHTML($string);
+ }
+ }
+ // Emulate 'finally' behavior.
+ catch (Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+
+ if (empty($document)) {
+ throw new \QueryPath\ParseException('Unknown parser exception.');
+ }
+ return $document;
+ }
+
+ /**
+ * EXPERT: Be very, very careful using this.
+ * A utility function for setting the current set of matches.
+ * It makes sure the last matches buffer is set (for end() and andSelf()).
+ * @since 2.0
+ */
+ public function setMatches($matches, $unique = TRUE) {
+ // This causes a lot of overhead....
+ //if ($unique) $matches = self::unique($matches);
+ $this->last = $this->matches;
+
+ // Just set current matches.
+ if ($matches instanceof \SplObjectStorage) {
+ $this->matches = $matches;
+ }
+ // This is likely legacy code that needs conversion.
+ elseif (is_array($matches)) {
+ trigger_error('Legacy array detected.');
+ $tmp = new \SplObjectStorage();
+ foreach ($matches as $m) $tmp->attach($m);
+ $this->matches = $tmp;
+ }
+ // For non-arrays, try to create a new match set and
+ // add this object.
+ else {
+ $found = new \SplObjectStorage();
+ if (isset($matches)) $found->attach($matches);
+ $this->matches = $found;
+ }
+
+ // EXPERIMENTAL: Support for qp()->length.
+ $this->length = $this->matches->count();
+ }
+
+ /**
+ * Set the match monitor to empty.
+ *
+ * This preserves history.
+ *
+ * @since 2.0
+ */
+ private function noMatches() {
+ $this->setMatches(NULL);
+ }
+
+ /**
+ * A utility function for retriving a match by index.
+ *
+ * The internal data structure used in DOMQuery does not have
+ * strong random access support, so we suppliment it with this method.
+ */
+ private function getNthMatch($index) {
+ if ($index > $this->matches->count() || $index < 0) return;
+
+ $i = 0;
+ foreach ($this->matches as $m) {
+ if ($i++ == $index) return $m;
+ }
+ }
+
+ /**
+ * Convenience function for getNthMatch(0).
+ */
+ private function getFirstMatch() {
+ $this->matches->rewind();
+ return $this->matches->current();
+ }
+
+ /**
+ * Parse an XML or HTML file.
+ *
+ * This attempts to autodetect the type of file, and then parse it.
+ *
+ * @param string $filename
+ * The file name to parse.
+ * @param int $flags
+ * The OR-combined flags accepted by the DOM parser. See the PHP documentation
+ * for DOM or for libxml.
+ * @param resource $context
+ * The stream context for the file IO. If this is set, then an alternate
+ * parsing path is followed: The file is loaded by PHP's stream-aware IO
+ * facilities, read entirely into memory, and then handed off to
+ * {@link parseXMLString()}. On large files, this can have a performance impact.
+ * @throws \QueryPath\ParseException
+ * Thrown when a file cannot be loaded or parsed.
+ */
+ private function parseXMLFile($filename, $flags = NULL, $context = NULL) {
+
+ // If a context is specified, we basically have to do the reading in
+ // two steps:
+ if (!empty($context)) {
+ try {
+ set_error_handler(array('\QueryPath\ParseException', 'initializeFromError'), $this->errTypes);
+ $contents = file_get_contents($filename, FALSE, $context);
+ }
+ // Apparently there is no 'finally' in PHP, so we have to restore the error
+ // handler this way:
+ catch(Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+
+ if ($contents == FALSE) {
+ throw new \QueryPath\ParseException(sprintf('Contents of the file %s could not be retrieved.', $filename));
+ }
+ return $this->parseXMLString($contents, $flags);
+ }
+
+ $document = new \DOMDocument();
+ $lastDot = strrpos($filename, '.');
+
+ $htmlExtensions = array(
+ '.html' => 1,
+ '.htm' => 1,
+ );
+
+ // Allow users to override parser settings.
+ if (empty($this->options['use_parser'])) {
+ $useParser = '';
+ }
+ else {
+ $useParser = strtolower($this->options['use_parser']);
+ }
+
+ $ext = $lastDot !== FALSE ? strtolower(substr($filename, $lastDot)) : '';
+
+ try {
+ set_error_handler(array('\QueryPath\ParseException', 'initializeFromError'), $this->errTypes);
+
+ // If the parser is explicitly set to XML, use that parser.
+ if ($useParser == 'xml') {
+ $r = $document->load($filename, $flags);
+ }
+ // Otherwise, see if it looks like HTML.
+ elseif (isset($htmlExtensions[$ext]) || $useParser == 'html') {
+ // Try parsing it as HTML.
+ $r = $document->loadHTMLFile($filename);
+ }
+ // Default to XML.
+ else {
+ $r = $document->load($filename, $flags);
+ }
+
+ }
+ // Emulate 'finally' behavior.
+ catch (Exception $e) {
+ restore_error_handler();
+ throw $e;
+ }
+ restore_error_handler();
+ return $document;
+ }
+
+ /**
+ * Call extension methods.
+ *
+ * This function is used to invoke extension methods. It searches the
+ * registered extenstensions for a matching function name. If one is found,
+ * it is executed with the arguments in the $arguments array.
+ *
+ * @throws QueryPath::Exception
+ * An exception is thrown if a non-existent method is called.
+ */
+ public function __call($name, $arguments) {
+
+ if (!ExtensionRegistry::$useRegistry) {
+ throw new \QueryPath\Exception("No method named $name found (Extensions disabled).");
+ }
+
+ // Loading of extensions is deferred until the first time a
+ // non-core method is called. This makes constructing faster, but it
+ // may make the first invocation of __call() slower (if there are
+ // enough extensions.)
+ //
+ // The main reason for moving this out of the constructor is that most
+ // new DOMQuery instances do not use extensions. Charging qp() calls
+ // with the additional hit is not a good idea.
+ //
+ // Also, this will at least limit the number of circular references.
+ if (empty($this->ext)) {
+ // Load the registry
+ $this->ext = ExtensionRegistry::getExtensions($this);
+ }
+
+ // Note that an empty ext registry indicates that extensions are disabled.
+ if (!empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
+ $owner = ExtensionRegistry::getMethodClass($name);
+ $method = new \ReflectionMethod($owner, $name);
+ return $method->invokeArgs($this->ext[$owner], $arguments);
+ }
+ throw new \QueryPath\Exception("No method named $name found. Possibly missing an extension.");
+ }
+
+ /**
+ * Get an iterator for the matches in this object.
+ * @return Iterable
+ * Returns an iterator.
+ */
+ public function getIterator() {
+ $i = new QueryPathIterator($this->matches);
+ $i->options = $this->options;
+ return $i;
+ }
+}
diff --git a/lib/querypath/src/QueryPath/Entities.php b/lib/querypath/src/QueryPath/Entities.php
new file mode 100644
index 0000000..5670dc5
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Entities.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * @file
+ * HTML entity utilities.
+ */
+
+namespace QueryPath;
+
+/**
+ * Perform various tasks on HTML/XML entities.
+ *
+ * @ingroup querypath_util
+ */
+class Entities {
+
+ /**
+ * This is three regexes wrapped into 1. The | divides them.
+ * 1: Match any char-based entity. This will go in $matches[1]
+ * 2: Match any num-based entity. This will go in $matches[2]
+ * 3: Match any hex-based entry. This will go in $matches[3]
+ * 4: Match any ampersand that is not an entity. This goes in $matches[4]
+ * This last rule will only match if one of the previous two has not already
+ * matched.
+ * XXX: Are octal encodings for entities acceptable?
+ */
+ //protected static $regex = '/&([\w]+);|&#([\d]+);|&([\w]*[\s$]+)/m';
+ protected static $regex = '/&([\w]+);|&#([\d]+);|&#(x[0-9a-fA-F]+);|(&)/m';
+
+ /**
+ * Replace all entities.
+ * This will scan a string and will attempt to replace all
+ * entities with their numeric equivalent. This will not work
+ * with specialized entities.
+ *
+ * @param string $string
+ * The string to perform replacements on.
+ * @return string
+ * Returns a string that is similar to the original one, but with
+ * all entity replacements made.
+ */
+ public static function replaceAllEntities($string) {
+ return preg_replace_callback(self::$regex, '\QueryPath\Entities::doReplacement', $string);
+ }
+
+ /**
+ * Callback for processing replacements.
+ *
+ * @param array $matches
+ * The regular expression replacement array.
+ */
+ protected static function doReplacement($matches) {
+ // See how the regex above works out.
+ //print_r($matches);
+
+ // From count, we can tell whether we got a
+ // char, num, or bare ampersand.
+ $count = count($matches);
+ switch ($count) {
+ case 2:
+ // We have a character entity
+ return '&#' . self::replaceEntity($matches[1]) . ';';
+ case 3:
+ case 4:
+ // we have a numeric entity
+ return '&#' . $matches[$count-1] . ';';
+ case 5:
+ // We have an unescaped ampersand.
+ return '&#38;';
+ }
+ }
+
+ /**
+ * Lookup an entity string's numeric equivalent.
+ *
+ * @param string $entity
+ * The entity whose numeric value is needed.
+ * @return int
+ * The integer value corresponding to the entity.
+ * @author Matt Butcher
+ * @author Ryan Mahoney
+ */
+ public static function replaceEntity($entity) {
+ return self::$entity_array[$entity];
+ }
+
+ /**
+ * Conversion mapper for entities in HTML.
+ * Large entity conversion table. This is
+ * significantly broader in range than
+ * get_html_translation_table(HTML_ENTITIES).
+ *
+ * This code comes from Rhizome ({@link http://code.google.com/p/sinciput})
+ *
+ * @todo See if we can do this as a const.
+ * @see get_html_translation_table()
+ */
+ private static $entity_array = array(
+ 'nbsp' => 160, 'iexcl' => 161, 'cent' => 162, 'pound' => 163,
+ 'curren' => 164, 'yen' => 165, 'brvbar' => 166, 'sect' => 167,
+ 'uml' => 168, 'copy' => 169, 'ordf' => 170, 'laquo' => 171,
+ 'not' => 172, 'shy' => 173, 'reg' => 174, 'macr' => 175, 'deg' => 176,
+ 'plusmn' => 177, 'sup2' => 178, 'sup3' => 179, 'acute' => 180,
+ 'micro' => 181, 'para' => 182, 'middot' => 183, 'cedil' => 184,
+ 'sup1' => 185, 'ordm' => 186, 'raquo' => 187, 'frac14' => 188,
+ 'frac12' => 189, 'frac34' => 190, 'iquest' => 191, 'Agrave' => 192,
+ 'Aacute' => 193, 'Acirc' => 194, 'Atilde' => 195, 'Auml' => 196,
+ 'Aring' => 197, 'AElig' => 198, 'Ccedil' => 199, 'Egrave' => 200,
+ 'Eacute' => 201, 'Ecirc' => 202, 'Euml' => 203, 'Igrave' => 204,
+ 'Iacute' => 205, 'Icirc' => 206, 'Iuml' => 207, 'ETH' => 208,
+ 'Ntilde' => 209, 'Ograve' => 210, 'Oacute' => 211, 'Ocirc' => 212,
+ 'Otilde' => 213, 'Ouml' => 214, 'times' => 215, 'Oslash' => 216,
+ 'Ugrave' => 217, 'Uacute' => 218, 'Ucirc' => 219, 'Uuml' => 220,
+ 'Yacute' => 221, 'THORN' => 222, 'szlig' => 223, 'agrave' => 224,
+ 'aacute' => 225, 'acirc' => 226, 'atilde' => 227, 'auml' => 228,
+ 'aring' => 229, 'aelig' => 230, 'ccedil' => 231, 'egrave' => 232,
+ 'eacute' => 233, 'ecirc' => 234, 'euml' => 235, 'igrave' => 236,
+ 'iacute' => 237, 'icirc' => 238, 'iuml' => 239, 'eth' => 240,
+ 'ntilde' => 241, 'ograve' => 242, 'oacute' => 243, 'ocirc' => 244,
+ 'otilde' => 245, 'ouml' => 246, 'divide' => 247, 'oslash' => 248,
+ 'ugrave' => 249, 'uacute' => 250, 'ucirc' => 251, 'uuml' => 252,
+ 'yacute' => 253, 'thorn' => 254, 'yuml' => 255, 'quot' => 34,
+ 'amp' => 38, 'lt' => 60, 'gt' => 62, 'apos' => 39, 'OElig' => 338,
+ 'oelig' => 339, 'Scaron' => 352, 'scaron' => 353, 'Yuml' => 376,
+ 'circ' => 710, 'tilde' => 732, 'ensp' => 8194, 'emsp' => 8195,
+ 'thinsp' => 8201, 'zwnj' => 8204, 'zwj' => 8205, 'lrm' => 8206,
+ 'rlm' => 8207, 'ndash' => 8211, 'mdash' => 8212, 'lsquo' => 8216,
+ 'rsquo' => 8217, 'sbquo' => 8218, 'ldquo' => 8220, 'rdquo' => 8221,
+ 'bdquo' => 8222, 'dagger' => 8224, 'Dagger' => 8225, 'permil' => 8240,
+ 'lsaquo' => 8249, 'rsaquo' => 8250, 'euro' => 8364, 'fnof' => 402,
+ 'Alpha' => 913, 'Beta' => 914, 'Gamma' => 915, 'Delta' => 916,
+ 'Epsilon' => 917, 'Zeta' => 918, 'Eta' => 919, 'Theta' => 920,
+ 'Iota' => 921, 'Kappa' => 922, 'Lambda' => 923, 'Mu' => 924, 'Nu' => 925,
+ 'Xi' => 926, 'Omicron' => 927, 'Pi' => 928, 'Rho' => 929, 'Sigma' => 931,
+ 'Tau' => 932, 'Upsilon' => 933, 'Phi' => 934, 'Chi' => 935, 'Psi' => 936,
+ 'Omega' => 937, 'alpha' => 945, 'beta' => 946, 'gamma' => 947,
+ 'delta' => 948, 'epsilon' => 949, 'zeta' => 950, 'eta' => 951,
+ 'theta' => 952, 'iota' => 953, 'kappa' => 954, 'lambda' => 955,
+ 'mu' => 956, 'nu' => 957, 'xi' => 958, 'omicron' => 959, 'pi' => 960,
+ 'rho' => 961, 'sigmaf' => 962, 'sigma' => 963, 'tau' => 964,
+ 'upsilon' => 965, 'phi' => 966, 'chi' => 967, 'psi' => 968,
+ 'omega' => 969, 'thetasym' => 977, 'upsih' => 978, 'piv' => 982,
+ 'bull' => 8226, 'hellip' => 8230, 'prime' => 8242, 'Prime' => 8243,
+ 'oline' => 8254, 'frasl' => 8260, 'weierp' => 8472, 'image' => 8465,
+ 'real' => 8476, 'trade' => 8482, 'alefsym' => 8501, 'larr' => 8592,
+ 'uarr' => 8593, 'rarr' => 8594, 'darr' => 8595, 'harr' => 8596,
+ 'crarr' => 8629, 'lArr' => 8656, 'uArr' => 8657, 'rArr' => 8658,
+ 'dArr' => 8659, 'hArr' => 8660, 'forall' => 8704, 'part' => 8706,
+ 'exist' => 8707, 'empty' => 8709, 'nabla' => 8711, 'isin' => 8712,
+ 'notin' => 8713, 'ni' => 8715, 'prod' => 8719, 'sum' => 8721,
+ 'minus' => 8722, 'lowast' => 8727, 'radic' => 8730, 'prop' => 8733,
+ 'infin' => 8734, 'ang' => 8736, 'and' => 8743, 'or' => 8744, 'cap' => 8745,
+ 'cup' => 8746, 'int' => 8747, 'there4' => 8756, 'sim' => 8764,
+ 'cong' => 8773, 'asymp' => 8776, 'ne' => 8800, 'equiv' => 8801,
+ 'le' => 8804, 'ge' => 8805, 'sub' => 8834, 'sup' => 8835, 'nsub' => 8836,
+ 'sube' => 8838, 'supe' => 8839, 'oplus' => 8853, 'otimes' => 8855,
+ 'perp' => 8869, 'sdot' => 8901, 'lceil' => 8968, 'rceil' => 8969,
+ 'lfloor' => 8970, 'rfloor' => 8971, 'lang' => 9001, 'rang' => 9002,
+ 'loz' => 9674, 'spades' => 9824, 'clubs' => 9827, 'hearts' => 9829,
+ 'diams' => 9830
+ );
+}
+
diff --git a/lib/querypath/src/QueryPath/Exception.php b/lib/querypath/src/QueryPath/Exception.php
new file mode 100644
index 0000000..c84e98e
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Exception.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * @file
+ *
+ * The top-level QueryPath exception.
+ */
+namespace QueryPath;
+/**
+ * Exception indicating that a problem has occured inside of a QueryPath object.
+ *
+ * @ingroup querypath_core
+ */
+class Exception extends \Exception {}
diff --git a/lib/querypath/src/QueryPath/Extension.php b/lib/querypath/src/QueryPath/Extension.php
new file mode 100644
index 0000000..ec370ca
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Extension.php
@@ -0,0 +1,94 @@
+<?php
+/** @file
+ * This file contains the Query Path extension tools.
+ *
+ * Query Path can be extended to support additional features. To do this,
+ * you need only create a new class that implements {@link Extension}
+ * and add your own methods. This class can then be registered as an extension.
+ * It will then be available through Query Path.
+ *
+ * For information on building your own extension, see {@link Extension}.
+ * If you are trying to load an extension you have downloaded, chances are good that
+ * all you need to do is {@link require_once} the file that contains the extension.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license MIT
+ * @see Extension
+ * @see ExtensionRegistry::extend()
+ */
+namespace QueryPath;
+
+/** @addtogroup querypath_extensions Extensions
+ * The QueryPath extension system and bundled extensions.
+ *
+ * Much like jQuery, QueryPath provides a simple extension mechanism that allows
+ * extensions to auto-register themselves upon being loaded. For a simple example, see
+ * QPXML. For the internals, see QueryPath::Extension and QueryPath::with().
+ */
+
+/**
+ * A Extension is a tool that extends the capabilities of a Query object.
+ *
+ * Extensions to QueryPath should implement the Extension interface. The
+ * only requirement is that the extension provide a constructor that takes a
+ * Query object as a parameter.
+ *
+ * Here is an example QueryPath extension:
+ * @code
+ * <?php
+ * class StubExtensionOne implements \QueryPath\Extension {
+ * private $qp = NULL;
+ * public function __construct(\QueryPath\Query $qp) {
+ * $this->qp = $qp;
+ * }
+ *
+ * public function stubToe() {
+ * $this->qp->find(':root')->append('<toe/>')->end();
+ * return $this->qp;
+ * }
+ * }
+ * ?>
+ * @endcode
+ * In this example, the StubExtensionOne class implements Extension.
+ * The constructor stores a local copyof the Query object. This is important
+ * if you are planning on fully integrating with QueryPath's Fluent Interface.
+ *
+ * Finally, the stubToe() function illustrates how the extension makes use of
+ * QueryPath's Query object internally, and remains part of the fluent interface by returning
+ * the $qp object.
+ *
+ * <b>Enabling an Extension</b>
+ *
+ * To enable an extension, call the QueryPath::enable() method.
+ *
+ * @code
+ * <?php
+ * QueryPath::enable('StubExtension');
+ * ?>
+ * @endcode
+ *
+ * More complex management of extensions can be accomplished with the
+ * QueryPath::ExtensionRegistry class.
+ *
+ * <b>How is a QueryPath extension called?</b>
+ *
+ * QueryPath extensions are called like regular QueryPath functions. For
+ * example, the extension above can be called like this:
+ * <code>
+ * qp('some.xml')->stubToe();
+ * // or
+ * QueryPath::with('some.xml')->stubToe();
+ * </code>
+ * Since it returns the Query ($qp) object, chaining is supported:
+ * <code>
+ * print qp('some.xml')->stubToe()->xml();
+ * </code>
+ * When you write your own extensions, anything that does not need to return a
+ * specific value should return the Query object. Between that and the
+ * extension registry, this will provide the best developer experience.
+ *
+ * @ingroup querypath_extensions
+ */
+interface Extension {
+ public function __construct(\QueryPath\Query $qp);
+}
diff --git a/lib/querypath/src/QueryPath/Extension/QPXML.php b/lib/querypath/src/QueryPath/Extension/QPXML.php
new file mode 100644
index 0000000..5372e59
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Extension/QPXML.php
@@ -0,0 +1,211 @@
+<?php
+/** @file
+ * XML extensions. See QPXML.
+ */
+namespace QueryPath\Extension;
+
+use \QueryPath;
+/**
+ * Provide QueryPath with additional XML tools.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @author Xander Guzman <theshadow@shadowpedia.info>
+ * @license MIT
+ * @see QueryPath::Extension
+ * @see QueryPath::ExtensionRegistry::extend()
+ * @see QPXML
+ * @ingroup querypath_extensions
+ */
+class QPXML implements \QueryPath\Extension {
+
+ protected $qp;
+
+ public function __construct(\QueryPath\Query $qp) {
+ $this->qp = $qp;
+ }
+
+ public function schema($file) {
+ $doc = $this->qp->branch()->top()->get(0)->ownerDocument;
+
+ if (!$doc->schemaValidate($file)) {
+ throw new \QueryPath\Exception('Document did not validate against the schema.');
+ }
+ }
+
+ /**
+ * Get or set a CDATA section.
+ *
+ * If this is given text, it will create a CDATA section in each matched element,
+ * setting that item's value to $text.
+ *
+ * If no parameter is passed in, this will return the first CDATA section that it
+ * finds in the matched elements.
+ *
+ * @param string $text
+ * The text data to insert into the current matches. If this is NULL, then the first
+ * CDATA will be returned.
+ *
+ * @return mixed
+ * If $text is not NULL, this will return a {@link QueryPath}. Otherwise, it will
+ * return a string. If no CDATA is found, this will return NULL.
+ * @see comment()
+ * @see QueryPath::text()
+ * @see QueryPath::html()
+ */
+ public function cdata($text = NULL) {
+ if (isset($text)) {
+ // Add this text as CDATA in the current elements.
+ foreach ($this->qp->get() as $element) {
+ $cdata = $element->ownerDocument->createCDATASection($text);
+ $element->appendChild($cdata);
+ }
+ return $this->qp;;
+ }
+
+ // Look for CDATA sections.
+ foreach ($this->qp->get() as $ele) {
+ foreach ($ele->childNodes as $node) {
+ if ($node->nodeType == XML_CDATA_SECTION_NODE) {
+ // Return first match.
+ return $node->textContent;
+ }
+ }
+ }
+ return NULL;
+ // Nothing found
+ }
+
+ /**
+ * Get or set a comment.
+ *
+ * This function is used to get or set comments in an XML or HTML document.
+ * If a $text value is passed in (and is not NULL), then this will add a comment
+ * (with the value $text) to every match in the set.
+ *
+ * If no text is passed in, this will return the first comment in the set of matches.
+ * If no comments are found, NULL will be returned.
+ *
+ * @param string $text
+ * The text of the comment. If set, a new comment will be created in every item
+ * wrapped by the current {@link QueryPath}.
+ * @return mixed
+ * If $text is set, this will return a {@link QueryPath}. If no text is set, this
+ * will search for a comment and attempt to return the string value of the first
+ * comment it finds. If no comment is found, NULL will be returned.
+ * @see cdata()
+ */
+ public function comment($text = NULL) {
+ if (isset($text)) {
+ foreach ($this->qp->get() as $element) {
+ $comment = $element->ownerDocument->createComment($text);
+ $element->appendChild($comment);
+ }
+ return $this->qp;
+ }
+ foreach ($this->qp->get() as $ele) {
+ foreach ($ele->childNodes as $node) {
+ if ($node->nodeType == XML_COMMENT_NODE) {
+ // Return first match.
+ return $node->textContent;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get or set a processor instruction.
+ */
+ public function pi($prefix = NULL, $text = NULL) {
+ if (isset($text)) {
+ foreach ($this->qp->get() as $element) {
+ $comment = $element->ownerDocument->createProcessingInstruction($prefix, $text);
+ $element->appendChild($comment);
+ }
+ return $this->qp;
+ }
+ foreach ($this->qp->get() as $ele) {
+ foreach ($ele->childNodes as $node) {
+ if ($node->nodeType == XML_PI_NODE) {
+
+ if (isset($prefix)) {
+ if ($node->tagName == $prefix) {
+ return $node->textContent;
+ }
+ }
+ else {
+ // Return first match.
+ return $node->textContent;
+ }
+ }
+ } // foreach
+ } // foreach
+ }
+ public function toXml() {
+ return $this->qp->document()->saveXml();
+ }
+
+ /**
+ * Create a NIL element.
+ *
+ * @param string $text
+ * @param string $value
+ * @reval object $element
+ */
+ public function createNilElement($text, $value) {
+ $value = ($value)? 'true':'false';
+ $element = $this->qp->createElement($text);
+ $element->attr('xsi:nil', $value);
+ return $element;
+ }
+
+ /**
+ * Create an element with the given namespace.
+ *
+ * @param string $text
+ * @param string $nsUri
+ * The namespace URI for the given element.
+ * @return \QueryPath\DOMQuery
+ */
+ public function createElement($text, $nsUri = null) {
+ if (isset ($text)) {
+ foreach ($this->qp->get() as $element) {
+ if ($nsUri === null && strpos($text, ':') !== false) {
+ $ns = array_shift(explode(':', $text));
+ $nsUri = $element->ownerDocument->lookupNamespaceURI($ns);
+
+ if ($nsUri === null) {
+ throw new \QueryPath\Exception("Undefined namespace for: " . $text);
+ }
+ }
+
+ $node = null;
+ if ($nsUri !== null) {
+ $node = $element->ownerDocument->createElementNS(
+ $nsUri,
+ $text
+ );
+ } else {
+ $node = $element->ownerDocument->createElement($text);
+ }
+ return QueryPath::with($node);
+ }
+ }
+ return;
+ }
+
+ /**
+ * Append an element.
+ *
+ * @param string $text
+ * @return \QueryPath\DOMQuery
+ */
+ public function appendElement($text) {
+ if (isset ($text)) {
+ foreach ($this->qp->get() as $element) {
+ $node = $this->qp->createElement($text);
+ QueryPath::with($element)->append($node);
+ }
+ }
+ return $this->qp;
+ }
+}
diff --git a/lib/querypath/src/QueryPath/Extension/QPXSL.php b/lib/querypath/src/QueryPath/Extension/QPXSL.php
new file mode 100644
index 0000000..6a2727c
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Extension/QPXSL.php
@@ -0,0 +1,76 @@
+<?php
+/** @file
+ * Provide QueryPath with XSLT support using the PHP libxslt module.
+ *
+ * This is called 'QPXSL' instead of 'QPXSLT' in accordance with the name
+ * of the PHP extension that provides libxslt support.
+ *
+ * You must have PHP XSL support for this to function.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license MIT
+ * @see QueryPath::Extension
+ * @see QueryPath::ExtensionRegistry::extend()
+ * @see QPXSL
+ * @see QPXML
+ */
+
+namespace QueryPath\Extension;
+
+/**
+ * Provide tools for running XSL Transformation (XSLT) on a document.
+ *
+ * This extension provides the {@link QPXSL::xslt()} function, which transforms
+ * a source XML document into another XML document according to the rules in
+ * an XSLT document.
+ *
+ * This QueryPath extension can be used as follows:
+ * <code>
+ * <?php
+ * require 'QueryPath/QueryPath.php';
+ * require 'QueryPath/Extension/QPXSL.php';
+ *
+ * qp('src.xml')->xslt('stylesheet.xml')->writeXML();
+ * ?>
+ *
+ * This will transform src.xml according to the XSLT rules in
+ * stylesheet.xml. The results are returned as a QueryPath object, which
+ * is written to XML using {@link QueryPath::writeXML()}.
+ * </code>
+ *
+ * @ingroup querypath_extensions
+ */
+class QPXSL implements \QueryPath\Extension {
+
+ protected $src = NULL;
+
+ public function __construct(\QueryPath\Query $qp) {
+ $this->src = $qp;
+ }
+
+ /**
+ * Given an XSLT stylesheet, run a transformation.
+ *
+ * This will attempt to read the provided stylesheet and then
+ * execute it on the current source document.
+ *
+ * @param mixed $style
+ * This takes a QueryPath object or <em>any</em> of the types that the
+ * {@link qp()} function can take.
+ * @return QueryPath
+ * A QueryPath object wrapping the transformed document. Note that this is a
+ * <i>different</em> document than the original. As such, it has no history.
+ * You cannot call {@link QueryPath::end()} to undo a transformation. (However,
+ * the original source document will remain unchanged.)
+ */
+ public function xslt($style) {
+ if (!($style instanceof QueryPath)) {
+ $style = \QueryPath::with($style);
+ }
+ $sourceDoc = $this->src->top()->get(0)->ownerDocument;
+ $styleDoc = $style->get(0)->ownerDocument;
+ $processor = new \XSLTProcessor();
+ $processor->importStylesheet($styleDoc);
+ return \QueryPath::with($processor->transformToDoc($sourceDoc));
+ }
+}
diff --git a/lib/querypath/src/QueryPath/ExtensionRegistry.php b/lib/querypath/src/QueryPath/ExtensionRegistry.php
new file mode 100644
index 0000000..963edd3
--- /dev/null
+++ b/lib/querypath/src/QueryPath/ExtensionRegistry.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * @file
+ * The extension registry.
+ */
+namespace QueryPath;
+/**
+ * A registry for QueryPath extensions.
+ *
+ * QueryPath extensions should call the QueryPath::ExtensionRegistry::extend()
+ * function to register their extension classes. The QueryPath library then
+ * uses this information to determine what QueryPath extensions should be loaded and
+ * executed.
+ *
+ * Extensions are attached to a Query object.
+ *
+ * To enable an extension (the easy way), use QueryPath::enable().
+ *
+ * This class provides lower-level interaction with the extension
+ * mechanism.
+ *
+ * @ingroup querypath_extensions
+ */
+class ExtensionRegistry {
+ /**
+ * Internal flag indicating whether or not the registry should
+ * be used for automatic extension loading. If this is false, then
+ * implementations should not automatically load extensions.
+ */
+ public static $useRegistry = TRUE;
+ /**
+ * The extension registry. This should consist of an array of class
+ * names.
+ */
+ protected static $extensionRegistry = array();
+ protected static $extensionMethodRegistry = array();
+ /**
+ * Extend a Query with the given extension class.
+ */
+ public static function extend($classname) {
+ self::$extensionRegistry[] = $classname;
+ $class = new \ReflectionClass($classname);
+ $methods = $class->getMethods();
+ foreach ($methods as $method) {
+ self::$extensionMethodRegistry[$method->getName()] = $classname;
+ }
+ }
+
+ /**
+ * Check to see if a method is known.
+ * This checks to see if the given method name belongs to one of the
+ * registered extensions. If it does, then this will return TRUE.
+ *
+ * @param string $name
+ * The name of the method to search for.
+ * @return boolean
+ * TRUE if the method exists, false otherwise.
+ */
+ public static function hasMethod($name) {
+ return isset(self::$extensionMethodRegistry[$name]);
+ }
+
+ /**
+ * Check to see if the given extension class is registered.
+ * Given a class name for a QueryPath::Extension class, this
+ * will check to see if that class is registered. If so, it will return
+ * TRUE.
+ *
+ * @param string $name
+ * The name of the class.
+ * @return boolean
+ * TRUE if the class is registered, FALSE otherwise.
+ */
+ public static function hasExtension($name) {
+ return in_array($name, self::$extensionRegistry);
+ }
+
+ /**
+ * Get the class that a given method belongs to.
+ * Given a method name, this will check all registered extension classes
+ * to see if any of them has the named method. If so, this will return
+ * the classname.
+ *
+ * Note that if two extensions are registered that contain the same
+ * method name, the last one registred will be the only one recognized.
+ *
+ * @param string $name
+ * The name of the method.
+ * @return string
+ * The name of the class.
+ */
+ public static function getMethodClass($name) {
+ return self::$extensionMethodRegistry[$name];
+ }
+
+ /**
+ * Get extensions for the given Query object.
+ *
+ * Given a Query object, this will return
+ * an associative array of extension names to (new) instances.
+ * Generally, this is intended to be used internally.
+ *
+ * @param Query $qp
+ * The Query into which the extensions should be registered.
+ * @return array
+ * An associative array of classnames to instances.
+ */
+ public static function getExtensions(Query $qp) {
+ $extInstances = array();
+ foreach (self::$extensionRegistry as $ext) {
+ $extInstances[$ext] = new $ext($qp);
+ }
+ return $extInstances;
+ }
+
+ public static function extensionNames() {
+ return self::$extensionRegistry;
+ }
+
+ /**
+ * Enable or disable automatic extension loading.
+ *
+ * If extension autoloading is disabled, then QueryPath will not
+ * automatically load all registred extensions when a new Query
+ * object is created using qp().
+ */
+ public static function autoloadExtensions($boolean = TRUE) {
+ self::$useRegistry = $boolean;
+ }
+}
diff --git a/lib/querypath/src/QueryPath/IOException.php b/lib/querypath/src/QueryPath/IOException.php
new file mode 100644
index 0000000..b23b381
--- /dev/null
+++ b/lib/querypath/src/QueryPath/IOException.php
@@ -0,0 +1,20 @@
+<?php
+/**
+ * @file
+ *
+ * General IO exception.
+ */
+
+namespace QueryPath;
+
+/**
+ * Indicates that an input/output exception has occurred.
+ *
+ * @ingroup querypath_core
+ */
+class IOException extends \QueryPath\ParseException {
+ public static function initializeFromError($code, $str, $file, $line, $cxt) {
+ $class = __CLASS__;
+ throw new $class($str, $code, $file, $line);
+ }
+}
diff --git a/lib/querypath/src/QueryPath/Options.php b/lib/querypath/src/QueryPath/Options.php
new file mode 100644
index 0000000..846984d
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Options.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * @file
+ *
+ * Options management.
+ */
+namespace QueryPath;
+
+
+/**
+ * Manage default options.
+ *
+ * This class stores the default options for QueryPath. When a new
+ * QueryPath object is constructed, options specified here will be
+ * used.
+ *
+ * <b>Details</b>
+ * This class defines no options of its own. Instead, it provides a
+ * central tool for developers to override options set by QueryPath.
+ * When a QueryPath object is created, it will evaluate options in the
+ * following order:
+ *
+ * - Options passed into qp() have highest priority.
+ * - Options in QueryPath::Options (this class) have the next highest priority.
+ * - If the option is not specified elsewhere, QueryPath will use its own defaults.
+ *
+ * @see qp()
+ * @see QueryPath::Options::set()
+ * @ingroup querypath_util
+ */
+class Options {
+ /**
+ * This is the static options array.
+ *
+ * Use the {@link set()}, {@link get()}, and {@link merge()} to
+ * modify this array.
+ */
+ static $options = array();
+ /**
+ * Set the default options.
+ *
+ * The passed-in array will be used as the default options list.
+ *
+ * @param array $array
+ * An associative array of options.
+ */
+ static function set($array) {
+ self::$options = $array;
+ }
+ /**
+ * Get the default options.
+ *
+ * Get all options currently set as default.
+ *
+ * @return array
+ * An array of options. Note that only explicitly set options are
+ * returned. {@link QueryPath} defines default options which are not
+ * stored in this object.
+ */
+ static function get() {
+ return self::$options;
+ }
+ /**
+ * Merge the provided array with existing options.
+ *
+ * On duplicate keys, the value in $array will overwrite the
+ * value stored in the options.
+ *
+ * @param array $array
+ * Associative array of options to merge into the existing options.
+ */
+ static function merge($array) {
+ self::$options = $array + self::$options;
+ }
+ /**
+ * Returns true of the specified key is already overridden in this object.
+ *
+ * @param string $key
+ * The key to search for.
+ */
+ static function has($key) {
+ return array_key_exists($key, self::$options);
+ }
+}
diff --git a/lib/querypath/src/QueryPath/ParseException.php b/lib/querypath/src/QueryPath/ParseException.php
new file mode 100644
index 0000000..7da7beb
--- /dev/null
+++ b/lib/querypath/src/QueryPath/ParseException.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * @file
+ * Query path parsing exception.
+ */
+
+namespace QueryPath;
+
+/**
+ * Exception indicating that a parser has failed to parse a file.
+ *
+ * This will report parser warnings as well as parser errors. It should only be
+ * thrown, though, under error conditions.
+ *
+ * @ingroup querypath_core
+ */
+class ParseException extends \QueryPath\Exception {
+ const ERR_MSG_FORMAT = 'Parse error in %s on line %d column %d: %s (%d)';
+ const WARN_MSG_FORMAT = 'Parser warning in %s on line %d column %d: %s (%d)';
+ // trigger_error
+ public function __construct($msg = '', $code = 0, $file = NULL, $line = NULL) {
+
+ $msgs = array();
+ foreach(libxml_get_errors() as $err) {
+ $format = $err->level == LIBXML_ERR_WARNING ? self::WARN_MSG_FORMAT : self::ERR_MSG_FORMAT;
+ $msgs[] = sprintf($format, $err->file, $err->line, $err->column, $err->message, $err->code);
+ }
+ $msg .= implode("\n", $msgs);
+
+ if (isset($file)) {
+ $msg .= ' (' . $file;
+ if (isset($line)) $msg .= ': ' . $line;
+ $msg .= ')';
+ }
+
+ parent::__construct($msg, $code);
+ }
+
+ public static function initializeFromError($code, $str, $file, $line, $cxt) {
+ //printf("\n\nCODE: %s %s\n\n", $code, $str);
+ $class = __CLASS__;
+ throw new $class($str, $code, $file, $line);
+ }
+}
diff --git a/lib/querypath/src/QueryPath/Query.php b/lib/querypath/src/QueryPath/Query.php
new file mode 100644
index 0000000..81b5cae
--- /dev/null
+++ b/lib/querypath/src/QueryPath/Query.php
@@ -0,0 +1,12 @@
+<?php
+namespace QueryPath;
+interface Query {
+ public function __construct($document = NULL, $selector = NULL, $options = NULL);
+ public function find($selector);
+ public function top($selector = NULL);
+ public function next($selector = NULL);
+ public function prev($selector = NULL);
+ public function siblings($selector = NULL);
+ public function parent($selector = NULL);
+ public function children($selector = NULL);
+}
diff --git a/lib/querypath/src/QueryPath/QueryPathIterator.php b/lib/querypath/src/QueryPath/QueryPathIterator.php
new file mode 100644
index 0000000..be2e66f
--- /dev/null
+++ b/lib/querypath/src/QueryPath/QueryPathIterator.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * @file
+ *
+ * Utility iterator for QueryPath.
+ */
+namespace QueryPath;
+
+/**
+ * An iterator for QueryPath.
+ *
+ * This provides iterator support for QueryPath. You do not need to construct
+ * a QueryPathIterator. QueryPath does this when its QueryPath::getIterator()
+ * method is called.
+ *
+ * @ingroup querypath_util
+ */
+class QueryPathIterator extends \IteratorIterator {
+ public $options = array();
+ private $qp = NULL;
+
+ public function current() {
+ if (!isset($this->qp)) {
+ $this->qp = \QueryPath::with(parent::current(), NULL, $this->options);
+ }
+ else {
+ $splos = new \SplObjectStorage();
+ $splos->attach(parent::current());
+ $this->qp->setMatches($splos);
+ }
+ return $this->qp;
+ }
+}
diff --git a/lib/querypath/src/documentation.php b/lib/querypath/src/documentation.php
new file mode 100644
index 0000000..3649a5b
--- /dev/null
+++ b/lib/querypath/src/documentation.php
@@ -0,0 +1,261 @@
+<?php
+/** @mainpage QueryPath: Find Your Way
+ * @image html querypath-200x333.png
+ * QueryPath is a PHP library for working with XML and HTML. It is a PHP implementation of jQuery's
+ * traversal and modification libraries.
+ *
+ * @section getting_started Getting Started
+ *
+ * To being using QueryPath, you will probably want to take a look at these three pieces of
+ * documentation:
+ * - qp(): The main QueryPath function (like jQuery's $ function.)
+ * - htmlqp(): A specialized version of qp() for dealing with poorly formatted HTML.
+ * - QueryPath: The QueryPath class, which has all of the main functions.
+ *
+ * One substantial difference from jQuery is that QueryPath does not return a new object for
+ * each call (for performance reasons). Instead, the same object is mutated from call to call.
+ * A chain, then, typically performs all methods on the same object.
+ * When you need multiple objects, QueryPath has a {@link QueryPath::branch()} function that
+ * will return a cloned QueryPath object.
+ *
+ * QueryPath also has numerous functions that jQuery does not. Some (like QueryPath::top() and
+ * QueryPath::dataURL()) are extensions we find useful.
+ * Most, however, are to either emphasize PHP features (QueryPath::filterPreg()) or adapt to
+ * server-side needs (QueryPathEntities::replaceAllEntities()).
+ *
+ * @subsection basic_example A Few Basic Examples
+ *
+ * Here is a basic example of QueryPath usage:
+ *
+ * @code
+ * require 'QueryPath/QueryPath.php';
+ *
+ * qp('<?xml version="1.0"?><root><foo/></root>', 'foo')->append('<bar>baz</bar>')->writeXML();
+ * @endcode
+ *
+ * The above will create a new document from the XML string, find the <code>foo</code> element, and then
+ * append the <code>bar</code> element (complete with its text). Finally, the call to QueryPath::writeXML() will
+ * print the entire finished XML document to standard out (usually the web browser).
+ *
+ * Here's an example using htmlqp():
+ *
+ * @code
+ * require 'QueryPath/QueryPath.php';
+ *
+ * // URL to fetch:
+ * $url = 'http://technosophos.com';
+ *
+ * print htmlqp($url, 'title')->text();
+ * @endcode
+ *
+ * The above will fetch the HTML from the given URL and then find the <code>title</code> tag. It will extract
+ * the text (QueryPath::text()) from the title and print it.
+ *
+ * For more examples, check out the #Examples namespace (start with {@link examples/html.php}). Also, read about the
+ * qp() and htmlqp() functions.
+ *
+ * @subsection online_sources Online Sources
+ *
+ * - The official QueryPath site http://querypath.org
+ * - The latest API docs http://api.querypath.org
+ * - IBM DeveloperWorks Intro to QueryPath http://www.ibm.com/developerworks/web/library/os-php-querypath/index.html
+ * - QueryPath articles at TechnoSophos.Com http://technosophos.com/qp/articles
+ * - The QueryPath GitHub repository http://github.com/technosophos/querypath
+ *
+ * If you find a good online resource, please submit it as an issue in GitHub, and we will
+ * most likely add it here.
+ *
+ * @subsection more_examples A Larger Example
+ *
+ * @include examples/html.php
+ *
+ * @page extensions Using and Writing Extensions
+ *
+ * Using an extension is as easy as including it in your code:
+ *
+ * @code
+ * <?php
+ * require 'QueryPath/QueryPath.php';
+ * require 'QueryPath/Extension/QPXML.php';
+ *
+ * // Now I have the QPXML methods available:
+ * qp(QueryPath::HTML_STUB)->comment('This is an HTML comment.');
+ * ?>
+ * @endcode
+ *
+ * Like jQuery, QueryPath provides a simple mechanism for writing extensions.
+ *
+ * Check out QPXSL and QPXML for a few easy-to-read extensions. QPDB provides an example of
+ * a more complex extension.
+ *
+ * QueryPathExtension is the master interface for all extensions.
+ *
+ */
+
+
+/** @page CSSReference CSS Selector Reference
+ * QueryPath provides two query 'languages' that you can use to search through XML and HTML
+ * documents. The main query language is an implementation of the CSS3 Selectors specification. This
+ * is the query language that jQuery and CSS use -- and more recently, FireFox itself supports it
+ * via its JavaScript API. CSS3 should be familiar to developers and designers who have worked with
+ * HTML and stylesheets.
+ *
+ * QueryPath also allows XPath selectors, which can be executed using QueryPath::xpath(). While
+ * fewer functions take XPath expressions, it is noless a powerful tool for querying DOM objects.
+ *
+ * @code
+ * <?php
+ * qp($xml)->xpath('//foo');
+ * ?>
+ * @endcode
+ *
+ * QueryPath provides a full CSS3 selector implementation, including all of the specified operators,
+ * robost not() and has() support, pseudo-class/elements, and XML namespace support.
+ *
+ * Selectors can be passed into a number of QueryPath functions including qp(), htmlqp(),
+ * QueryPath::find(), QueryPath::top(), QueryPath::children() and others.
+ * @code
+ * <?php
+ * $qp = qp($html, 'body'); // Find the body
+ * $another_qp = $qp->branch('p'); // Create another QP object that searches BODY for P tags.
+ * $qp->find('strong>a'); // Find all of the A elements directly inside of STRONG elements.
+ * $qp->top('head'); // Start over at the top of the document, and find the HEAD tag.
+ * ?>
+ * @endcode
+ *
+ * In all of the examples above, CSS selectors are used to locate specific things inside of the
+ * document.
+ *
+ * @section selector_examples Example Selectors
+ * Example selectors:
+ * - <code>p</code>: Select all P elements in a document.
+ * - <code>strong a</code>: Select any A elements that are inside (children or descendants of) a STRONG
+ * element.
+ * - <code>strong>a</code>: Select only A elements that are directly beneath STRONG elements.
+ * - <code>:root>head</code> select HEAD elements that are directly beneath the document root.
+ * - <code>h1, h2</code>: Select all H1's and H2's.
+ * - <code>a:link</code>: Select all A tags that have hrefs
+ * - <code>div.content</code>: Select all DIV elements that have the class=content set.
+ * - <code>#my-id</code>: Select the element that has id=my-id.
+ * - <code>p:contains(Hello World)</code>: Select any P elements that have the text Hello World.
+ * - <code>p:not(.nav)</code>: Select any elements in P that do not have the nav class.
+ *
+ * @section pseudo_reference Pseudo-class and pseudo-element selectors
+ * QueryPath provides an implementation of the CSS3 spec, including the CSS3 pseudo-classes and
+ * pseudo-elements defined in the spec. Some of the CSS3 pseudo-classes require a user agent, and
+ * so cannot be adequately captured on the server side, but all others have been implemented.
+ *
+ * Additionally, jQuery has added its own pseudo-classes, and jQuery users have come to expect those
+ * to work. So for the sake of convenience, we have implemented those as well. These include the
+ * form pseudo-classes, along with several others.
+ *
+ * Finally, QueryPath has added a couple of useful pseudo-classes, namely :x-root and
+ * :contains-exactly.
+ *
+ * @subsection pseudoelement_ref Pseudo-Elements
+ *
+ * Pseudo-elements are new in CSS3, and are syntactically similar to pseudo-classes. To use a
+ * pseudo-element in a selector, use the double-colon syntax: <code>::begins</code>. The following pseudo-
+ * elements are defined in QueryPath:
+ *
+ * - first-line: Selects the first line -- everything up to the first LF character (\ n).
+ * - first-letter: Slects the first letter of the element.
+ *
+ * These throw exceptions because they cannot be implemented without a user agent:
+ * - before
+ * - after
+ * - selection
+ *
+ * Pseudo-elements should be used with care, as they act like elements, but are not.
+ *
+ * @code
+ * <?php
+ * $textNode = qp($xml, 'p::first-letter')->get();
+ * ?>
+ * @endcode
+ *
+ * @subsection pseudoclass_reference Pseudo-Classes
+ *
+ * Pseudo-classes are more familiar to CSS and jQuery users. They use a single-colon syntax, and
+ * are used to narrow the set of selected elements.
+ *
+ * The following pseudo-classes are supported:
+ * - link: Matches anything with the href attribute.
+ * - root: The root element of the document
+ * - x-root: The root element that was passed into QueryPath's constructor
+ * - x-reset: Same as above.
+ * - even: All even elements in a set. First element is odd.
+ * - odd: Odd elements in a set. First element is odd.
+ * - nth-child: Every nth child in a set.
+ * - nth-last-child: Every nth child in a set, counting from the end.
+ * - nth-of-type: Every nth tag in a set.
+ * - nth-last-of-type: Every nth tag in a set, counting from the end.
+ * - first-child: The first child in a set.
+ * - last-child: The last child in a set.
+ * - first-of-type: The first child of the specified tag in a set.
+ * - last-of-type: The last child of the specified tag in a set.
+ * - only-child: Matches only if this is the only child in a set.
+ * - only-of-type: Matches only if it is the only child of the given tag in a set.
+ * - empty: Selects only empty elements.
+ * - not: The negation operator, takes a CSS3 selector, e.g. <code>:not(strong>a)</code>.
+ * - lt: Items in a set whose index is less than the given integer, e.g. <code>lt(3)</code>
+ * - gt: Items in a set whose index is greater than the given integer, e.g. <code>gt(3)</code>
+ * - nth: The nth item in a set, e.g. <code>nth(3)</code>
+ * - eq: The nth item in a set, e.g. <code>eq(3)</code>
+ * - first: The first item in a set.
+ * - last: The last item in a set.
+ * - parent: Matches if the item is a parent of child elements.
+ * - enabled: Matches (form) items that are enabled
+ * - disabled: Matches form items that are disabled
+ * - checked: Matches form items that are checked
+ * - text: Matches form items that are text fields
+ * - radio: Matches form items that are radio fields
+ * - checkbox: Matches form items that are checkboxes.
+ * - file: Matches form items that are file upload widgets.
+ * - password: Mathces form items that are password entry boxes.
+ * - submit: Matches submit buttons
+ * - image: Matches image buttons
+ * - reset: Matches reset buttons
+ * - button: Matches buttons
+ * - header: Matches header fields (h1-h6)
+ * - has: Matches any items that have children that match the given selector, e.g. <code>:has(strong>a)</code>
+ * - contains: Contains *text* that matches. This is a substring match.
+ * - contains-exactly: Contains *exactly* the given text. This is NOT a substring match.
+ *
+ * These generate errors because they are not implemented:
+ * - indeterminate
+ * - lang
+ *
+ * These are quietly ignored because they require a user agent to be meaningful.
+ * - visited
+ * - hover
+ * - active
+ * - focus
+ * - animated
+ * - visible
+ * - hidden
+ * - target
+ *
+ * Examples:
+ * @code
+ * <?php
+ * qp($html, 'form input:text'); // Get all text input elements in a form.
+ * ?>
+ * @endcode
+ * @section xml_namespaces XML Namespaces
+ * QueryPath also supports the CSS3 namespace selection syntax. <b>This is syntactically different
+ * than the XML namespace tag format</b>. To select a tag whose namespaced name is foo:bar, the
+ * CSS element selector would be <code>foo|bar</code> (note the vertical bar instead of a colon). While
+ * QueryPath does its best to resolve namespaces to short names, there is a possibility that a
+ * malformed namespace will prevent specific namespace queries.
+ *
+ * You can also query across namespaces with <code>*|tagname</code>.
+ *
+ * @code
+ * <?php
+ * qp($xml, 'atom|entry'); // Find all <atom:entry> elements.
+ * qp($xml, 'atom|entry > xmedia|video'); // Find all <xmedia:video> elements directly inside <atom:entry> elements.
+ * qp($xml, '*|entry'); // Find any namespaced tag that has `entry` as the tag name.
+ * ?>
+ * @endcode
+ */
diff --git a/lib/querypath/src/qp.php b/lib/querypath/src/qp.php
new file mode 100644
index 0000000..9d38137
--- /dev/null
+++ b/lib/querypath/src/qp.php
@@ -0,0 +1,82 @@
+<?php
+/**
+ * @file
+ *
+ * QueryPath bootstrap.
+ *
+ * This file holds bootstrap code to load the QueryPath library.
+ *
+ * Usage:
+ *
+ * @code
+ * <?php
+ * require 'qp.php';
+ *
+ * qp($xml)->find('foo')->count();
+ * ?>
+ * @endcode
+ *
+ * If no autoloader is currently operating, this will use
+ * QueryPath's default autoloader **unless**
+ * QP_NO_AUTOLOADER is defined, in which case all of the
+ * files will be statically required in.
+ */
+
+// This is sort of a last ditch attempt to load QueryPath if no
+// autoloader is used.
+if (!class_exists('\QueryPath')) {
+
+ // If classloaders are explicitly disabled, load everything.
+ if (defined('QP_NO_AUTOLOADER')) {
+ // This is all (and only) the required classes for QueryPath.
+ // Extensions are not loaded automatically.
+ require __DIR__ . '/QueryPath/Exception.php';
+ require __DIR__ . '/QueryPath/ParseException.php';
+ require __DIR__ . '/QueryPath/IOException.php';
+ require __DIR__ . '/QueryPath/CSS/ParseException.php';
+ require __DIR__ . '/QueryPath/CSS/NotImplementedException.php';
+ require __DIR__ . '/QueryPath/CSS/EventHandler.php';
+ require __DIR__ . '/QueryPath/CSS/SimpleSelector.php';
+ require __DIR__ . '/QueryPath/CSS/Selector.php';
+ require __DIR__ . '/QueryPath/CSS/Traverser.php';
+ require __DIR__ . '/QueryPath/CSS/DOMTraverser/PseudoClass.php';
+ // require __DIR__ . '/QueryPath/CSS/DOMTraverser/PseudoElement.php';
+ require __DIR__ . '/QueryPath/CSS/DOMTraverser/Util.php';
+ require __DIR__ . '/QueryPath/CSS/DOMTraverser.php';
+ require __DIR__ . '/QueryPath/CSS/Token.php';
+ require __DIR__ . '/QueryPath/CSS/InputStream.php';
+ require __DIR__ . '/QueryPath/CSS/Scanner.php';
+ require __DIR__ . '/QueryPath/CSS/Parser.php';
+ require __DIR__ . '/QueryPath/CSS/QueryPathEventHandler.php';
+ require __DIR__ . '/QueryPath/Query.php';
+ require __DIR__ . '/QueryPath/Entities.php';
+ require __DIR__ . '/QueryPath/Extension.php';
+ require __DIR__ . '/QueryPath/ExtensionRegistry.php';
+ require __DIR__ . '/QueryPath/Options.php';
+ require __DIR__ . '/QueryPath/QueryPathIterator.php';
+ require __DIR__ . '/QueryPath/DOMQuery.php';
+ require __DIR__ . '/QueryPath.php';
+ }
+ else {
+ spl_autoload_register(function ($klass) {
+ $parts = explode('\\', $klass);
+
+ // Issue #164
+ if ($parts[0] == '') {
+ array_shift($parts);
+ }
+
+ if ($parts[0] == 'QueryPath') {
+ $path = __DIR__ . '/' . implode('/', $parts) . '.php';
+ if (file_exists($path)) {
+ require $path;
+ }
+ }
+ });
+ }
+}
+
+// Define qp() and qphtml() function.
+if (!function_exists('qp')) {
+ require __DIR__ . '/qp_functions.php';
+}
diff --git a/lib/querypath/src/qp_functions.php b/lib/querypath/src/qp_functions.php
new file mode 100644
index 0000000..a6afdc1
--- /dev/null
+++ b/lib/querypath/src/qp_functions.php
@@ -0,0 +1,216 @@
+<?php
+/**
+ * @file
+ *
+ * QueryPath functions.
+ *
+ * This file holds the QueryPath functions, qp() and htmlqp().
+ *
+ * Usage:
+ *
+ * @code
+ * <?php
+ * require 'qp.php';
+ *
+ * qp($xml)->find('foo')->count();
+ * ?>
+ * @endcode
+ */
+
+/** @addtogroup querypath_core Core API
+ * Core classes and functions for QueryPath.
+ *
+ * These are the classes, objects, and functions that developers who use QueryPath
+ * are likely to use. The qp() and htmlqp() functions are the best place to start,
+ * while most of the frequently used methods are part of the QueryPath object.
+ */
+
+/** @addtogroup querypath_util Utilities
+ * Utility classes for QueryPath.
+ *
+ * These classes add important, but less-often used features to QueryPath. Some of
+ * these are used transparently (QueryPathIterator). Others you can use directly in your
+ * code (QueryPathEntities).
+ */
+
+/** @namespace QueryPath
+ * The core classes that compose QueryPath.
+ *
+ * The QueryPath classes contain the brunt of the QueryPath code. If you are
+ * interested in working with just the CSS engine, you may want to look at CssEventHandler,
+ * which can be used without the rest of QueryPath. If you are interested in looking
+ * carefully at QueryPath's implementation details, then the QueryPath class is where you
+ * should begin. If you are interested in writing extensions, than you may want to look at
+ * QueryPathExtension, and also at some of the simple extensions, such as QPXML.
+ */
+
+/**
+ * Build a new Query Path.
+ * This builds a new Query Path object. The new object can be used for
+ * reading, search, and modifying a document.
+ *
+ * While it is permissible to directly create new instances of a QueryPath
+ * implementation, it is not advised. Instead, you should use this function
+ * as a factory.
+ *
+ * Example:
+ * @code
+ * <?php
+ * qp(); // New empty QueryPath
+ * qp('path/to/file.xml'); // From a file
+ * qp('<html><head></head><body></body></html>'); // From HTML or XML
+ * qp(QueryPath::XHTML_STUB); // From a basic HTML document.
+ * qp(QueryPath::XHTML_STUB, 'title'); // Create one from a basic HTML doc and position it at the title element.
+ *
+ * // Most of the time, methods are chained directly off of this call.
+ * qp(QueryPath::XHTML_STUB, 'body')->append('<h1>Title</h1>')->addClass('body-class');
+ * ?>
+ * @endcode
+ *
+ * This function is used internally by QueryPath. Anything that modifies the
+ * behavior of this function may also modify the behavior of common QueryPath
+ * methods.
+ *
+ * <b>Types of documents that QueryPath can support</b>
+ *
+ * qp() can take any of these as its first argument:
+ *
+ * - A string of XML or HTML (See {@link XHTML_STUB})
+ * - A path on the file system or a URL
+ * - A {@link DOMDocument} object
+ * - A {@link SimpleXMLElement} object.
+ * - A {@link DOMNode} object.
+ * - An array of {@link DOMNode} objects (generally {@link DOMElement} nodes).
+ * - Another {@link QueryPath} object.
+ *
+ * Keep in mind that most features of QueryPath operate on elements. Other
+ * sorts of DOMNodes might not work with all features.
+ *
+ * <b>Supported Options</b>
+ * - context: A stream context object. This is used to pass context info
+ * to the underlying file IO subsystem.
+ * - encoding: A valid character encoding, such as 'utf-8' or 'ISO-8859-1'.
+ * The default is system-dependant, typically UTF-8. Note that this is
+ * only used when creating new documents, not when reading existing content.
+ * (See convert_to_encoding below.)
+ * - parser_flags: An OR-combined set of parser flags. The flags supported
+ * by the DOMDocument PHP class are all supported here.
+ * - omit_xml_declaration: Boolean. If this is TRUE, then certain output
+ * methods (like {@link QueryPath::xml()}) will omit the XML declaration
+ * from the beginning of a document.
+ * - format_output: Boolean. If this is set to TRUE, QueryPath will format
+ * the HTML or XML output to make it more readible. If this is set to
+ * FALSE, QueryPath will minimize whitespace to keep the document smaller
+ * but harder to read.
+ * - replace_entities: Boolean. If this is TRUE, then any of the insertion
+ * functions (before(), append(), etc.) will replace named entities with
+ * their decimal equivalent, and will replace un-escaped ampersands with
+ * a numeric entity equivalent.
+ * - ignore_parser_warnings: Boolean. If this is TRUE, then E_WARNING messages
+ * generated by the XML parser will not cause QueryPath to throw an exception.
+ * This is useful when parsing
+ * badly mangled HTML, or when failure to find files should not result in
+ * an exception. By default, this is FALSE -- that is, parsing warnings and
+ * IO warnings throw exceptions.
+ * - convert_to_encoding: Use the MB library to convert the document to the
+ * named encoding before parsing. This is useful for old HTML (set it to
+ * iso-8859-1 for best results). If this is not supplied, no character set
+ * conversion will be performed. See {@link mb_convert_encoding()}.
+ * (QueryPath 1.3 and later)
+ * - convert_from_encoding: If 'convert_to_encoding' is set, this option can be
+ * used to explicitly define what character set the source document is using.
+ * By default, QueryPath will allow the MB library to guess the encoding.
+ * (QueryPath 1.3 and later)
+ * - strip_low_ascii: If this is set to TRUE then markup will have all low ASCII
+ * characters (<32) stripped out before parsing. This is good in cases where
+ * icky HTML has (illegal) low characters in the document.
+ * - use_parser: If 'xml', Parse the document as XML. If 'html', parse the
+ * document as HTML. Note that the XML parser is very strict, while the
+ * HTML parser is more lenient, but does enforce some of the DTD/Schema.
+ * <i>By default, QueryPath autodetects the type.</i>
+ * - escape_xhtml_js_css_sections: XHTML needs script and css sections to be
+ * escaped. Yet older readers do not handle CDATA sections, and comments do not
+ * work properly (for numerous reasons). By default, QueryPath's *XHTML methods
+ * will wrap a script body with a CDATA declaration inside of C-style comments.
+ * If you want to change this, you can set this option with one of the
+ * JS_CSS_ESCAPE_* constants, or you can write your own.
+ * - QueryPath_class: (ADVANCED) Use this to set the actual classname that
+ * {@link qp()} loads as a QueryPath instance. It is assumed that the
+ * class is either {@link QueryPath} or a subclass thereof. See the test
+ * cases for an example.
+ *
+ * @ingroup querypath_core
+ * @param mixed $document
+ * A document in one of the forms listed above.
+ * @param string $string
+ * A CSS 3 selector.
+ * @param array $options
+ * An associative array of options. Currently supported options are listed above.
+ * @return \QueryPath\DOMQuery
+ * Or possibly another QueryPath-like object if you overrode QueryPath_class.
+ */
+function qp($document = NULL, $string = NULL, $options = array()) {
+ return QueryPath::with($document, $string, $options);
+}
+
+/**
+ * A special-purpose version of {@link qp()} designed specifically for HTML.
+ *
+ * XHTML (if valid) can be easily parsed by {@link qp()} with no problems. However,
+ * because of the way that libxml handles HTML, there are several common steps that
+ * need to be taken to reliably parse non-XML HTML documents. This function is
+ * a convenience tool for configuring QueryPath to parse HTML.
+ *
+ * The following options are automatically set unless overridden:
+ * - ignore_parser_warnings: TRUE
+ * - convert_to_encoding: ISO-8859-1 (the best for the HTML parser).
+ * - convert_from_encoding: auto (autodetect encoding)
+ * - use_parser: html
+ *
+ * Parser warning messages are also suppressed, so if the parser emits a warning,
+ * the application will not be notified. This is equivalent to
+ * calling @code@qp()@endcode.
+ *
+ * Warning: Character set conversions will only work if the Multi-Byte (mb) library
+ * is installed and enabled. This is usually enabled, but not always.
+ *
+ * @ingroup querypath_core
+ * @see qp()
+ */
+function htmlqp($document = NULL, $selector = NULL, $options = array()) {
+
+ return QueryPath::withHTML($document, $selector, $options);
+}
+
+/**
+ * Parse HTML5 documents.
+ *
+ * This uses HTML5-PHP to parse the document. In actuality, this parser does
+ * a fine job with pre-HTML5 documents in most cases, though really old HTML
+ * (like 2.0) may have some substantial quirks.
+ *
+ * <b>Supported Options</b>
+ * Any options supported by HTML5-PHP are allowed here. Additionally, the
+ * following options have meaning to QueryPath.
+ * - QueryPath_class
+ *
+ *
+ * @param mixed $source
+ * A document as an HTML string, or a path/URL. For compatibility with
+ * existing functions, a DOMDocument, SimpleXMLElement, DOMNode or array
+ * of DOMNodes will be passed through as well. However, these types are not
+ * validated in any way.
+ *
+ * @param string $selector
+ * A CSS3 selector.
+ *
+ * @param array $options
+ * An associative array of options, which is passed on into HTML5-PHP. Note
+ * that the standard QueryPath options may be ignored for this function,
+ * since it uses a different parser.
+ *
+ * @return \QueryPath\DOMQuery
+ */
+function html5qp($document = NULL, $selector = NULL, $options = array()) {
+ return QueryPath::withHTML5($document, $selector, $options);
+}
diff --git a/lib/querypath/test/DOMTraverserTest.xml b/lib/querypath/test/DOMTraverserTest.xml
new file mode 100644
index 0000000..48288ad
--- /dev/null
+++ b/lib/querypath/test/DOMTraverserTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<root xmlns="urn:foo/bar" xmlns:test="urn:foo/bar/baz">
+ <outside level="1" id="first-outside" class="first">
+ <inside level="2" type="thing">
+ <bottom/>
+ </inside>
+ <inside level="2" type="thing">
+ <bottom level="3"/>
+ </inside>
+ </outside>
+ <outside level="1" id="second-outside">
+ <ul>
+ <li class="first">one</li>
+ <li>two</li>
+ <li>three</li>
+ <li>four</li>
+ <li>five</li>
+ <li class="last">six</li>
+ </ul>
+ </outside>
+ <outside level="1" id="third-outside">
+ <a class="a1"><a class="a2"><a class="a3">bottom</a></a></a>
+ </outside>
+ <crowded attr1="one" attr2="two" attr3="three" attr4="four"/>
+ <classtest class="one two three four five six seven"/>
+ <idtest id="idtest-one"/>
+ <p author="T.S. Eliot">this is the way the world ends. Not with a bang, but a whimper.</p>
+ ==FOO==
+ <one><two><three><four>target</four></three></two></one>
+ <test:ns_test test:level="1">
+ <ns_attr test:myattr="foo">text</ns_attr>
+ <newns:my_element xmlns:newns="urn:new/ns">More text</newns:my_element>
+ </test:ns_test>
+ <ns_test/><ns_test xmlns:nuther="urn:nuther" ping="1" test:ping="2" nuther:ping="3"/>
+</root>
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/DOMTraverserTest.php b/lib/querypath/test/Tests/QueryPath/CSS/DOMTraverserTest.php
new file mode 100644
index 0000000..9542482
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/DOMTraverserTest.php
@@ -0,0 +1,357 @@
+<?php
+/**
+ * @file
+ * CSS Event handling tests
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\Token;
+use \QueryPath\CSS\DOMTraverser;
+use \QueryPath\CSS\Parser;
+use \QueryPath\CSS\EventHandler;
+
+define('TRAVERSER_XML', __DIR__ . '/../../../DOMTraverserTest.xml');
+
+/**
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class DOMTraverserTest extends TestCase {
+
+ protected $xml_file = TRAVERSER_XML;
+ public function debug($msg) {
+ fwrite(STDOUT, PHP_EOL . $msg);
+ }
+
+ public function testConstructor() {
+ $dom = new \DOMDocument('1.0');
+ $dom->load($this->xml_file);
+
+ $splos = new \SPLObjectStorage();
+ $splos->attach($dom);
+
+ $traverser = new DOMTraverser($splos);
+
+ $this->assertInstanceOf('\QueryPath\CSS\Traverser', $traverser);
+ $this->assertInstanceOf('\QueryPath\CSS\DOMTraverser', $traverser);
+ }
+
+ protected function traverser() {
+ $dom = new \DOMDocument('1.0');
+ $dom->load($this->xml_file);
+
+ $splos = new \SPLObjectStorage();
+ $splos->attach($dom->documentElement);
+
+ $traverser = new DOMTraverser($splos);
+
+ return $traverser;
+ }
+
+ protected function find($selector) {
+ return $this->traverser()->find($selector)->matches();
+ }
+
+ public function testFind() {
+ $res = $this->traverser()->find('root');
+
+ // Ensure that return contract is not violated.
+ $this->assertInstanceOf('\QueryPath\CSS\Traverser', $res);
+ }
+
+ public function testMatches() {
+ $res = $this->traverser()->matches();
+ $this->assertEquals(1, count($res));
+ }
+
+ public function testMatchElement() {
+ // Canary: If element does not exist, must return FALSE.
+ $matches = $this->find('NO_SUCH_ELEMENT');
+ $this->assertEquals(0, count($matches));
+
+ // Test without namespace
+ $matches = $this->find('root');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('crowded');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('outside');
+ $this->assertEquals(3, count($matches));
+
+ // Check nested elements.
+ $matches = $this->find('a');
+ $this->assertEquals(3, count($matches));
+
+ // Test wildcard.
+ $traverser = $this->traverser();
+ $matches = $traverser->find('*')->matches();
+ $actual= $traverser->getDocument()->getElementsByTagName('*');
+ $this->assertEquals($actual->length, count($matches));
+
+
+ // Test with namespace
+ //$this->markTestIncomplete();
+ $matches = $this->find('ns_test');
+ $this->assertEquals(3, count($matches));
+
+ $matches = $this->find('test|ns_test');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('test|ns_test>ns_attr');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('*|ns_test');
+ $this->assertEquals(3, count($matches));
+
+ $matches = $this->find('test|*');
+ $this->assertEquals(1, count($matches));
+
+ // Test where namespace is declared on the element.
+ $matches = $this->find('newns|my_element');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('test|ns_test>newns|my_element');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('test|*>newns|my_element');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('*|ns_test>newns|my_element');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('*|*>newns|my_element');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('*>newns|my_element');
+ $this->assertEquals(1, count($matches));
+ }
+
+ public function testMatchAttributes() {
+
+ $matches = $this->find('crowded[attr1]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('crowded[attr1=one]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('crowded[attr2^=tw]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('classtest[class~=two]');
+ $this->assertEquals(1, count($matches));
+ $matches = $this->find('classtest[class~=one]');
+ $this->assertEquals(1, count($matches));
+ $matches = $this->find('classtest[class~=seven]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('crowded[attr0]');
+ $this->assertEquals(0, count($matches));
+
+ $matches = $this->find('[level=1]');
+ $this->assertEquals(3, count($matches));
+
+ $matches = $this->find('[attr1]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('[test|myattr]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('[test|myattr=foo]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('[*|myattr=foo]');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('[|myattr=foo]');
+ $this->assertEquals(0, count($matches));
+
+ $matches = $this->find('[|level=1]');
+ $this->assertEquals(3, count($matches));
+
+ $matches = $this->find('[*|level=1]');
+ $this->assertEquals(4, count($matches));
+
+ // Test namespace on attr where namespace
+ // is declared on that element
+ $matches = $this->find('[nuther|ping]');
+ $this->assertEquals(1, count($matches));
+
+ // Test multiple namespaces on an element.
+ $matches = $this->find('[*|ping=3]');
+ $this->assertEquals(1, count($matches));
+
+ // Test multiple namespaces on an element.
+ $matches = $this->find('[*|ping]');
+ $this->assertEquals(1, count($matches));
+ }
+
+ public function testMatchId() {
+ $matches = $this->find('idtest#idtest-one');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('#idtest-one');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('outter#fake');
+ $this->assertEquals(0, count($matches));
+
+ $matches = $this->find('#fake');
+ $this->assertEquals(0, count($matches));
+ }
+
+ public function testMatchClasses() {
+ // Basic test.
+ $matches = $this->find('a.a1');
+ $this->assertEquals(1, count($matches));
+
+ // Count multiple.
+ $matches = $this->find('.first');
+ $this->assertEquals(2, count($matches));
+
+ // Grab one in the middle of a list.
+ $matches = $this->find('.four');
+ $this->assertEquals(1, count($matches));
+
+ // One element with two classes.
+ $matches = $this->find('.three.four');
+ $this->assertEquals(1, count($matches));
+ }
+
+ public function testMatchPseudoClasses() {
+
+ $matches = $this->find('ul>li:first');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('ul>li:not(.first)');
+ $this->assertEquals(5, count($matches));
+
+ }
+
+ public function testMatchPseudoElements() {
+ $matches = $this->find('p::first-line');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('p::first-letter');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('p::before');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('p::after');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('bottom::after');
+ $this->assertEquals(0, count($matches));
+ }
+
+ public function testCombineAdjacent() {
+ // Simple test
+ $matches = $this->find('idtest + p');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('p', $m->tagName);
+ }
+
+ // Test ignoring PCDATA
+ $matches = $this->find('p + one');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('one', $m->tagName);
+ }
+
+ // Test that non-adjacent elements don't match.
+ $matches = $this->find('idtest + one');
+ foreach ($matches as $m) {
+ $this->assertEquals('one', $m->tagName);
+ }
+ $this->assertEquals(0, count($matches), 'Non-adjacents should not match.');
+
+ // Test that elements BEFORE don't match
+ $matches = $this->find('one + p');
+ foreach ($matches as $m) {
+ $this->assertEquals('one', $m->tagName);
+ }
+ $this->assertEquals(0, count($matches), 'Match only if b is after a');
+ }
+
+ public function testCombineSibling() {
+ // Canary:
+ $matches = $this->find('one ~ two');
+ $this->assertEquals(0, count($matches));
+
+ // Canary 2:
+ $matches = $this->find('NO_SUCH_ELEMENT ~ two');
+ $this->assertEquals(0, count($matches));
+
+ // Simple test
+ $matches = $this->find('idtest ~ p');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('p', $m->tagName);
+ }
+
+ // Simple test
+ $matches = $this->find('outside ~ p');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('p', $m->tagName);
+ }
+
+ // Matches only go left, not right.
+ $matches = $this->find('p ~ outside');
+ $this->assertEquals(0, count($matches));
+ }
+ public function testCombineDirectDescendant() {
+ // Canary:
+ $matches = $this->find('one > four');
+ $this->assertEquals(0, count($matches));
+
+ $matches = $this->find('two>three');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('three', $m->tagName);
+ }
+
+ $matches = $this->find('one > two > three');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('three', $m->tagName);
+ }
+
+ $matches = $this->find('a>a>a');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('a>a');
+ $this->assertEquals(2, count($matches));
+ }
+ public function testCombineAnyDescendant() {
+ // Canary
+ $matches = $this->find('four one');
+ $this->assertEquals(0, count($matches));
+
+ $matches = $this->find('one two');
+ $this->assertEquals(1, count($matches));
+ foreach ($matches as $m) {
+ $this->assertEquals('two', $m->tagName);
+ }
+
+ $matches = $this->find('one four');
+ $this->assertEquals(1, count($matches));
+
+ $matches = $this->find('a a');
+ $this->assertEquals(2, count($matches));
+
+ $matches = $this->find('root two four');
+ $this->assertEquals(1, count($matches));
+ }
+ public function testMultipleSelectors() {
+ // fprintf(STDOUT, "=========TEST=========\n\n");
+ $matches = $this->find('one, two');
+ $this->assertEquals(2, count($matches));
+ }
+
+}
+
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/ParserTest.php b/lib/querypath/test/Tests/QueryPath/CSS/ParserTest.php
new file mode 100644
index 0000000..981ddd1
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/ParserTest.php
@@ -0,0 +1,520 @@
+<?php
+/**
+ * @file
+ * CSS Event handling tests
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\Token;
+use \QueryPath\CSS\QueryPathEventHandler;
+use \QueryPath\CSS\Parser;
+use \QueryPath\CSS\EventHandler;
+
+/**
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class ParserTest extends TestCase {
+
+ private function getMockHandler($method) {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array($method));
+ $mock->expects($this->once())
+ ->method($method)
+ ->with($this->equalTo('mytest'));
+ return $mock;
+ }
+
+ public function testElementID() {
+ $mock = $this->getMockHandler('elementID');
+ $parser = new Parser('#mytest', $mock);
+ $parser->parse();
+
+ }
+
+ public function testElement() {
+
+ // Without namespace
+ $mock = $this->getMockHandler('element');
+ $parser = new Parser('mytest', $mock);
+ $parser->parse();
+
+ // With empty namespace
+ $mock = $this->getMockHandler('element');
+ $parser = new Parser('|mytest', $mock);
+ $parser->parse();
+ }
+
+ public function testElementNS() {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('elementNS'));
+ $mock->expects($this->once())
+ ->method('elementNS')
+ ->with($this->equalTo('mytest'), $this->equalTo('myns'));
+
+ $parser = new Parser('myns|mytest', $mock);
+ $parser->parse();
+
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('elementNS'));
+ $mock->expects($this->once())
+ ->method('elementNS')
+ ->with($this->equalTo('mytest'), $this->equalTo('*'));
+
+ $parser = new Parser('*|mytest', $mock);
+ $parser->parse();
+
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('anyElementInNS'));
+ $mock->expects($this->once())
+ ->method('anyElementInNS')
+ ->with($this->equalTo('*'));
+
+ $parser = new Parser('*|*', $mock);
+ $parser->parse();
+ }
+
+ public function testAnyElement() {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('anyElement', 'element'));
+ $mock->expects($this->once())
+ ->method('anyElement');
+
+ $parser = new Parser('*', $mock);
+ $parser->parse();
+ }
+
+ public function testAnyElementInNS() {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('anyElementInNS', 'element'));
+ $mock->expects($this->once())
+ ->method('anyElementInNS')
+ ->with($this->equalTo('myns'));
+
+ $parser = new Parser('myns|*', $mock);
+ $parser->parse();
+ }
+
+ public function testElementClass() {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('elementClass'));
+ $mock->expects($this->once())
+ ->method('elementClass')
+ ->with($this->equalTo('myclass'));
+
+ $parser = new Parser('.myclass', $mock);
+ $parser->parse();
+ }
+
+ public function testPseudoClass() {
+
+ // Test empty pseudoclass
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('pseudoClass'));
+ $mock->expects($this->once())
+ ->method('pseudoClass')
+ ->with($this->equalTo('mypclass'));
+
+ $parser = new Parser('myele:mypclass', $mock);
+ $parser->parse();
+
+ // Test pseudoclass with value
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('pseudoClass'));
+ $mock->expects($this->once())
+ ->method('pseudoClass')
+ ->with($this->equalTo('mypclass'), $this->equalTo('myval'));
+
+ $parser = new Parser('myele:mypclass(myval)', $mock);
+ $parser->parse();
+
+ // Test pseudclass with pseudoclass:
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('pseudoClass'));
+ $mock->expects($this->once())
+ ->method('pseudoClass')
+ ->with($this->equalTo('mypclass'), $this->equalTo(':anotherPseudo'));
+
+ $parser = new Parser('myele:mypclass(:anotherPseudo)', $mock);
+ $parser->parse();
+
+ }
+
+ public function testPseudoElement() {
+ // Test pseudo-element
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('pseudoElement'));
+ $mock->expects($this->once())
+ ->method('pseudoElement')
+ ->with($this->equalTo('mypele'));
+
+ $parser = new Parser('myele::mypele', $mock);
+ $parser->parse();
+ }
+
+ public function testDirectDescendant() {
+ // Test direct Descendant
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('directDescendant'));
+ $mock->expects($this->once())
+ ->method('directDescendant');
+
+ $parser = new Parser('ele1 > ele2', $mock);
+ $parser->parse();
+
+ }
+
+ public function testAnyDescendant() {
+ // Test direct Descendant
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('anyDescendant'));
+ $mock->expects($this->once())
+ ->method('anyDescendant');
+
+ $parser = new Parser('ele1 .class', $mock);
+ $parser->parse();
+
+ }
+
+ public function testAdjacent() {
+ // Test sibling
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('adjacent'));
+ $mock->expects($this->once())
+ ->method('adjacent');
+
+ $parser = new Parser('ele1 + ele2', $mock);
+ $parser->parse();
+ }
+
+ public function testSibling() {
+ // Test adjacent
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('sibling'));
+ $mock->expects($this->once())
+ ->method('sibling');
+
+ $parser = new Parser('ele1 ~ ele2', $mock);
+ $parser->parse();
+ }
+
+ public function testAnotherSelector() {
+ // Test adjacent
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('anotherSelector'));
+ $mock->expects($this->once())
+ ->method('anotherSelector');
+
+ $parser = new Parser('ele1 , ele2', $mock);
+ $parser->parse();
+ }
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testIllegalAttribute() {
+
+ // Note that this is designed to test throwError() as well as
+ // bad selector handling.
+
+ $parser = new Parser('[test=~far]', new TestEventHandler());
+ try {
+ $parser->parse();
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ public function testAttribute() {
+ $selectors = array(
+ 'element[attr]' => 'attr',
+ '*[attr]' => 'attr',
+ 'element[attr]:class' => 'attr',
+ 'element[attr2]' => 'attr2', // Issue #
+ );
+ foreach ($selectors as $filter => $expected) {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('attribute'));
+ $mock->expects($this->once())
+ ->method('attribute')
+ ->with($this->equalTo($expected));
+
+ $parser = new Parser($filter, $mock);
+ $parser->parse();
+ }
+
+ $selectors = array(
+ '*[attr="value"]' => array('attr','value',EventHandler::isExactly),
+ '*[attr^="value"]' => array('attr','value',EventHandler::beginsWith),
+ '*[attr$="value"]' => array('attr','value',EventHandler::endsWith),
+ '*[attr*="value"]' => array('attr','value',EventHandler::containsInString),
+ '*[attr~="value"]' => array('attr','value',EventHandler::containsWithSpace),
+ '*[attr|="value"]' => array('attr','value',EventHandler::containsWithHyphen),
+
+ // This should act like [attr="value"]
+ '*[|attr="value"]' => array('attr', 'value', EventHandler::isExactly),
+
+ // This behavior is displayed in the spec, but not accounted for in the
+ // grammar:
+ '*[attr=value]' => array('attr','value',EventHandler::isExactly),
+
+ // Should be able to escape chars using backslash.
+ '*[attr="\.value"]' => array('attr','.value',EventHandler::isExactly),
+ '*[attr="\.\]\]\]"]' => array('attr','.]]]',EventHandler::isExactly),
+
+ // Backslash-backslash should resolve to single backslash.
+ '*[attr="\\\c"]' => array('attr','\\c',EventHandler::isExactly),
+
+ // Should return an empty value. It seems, though, that a value should be
+ // passed here.
+ '*[attr=""]' => array('attr','',EventHandler::isExactly),
+ );
+ foreach ($selectors as $filter => $expected) {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('attribute'));
+ $mock->expects($this->once())
+ ->method('attribute')
+ ->with($this->equalTo($expected[0]), $this->equalTo($expected[1]), $this->equalTo($expected[2]));
+
+ $parser = new Parser($filter, $mock);
+ $parser->parse();
+ }
+ }
+
+ public function testAttributeNS() {
+ $selectors = array(
+ '*[ns|attr="value"]' => array('attr', 'ns', 'value',EventHandler::isExactly),
+ '*[*|attr^="value"]' => array('attr', '*', 'value',EventHandler::beginsWith),
+ '*[*|attr|="value"]' => array('attr', '*', 'value',EventHandler::containsWithHyphen),
+ );
+
+ foreach ($selectors as $filter => $expected) {
+ $mock = $this->getMock('\QueryPath\Tests\TestEventHandler', array('attributeNS'));
+ $mock->expects($this->once())
+ ->method('attributeNS')
+ ->with($this->equalTo($expected[0]), $this->equalTo($expected[1]), $this->equalTo($expected[2]), $this->equalTo($expected[3]));
+
+ $parser = new Parser($filter, $mock);
+ $parser->parse();
+ }
+ }
+
+ // Test things that should break...
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testIllegalCombinators1() {
+ $handler = new TestEventHandler();
+ $parser = new Parser('ele1 > > ele2', $handler);
+ $parser->parse();
+ }
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testIllegalCombinators2() {
+ $handler = new TestEventHandler();
+ $parser = new Parser('ele1+ ,ele2', $handler);
+ $parser->parse();
+ }
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testIllegalID() {
+ $handler = new TestEventHandler();
+ $parser = new Parser('##ID', $handler);
+ $parser->parse();
+ }
+
+ // Test combinations
+
+ public function testElementNSClassAndAttribute() {
+
+ $expect = array(
+ new TestEvent(TestEvent::elementNS, 'element', 'ns'),
+ new TestEvent(TestEvent::elementClass, 'class'),
+ new TestEvent(TestEvent::attribute, 'name', 'value', EventHandler::isExactly),
+ );
+ $selector = 'ns|element.class[name="value"]';
+
+ $handler = new TestEventHandler();
+ $handler->expects($expect);
+ $parser = new Parser($selector, $handler);
+ $parser->parse();
+ $this->assertTrue($handler->success());
+
+ // Again, with spaces this time:
+ $selector = ' ns|element. class[ name = "value" ]';
+
+ $handler = new TestEventHandler();
+ $handler->expects($expect);
+ $parser = new Parser($selector, $handler);
+ $parser->parse();
+
+ //$handler->dumpStack();
+ $this->assertTrue($handler->success());
+ }
+
+ public function testAllCombo() {
+
+ $selector = '*|ele1 > ele2.class1 + ns1|ele3.class2[attr=simple] ~
+ .class2[attr2~="longer string of text."]:pseudoClass(value)
+ .class3::pseudoElement';
+ $expect = array(
+ new TestEvent(TestEvent::elementNS, 'ele1', '*'),
+ new TestEvent(TestEvent::directDescendant),
+ new TestEvent(TestEvent::element, 'ele2'),
+ new TestEvent(TestEvent::elementClass, 'class1'),
+ new TestEvent(TestEvent::adjacent),
+ new TestEvent(TestEvent::elementNS, 'ele3', 'ns1'),
+ new TestEvent(TestEvent::elementClass, 'class2'),
+ new TestEvent(TestEvent::attribute, 'attr', 'simple', EventHandler::isExactly),
+ new TestEvent(TestEvent::sibling),
+ new TestEvent(TestEvent::elementClass, 'class2'),
+ new TestEvent(TestEvent::attribute, 'attr2', 'longer string of text.', EventHandler::containsWithSpace),
+ new TestEvent(TestEvent::pseudoClass, 'pseudoClass', 'value'),
+ new TestEvent(TestEvent::anyDescendant),
+ new TestEvent(TestEvent::elementClass, 'class3'),
+ new TestEvent(TestEvent::pseudoElement, 'pseudoElement'),
+ );
+
+
+ $handler = new TestEventHandler();
+ $handler->expects($expect);
+ $parser = new Parser($selector, $handler);
+ $parser->parse();
+
+ //$handler->dumpStack();
+
+ $this->assertTrue($handler->success());
+
+ /*
+ // Again, with spaces this time:
+ $selector = ' *|ele1 > ele2. class1 + ns1|ele3. class2[ attr=simple] ~ .class2[attr2 ~= "longer string of text."]:pseudoClass(value) .class3::pseudoElement';
+
+ $handler = new TestEventHandler();
+ $handler->expects($expect);
+ $parser = new Parser($selector, $handler);
+ $parser->parse();
+
+ $handler->dumpStack();
+ $this->assertTrue($handler->success());
+ */
+ }
+}
+
+/**
+ * Testing harness for the EventHandler.
+ *
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class TestEventHandler implements EventHandler {
+ var $stack = NULL;
+ var $expect = array();
+
+ public function __construct() {
+ $this->stack = array();
+ }
+
+ public function getStack() {
+ return $this->stack;
+ }
+
+ public function dumpStack() {
+ print "\nExpected:\n";
+ $format = "Element %d: %s\n";
+ foreach ($this->expect as $item) {
+ printf($format, $item->eventType(), implode(',', $item->params()));
+ }
+
+ print "Got:\n";
+ foreach($this->stack as $item){
+ printf($format, $item->eventType(), implode(',', $item->params()));
+ }
+ }
+
+ public function expects($stack) {
+ $this->expect = $stack;
+ }
+
+ public function success() {
+ return ($this->expect == $this->stack);
+ }
+
+ public function elementID($id) {
+ $this->stack[] = new TestEvent(TestEvent::elementID, $id);
+ }
+ public function element($name) {
+ $this->stack[] = new TestEvent(TestEvent::element, $name);
+ }
+ public function elementNS($name, $namespace = NULL){
+ $this->stack[] = new TestEvent(TestEvent::elementNS, $name, $namespace);
+ }
+ public function anyElement(){
+ $this->stack[] = new TestEvent(TestEvent::anyElement);
+ }
+ public function anyElementInNS($ns){
+ $this->stack[] = new TestEvent(TestEvent::anyElementInNS, $ns);
+ }
+ public function elementClass($name){
+ $this->stack[] = new TestEvent(TestEvent::elementClass, $name);
+ }
+ public function attribute($name, $value = NULL, $operation = EventHandler::isExactly){
+ $this->stack[] = new TestEvent(TestEvent::attribute, $name, $value, $operation);
+ }
+ public function attributeNS($name, $ns, $value = NULL, $operation = EventHandler::isExactly){
+ $this->stack[] = new TestEvent(TestEvent::attributeNS, $name, $ns, $value, $operation);
+ }
+ public function pseudoClass($name, $value = NULL){
+ $this->stack[] = new TestEvent(TestEvent::pseudoClass, $name, $value);
+ }
+ public function pseudoElement($name){
+ $this->stack[] = new TestEvent(TestEvent::pseudoElement, $name);
+ }
+ public function directDescendant(){
+ $this->stack[] = new TestEvent(TestEvent::directDescendant);
+ }
+ public function anyDescendant() {
+ $this->stack[] = new TestEvent(TestEvent::anyDescendant);
+ }
+ public function adjacent(){
+ $this->stack[] = new TestEvent(TestEvent::adjacent);
+ }
+ public function anotherSelector(){
+ $this->stack[] = new TestEvent(TestEvent::anotherSelector);
+ }
+ public function sibling(){
+ $this->stack[] = new TestEvent(TestEvent::sibling);
+ }
+}
+
+/**
+ * Simple utility object for use with the TestEventHandler.
+ *
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class TestEvent {
+ const elementID = 0;
+ const element = 1;
+ const elementNS = 2;
+ const anyElement = 3;
+ const elementClass = 4;
+ const attribute = 5;
+ const attributeNS = 6;
+ const pseudoClass = 7;
+ const pseudoElement = 8;
+ const directDescendant = 9;
+ const adjacent = 10;
+ const anotherSelector = 11;
+ const sibling = 12;
+ const anyElementInNS = 13;
+ const anyDescendant = 14;
+
+ var $type = NULL;
+ var $params = NULL;
+
+ public function __construct($event_type) {
+ $this->type = $event_type;
+ $args = func_get_args();
+ array_shift($args);
+ $this->params = $args;
+ }
+
+ public function eventType() {
+ return $this->type;
+ }
+
+ public function params() {
+ return $this->params;
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/PseudoClassTest.php b/lib/querypath/test/Tests/QueryPath/CSS/PseudoClassTest.php
new file mode 100644
index 0000000..6798b1c
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/PseudoClassTest.php
@@ -0,0 +1,828 @@
+<?php
+/**
+ * @file
+ * CSS Event handling tests for PseudoClasses.
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\DOMTraverser\PseudoClass;
+
+/**
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class PseudoClassTest extends TestCase {
+
+ protected function doc($string, $tagname) {
+
+ $doc = new \DOMDocument('1.0');
+ $doc->loadXML($string);
+
+ $found = $doc->getElementsByTagName($tagname)->item(0);
+
+ return array($found, $doc->documentElement);
+
+ }
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testUnknownPseudoClass() {
+ $xml = '<?xml version="1.0"?><root><foo>test</foo></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ps->elementMatches('TotallyFake', $ele, $root);
+ }
+
+ public function testLang() {
+ $xml = '<?xml version="1.0"?><root><foo lang="en-US">test</foo></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('lang', $ele, $root, 'en-US');
+ $this->assertTrue($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'en');
+ $this->assertTrue($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'fr-FR');
+ $this->assertFalse($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'fr');
+ $this->assertFalse($ret);
+
+
+ // Check on ele that doesn't have lang.
+ $ret = $ps->elementMatches('lang', $root, $root, 'fr');
+ $this->assertFalse($ret);
+
+ }
+
+ public function testLangNS() {
+ $xml = '<?xml version="1.0"?><root><foo xml:lang="en-US">test</foo></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('lang', $ele, $root, 'en-US');
+ $this->assertTrue($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'en');
+ $this->assertTrue($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'fr-FR');
+ $this->assertFalse($ret);
+ $ret = $ps->elementMatches('lang', $ele, $root, 'fr');
+ $this->assertFalse($ret);
+
+
+ // Check on ele that doesn't have lang.
+ $ret = $ps->elementMatches('lang', $root, $root, 'fr');
+ $this->assertFalse($ret);
+ }
+
+ public function testFormType() {
+ $xml = '<?xml version="1.0"?><root><foo type="submit">test</foo></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('submit', $ele, $root);
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('reset', $ele, $root);
+ $this->assertFalse($ret);
+
+ }
+
+ public function testHasAttribute() {
+ $xml = '<?xml version="1.0"?><root><foo enabled="enabled">test</foo></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('enabled', $ele, $root);
+ $this->assertTrue($ret);
+ $ret = $ps->elementMatches('disabled', $ele, $root);
+ $this->assertFalse($ret);
+ }
+
+ public function testHeader() {
+ $xml = '<?xml version="1.0"?><root><h1>TEST</h1><H6></H6><hi/><h12/><h1i/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'h1');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('header', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'H6');
+ $ret = $ps->elementMatches('header', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'hi');
+ $ret = $ps->elementMatches('header', $ele, $root);
+ $this->assertFalse($ret);
+ list($ele, $root) = $this->doc($xml, 'h1i');
+ $ret = $ps->elementMatches('header', $ele, $root);
+ $this->assertFalse($ret);
+ list($ele, $root) = $this->doc($xml, 'h12');
+ $ret = $ps->elementMatches('header', $ele, $root);
+ $this->assertFalse($ret);
+ }
+
+ public function testContains(){
+ $xml = '<?xml version="1.0"?><root><h>This is a test of :contains.</h></root>';
+
+ list($ele, $root) = $this->doc($xml, 'h');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('contains', $ele, $root, 'test');
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('contains', $ele, $root, 'is a test');
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('contains', $ele, $root, 'This is a test of :contains.');
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('contains', $ele, $root, 'Agent P, here is your mission.');
+ $this->assertFalse($ret);
+
+ $ret = $ps->elementMatches('contains', $ele, $root, "'Agent P, here is your mission.'");
+ $this->assertFalse($ret);
+
+ }
+ public function testContainsExactly() {
+ $xml = '<?xml version="1.0"?><root><h>This is a test of :contains-exactly.</h></root>';
+
+ list($ele, $root) = $this->doc($xml, 'h');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('contains-exactly', $ele, $root, 'test');
+ $this->assertFalse($ret);
+
+ $ret = $ps->elementMatches('contains-exactly', $ele, $root, 'This is a test of :contains-exactly.');
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('contains-exactly', $ele, $root, 'Agent P, here is your mission.');
+ $this->assertFalse($ret);
+
+ $ret = $ps->elementMatches('contains-exactly', $ele, $root, '"Agent P, here is your mission."');
+ $this->assertFalse($ret);
+ }
+ public function testHas() {
+ $xml = '<?xml version="1.0"?><root><button disabled="disabled"/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'button');
+ $ps = new PseudoClass();
+
+ // Example from CSS 4 spec
+ $ret = $ps->elementMatches('matches', $ele, $root, '[disabled]');
+ $this->assertTrue($ret);
+
+ // Regression for Issue #84:
+ $xml = '<?xml version="1.0"?><root><a/><a/><a src="/foo/bar"/><b src="http://example.com/foo/bar"/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $ele->childNodes;
+ $ps = new PseudoClass();
+
+ $i = 0;
+ foreach ($nl as $n) {
+ $ret = $ps->elementMatches('matches', $n, $root, '[src^="/foo/"]');
+ if ($ret) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(1, $i);
+ }
+ public function testParent() {
+ $ps = new PseudoClass();
+
+ $xml = '<?xml version="1.0"?><root><p/></root>';
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('parent', $ele, $root);
+ $this->assertFalse($ret);
+
+ $xml = '<?xml version="1.0"?><root><p></p>></root>';
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('parent', $ele, $root);
+ $this->assertFalse($ret);
+
+ $xml = '<?xml version="1.0"?><root><p>TEST</p></root>';
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('parent', $ele, $root);
+ $this->assertTrue($ret);
+
+ $xml = '<?xml version="1.0"?><root><p><q/></p></root>';
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('parent', $ele, $root);
+ $this->assertTrue($ret);
+
+ }
+ public function testFirst() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><p><q/></p><a></a><b/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'q');
+ $ret = $ps->elementMatches('first', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('first', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'b');
+ $ret = $ps->elementMatches('first', $ele, $root);
+ $this->assertFalse($ret);
+
+ }
+ public function testLast() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><p><q/></p><a></a><b/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'q');
+ $ret = $ps->elementMatches('last', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'p');
+ $ret = $ps->elementMatches('last', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'b');
+ $ret = $ps->elementMatches('last', $ele, $root);
+ $this->assertTrue($ret);
+ }
+ public function testNot() {
+ $xml = '<?xml version="1.0"?><root><button/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'button');
+ $ps = new PseudoClass();
+
+ // Example from CSS 4 spec
+ $ret = $ps->elementMatches('not', $ele, $root, '[disabled]');
+ $this->assertTrue($ret);
+
+ $xml = '<?xml version="1.0"?><root><b/><b/><c/><b/></root>';
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $ele->childNodes;
+
+ $i = 0;
+ foreach ($nl as $n) {
+ $ret = $ps->elementMatches('not', $n, $root, 'c');
+ if ($ret) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(3, $i);
+
+ }
+ public function testEmpty() {
+ $xml = '<?xml version="1.0"?><root><foo lang="en-US">test</foo><bar/><baz></baz></root>';
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ps = new PseudoClass();
+
+ $ret = $ps->elementMatches('empty', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'bar');
+ $ret = $ps->elementMatches('empty', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'baz');
+ $ret = $ps->elementMatches('empty', $ele, $root);
+ $this->assertTrue($ret);
+ }
+ public function testOnlyChild() {
+ $xml = '<?xml version="1.0"?><root><foo>test<a/></foo><b><c/></b></root>';
+ $ps = new PseudoClass();
+
+ list($ele, $root) = $this->doc($xml, 'foo');
+ $ret = $ps->elementMatches('only-child', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('only-child', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'c');
+ $ret = $ps->elementMatches('only-child', $ele, $root);
+ $this->assertTrue($ret);
+ }
+ public function testLastOfType() {
+ $xml = '<?xml version="1.0"?><root><one><a/><b/><c/></one><two><d/><d/><b/></two></root>';
+ $ps = new PseudoClass();
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('last-of-type', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $nl = $root->getElementsByTagName('d');
+
+ $ret = $ps->elementMatches('last-of-type', $nl->item(0), $root);
+ $this->assertFalse($ret);
+
+ $ret = $ps->elementMatches('last-of-type', $nl->item(1), $root);
+ $this->assertTrue($ret);
+ }
+ public function testFirstOftype() {
+ $xml = '<?xml version="1.0"?><root><one><a/><b/><c/></one><two><d/><d/><b/></two></root>';
+ $ps = new PseudoClass();
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('first-of-type', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $nl = $root->getElementsByTagName('d');
+
+ $ret = $ps->elementMatches('first-of-type', $nl->item(0), $root);
+ $this->assertTrue($ret);
+
+ $ret = $ps->elementMatches('first-of-type', $nl->item(1), $root);
+ $this->assertFalse($ret);
+ }
+ public function testOnlyOfType() {
+ $xml = '<?xml version="1.0"?><root><one><a/><b/><c/></one><two><d/><d/><b/></two></root>';
+ $ps = new PseudoClass();
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('only-of-type', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $nl = $root->getElementsByTagName('d');
+
+ $ret = $ps->elementMatches('only-of-type', $nl->item(0), $root);
+ $this->assertFalse($ret);
+
+ $ret = $ps->elementMatches('only-of-type', $nl->item(1), $root);
+ $this->assertFalse($ret);
+ }
+ public function testNthLastChild() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><b/><c/><d/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // 2n + 1 -- Every odd row, from the last element.
+ $i = 0;
+ $expects = array('b', 'd');
+ $j = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-last-child', $n, $root, '2n+1');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, sprintf('Expected b or d, got %s in slot %s', $name, ++$j));
+ }
+ }
+ $this->assertEquals(10, $i, '2n+1 is ten items.');
+
+ // 3 (0n+3) -- third from the end (b).
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-last-child', $n, $root, '3');
+ if ($res) {
+ ++$i;
+ $this->assertEquals('b', $n->tagName);
+ }
+ }
+ $this->assertEquals(1, $i);
+
+ // -n+3: Last three elements.
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-last-child', $n, $root, '-n+3');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(3, $i);
+ }
+ public function testNthChild() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><b/><c/><d/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // 2n + 1 -- Every odd row.
+ $i = 0;
+ $expects = array('a', 'c');
+ $j = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '2n+1');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, sprintf('Expected b or d, got %s in slot %s', $name, ++$j));
+ }
+ }
+ $this->assertEquals(10, $i, '2n+1 is ten items.');
+
+ // Odd
+ $i = 0;
+ $expects = array('a', 'c');
+ $j = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, 'odd');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, sprintf('Expected b or d, got %s in slot %s', $name, ++$j));
+ }
+ }
+ $this->assertEquals(10, $i, '2n+1 is ten items.');
+
+ // 2n + 0 -- every even row
+ $i = 0;
+ $expects = array('b', 'd');
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '2n');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, 'Expected a or c, got ' . $name);
+ }
+ }
+ $this->assertEquals(10, $i, '2n+0 is ten items.');
+
+ // Even (2n)
+ $i = 0;
+ $expects = array('b', 'd');
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, 'even');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, 'Expected a or c, got ' . $name);
+ }
+ }
+ $this->assertEquals(10, $i, ' even is ten items.');
+
+ // 4n - 1 == 4n + 3
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '4n-1');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertEquals('c', $name, 'Expected c, got ' . $name);
+ }
+ }
+ $this->assertEquals(5, $i);
+
+ // 6n - 1
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '6n-1');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(3, $i);
+
+ // 6n + 1
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '6n+1');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(4, $i);
+
+ // 26n - 1
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '26n-1');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(0, $i);
+
+ // 0n + 0 -- spec says this is always FALSE.
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '0n+0');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(0, $i);
+
+ // 3 (0n+3)
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '3');
+ if ($res) {
+ ++$i;
+ $this->assertEquals('c', $n->tagName);
+ }
+ }
+ $this->assertEquals(1, $i);
+
+ // -n+3: First three elements.
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '-n+3');
+ if ($res) {
+ ++$i;
+ //$this->assertEquals('c', $n->tagName);
+ }
+ }
+ $this->assertEquals(3, $i);
+
+ // BROKEN RULES -- these should always fail to match.
+
+ // 6n + 7;
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-child', $n, $root, '6n+7');
+ if ($res) {
+ ++$i;
+ }
+ }
+ $this->assertEquals(0, $i);
+
+ }
+ public function testEven() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><b/><c/><d/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ $i = 0;
+ $expects = array('b', 'd');
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('even', $n, $root);
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, 'Expected a or c, got ' . $name);
+ }
+ }
+ $this->assertEquals(10, $i, ' even is ten items.');
+ }
+ public function testOdd() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><b/><c/><d/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Odd
+ $i = 0;
+ $expects = array('a', 'c');
+ $j = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('odd', $n, $root);
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertContains($name, $expects, sprintf('Expected b or d, got %s in slot %s', $name, ++$j));
+ }
+ }
+ $this->assertEquals(10, $i, 'Ten odds.');
+ }
+ public function testNthOfTypeChild() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><a/><a/><a/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Odd
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-of-type', $n, $root, '2n+1');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertEquals('a', $name);
+ }
+ }
+ $this->assertEquals(10, $i, 'Ten odds.');
+
+ // Fun with ambiguous pseudoclasses:
+ // This emulates the selector 'root > :nth-of-type(2n+1)'
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= '<a/><b/><c/><a/><a/><a/>';
+ $xml .= '</root>';
+
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Odd
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth-of-type', $n, $root, '2n+1');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ }
+ }
+ // THis should be: 2 x a + 1 x b + 1 x c = 4
+ $this->assertEquals(4, $i, 'Four odds.');
+ }
+ public function testNthLastOfTypeChild() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= '<a/><a/><OOPS/><a/><a/>';
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Third from beginning is second from last.
+ $third = $nl->item(2);
+ $res = $ps->elementMatches('nth-last-of-type', $third, $root, '3');
+ $this->assertFalse($res);
+
+ $first = $nl->item(0);
+ $res = $ps->elementMatches('nth-last-of-type', $first, $root, '3');
+ $this->assertFalse($res);
+
+ $last = $nl->item(3);
+ $res = $ps->elementMatches('nth-last-of-type', $last, $root, '3');
+ $this->assertFalse($res);
+
+ // Second from start is 3rd from last
+ $second = $nl->item(1);
+ $res = $ps->elementMatches('nth-last-of-type', $second, $root, '3');
+ $this->assertTrue($res);
+
+ }
+ public function testLink() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><a href="foo"><b hreff="bar">test</b></a><c/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'c');
+ $ret = $ps->elementMatches('link', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('link', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'b');
+ $ret = $ps->elementMatches('link', $ele, $root);
+ $this->assertFalse($ret);
+ }
+ public function testRoot() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><p><q/></p><a></a><b/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'q');
+ $ret = $ps->elementMatches('root', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'root');
+ $ret = $ps->elementMatches('root', $ele, $root);
+ $this->assertTrue($ret);
+ }
+ /* Deprecated and removed.
+ public function testXRoot() {
+ }
+ public function testXReset() {
+ }
+ */
+ public function testLt() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><a/><a/><a/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Odd
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('lt', $n, $root, '15');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ }
+ }
+ $this->assertEquals(15, $i, 'Less than or equal to 15.');
+ }
+ public function testGt() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><a/><a/><a/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ // Odd
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('gt', $n, $root, '15');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ }
+ }
+ $this->assertEquals(5, $i, 'Greater than the 15th element.');
+ }
+ public function testEq() {
+ $xml = '<?xml version="1.0"?><root>';
+ $xml .= str_repeat('<a/><b/><c/><a/>', 5);
+ $xml .= '</root>';
+
+ $ps = new PseudoClass();
+ list($ele, $root) = $this->doc($xml, 'root');
+ $nl = $root->childNodes;
+
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('eq', $n, $root, '15');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertEquals('c', $name);
+ }
+ }
+ $this->assertEquals(1, $i, 'The 15th element.');
+
+ $i = 0;
+ foreach ($nl as $n) {
+ $res = $ps->elementMatches('nth', $n, $root, '15');
+ if ($res) {
+ ++$i;
+ $name = $n->tagName;
+ $this->assertEquals('c', $name);
+ }
+ }
+ $this->assertEquals(1, $i, 'The 15th element.');
+ }
+
+ public function testAnyLink() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><a href="foo"><b hreff="bar">test</b></a><c/><d src="foo"/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'c');
+ $ret = $ps->elementMatches('any-link', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('any-link', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'd');
+ $ret = $ps->elementMatches('any-link', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'b');
+ $ret = $ps->elementMatches('any-link', $ele, $root);
+ $this->assertFalse($ret);
+ }
+ public function testLocalLink() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><a href="foo"><b href="http://example.com/bar">test</b></a><c/><d href="file://foo"/></root>';
+
+ list($ele, $root) = $this->doc($xml, 'c');
+ $ret = $ps->elementMatches('local-link', $ele, $root);
+ $this->assertFalse($ret);
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $ret = $ps->elementMatches('local-link', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'd');
+ $ret = $ps->elementMatches('local-link', $ele, $root);
+ $this->assertTrue($ret);
+
+ list($ele, $root) = $this->doc($xml, 'b');
+ $ret = $ps->elementMatches('local-link', $ele, $root);
+ $this->assertFalse($ret);
+ }
+ public function testScope() {
+ $ps = new PseudoClass();
+ $xml = '<?xml version="1.0"?><root><a href="foo"><b>test</b></a></root>';
+
+ list($ele, $root) = $this->doc($xml, 'a');
+ $node = $ele->childNodes->item(0);
+ $ret = $ps->elementMatches('scope', $node, $ele);
+ $this->assertFalse($ret);
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/QueryPathEventHandlerTest.php b/lib/querypath/test/Tests/QueryPath/CSS/QueryPathEventHandlerTest.php
new file mode 100644
index 0000000..01af591
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/QueryPathEventHandlerTest.php
@@ -0,0 +1,1439 @@
+<?php
+/** @file
+ * CSS Event handling tests
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\Token;
+use \QueryPath\CSS\QueryPathEventHandler;
+use \QueryPath\CSS\Parser;
+use \QueryPath\CSS\EventHandler;
+
+/**
+ * Tests for QueryPathEventHandler class.
+ * @ingroup querypath_tests
+ * @group deprecated
+ */
+class QueryPathEventHandlerTest extends TestCase {
+
+
+ var $xml = '<?xml version="1.0" ?>
+ <html>
+ <head>
+ <title>This is the title</title>
+ </head>
+ <body>
+ <div id="one">
+ <div id="two" class="class-one">
+ <div id="three">
+ Inner text.
+ </div>
+ </div>
+ </div>
+ <span class="class-two">Nada</span>
+ <p><p><p><p><p><p><p class="Odd"><p>8</p></p></p></p></p></p></p></p>
+ <ul>
+ <li class="Odd" id="li-one">Odd</li>
+ <li class="even" id="li-two">Even</li>
+ <li class="Odd" id="li-three">Odd</li>
+ <li class="even" id="li-four">Even</li>
+ <li class="Odd" id="li-five">Odd</li>
+ <li class="even" id="li-six">Even</li>
+ <li class="Odd" id="li-seven">Odd</li>
+ <li class="even" id="li-eight">Even</li>
+ <li class="Odd" id="li-nine">Odd</li>
+ <li class="even" id="li-ten">Even</li>
+ </ul>
+ </body>
+ </html>
+ ';
+
+ private function firstMatch($matches) {
+ $matches->rewind();
+ return $matches->current();
+ }
+ private function nthMatch($matches, $n = 0) {
+ foreach ($matches as $m) {
+ if ($matches->key() == $n) return $m;
+ }
+ }
+
+ public function testGetMatches() {
+ // Test root element:
+ $xml = '<?xml version="1.0" ?><test><inside/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test handing it a DOM Document
+ $handler = new QueryPathEventHandler($doc);
+ $matches = $handler->getMatches();
+ $this->assertTrue($matches->count() == 1);
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('test', $match->tagName);
+
+ // Test handling single element
+ $root = $doc->documentElement;
+ $handler = new QueryPathEventHandler($root);
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('test', $match->tagName);
+
+ // Test handling a node list
+ $eles = $doc->getElementsByTagName('inside');
+ $handler = new QueryPathEventHandler($eles);
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('inside', $match->tagName);
+
+ // Test handling an array of elements
+ $eles = $doc->getElementsByTagName('inside');
+ $array = array();
+ foreach ($eles as $ele) $array[] = $ele;
+ $handler = new QueryPathEventHandler($array);
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('inside', $match->tagName);
+ }
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testEmptySelector() {
+ $xml = '<?xml version="1.0" ?><t:test xmlns:t="urn:foo/bar"><t:inside id="first"/>Text<t:inside/><inside/></t:test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Basic test
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('');
+ $matches = $handler->getMatches();
+ $this->assertEquals(0, $matches->count());
+ }
+
+ public function testElementNS() {
+ $xml = '<?xml version="1.0" ?><t:test xmlns:t="urn:foo/bar"><t:inside id="first"/>Text<t:inside/><inside/></t:test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Basic test
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('t|inside');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('t:inside', $match->tagName);
+
+ // Basic test
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('t|test');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('t:test', $match->tagName);
+ }
+
+
+ /**
+ * @expectedException \QueryPath\CSS\ParseException
+ */
+ public function testFailedElementNS() {
+ $xml = '<?xml version="1.0" ?><t:test xmlns:t="urn:foo/bar"><t:inside id="first"/>Text<t:inside/><inside/></t:test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('myns\:mytest');
+ }
+
+ public function testElement() {
+ $xml = '<?xml version="1.0" ?><test><inside id="first"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Basic test
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('inside', $match->tagName);
+
+ $doc = new \DomDocument();
+ $doc->loadXML($this->xml);
+
+ // Test getting nested
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('div');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('div', $match->tagName);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test getting a list
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('li');
+ $matches = $handler->getMatches();
+ $this->assertEquals(10, $matches->count());
+ $match = $this->firstMatch($matches);
+ //$this->assertEquals('div', $match->tagName);
+ $this->assertEquals('li-one', $match->getAttribute('id'));
+
+ // Test getting the root element
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('html');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('html', $match->tagName);
+ }
+
+ public function testElementId() {
+ // Test root element:
+ $xml = '<?xml version="1.0" ?><test><inside id="first"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#first');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('inside', $match->tagName);
+
+ // Test a search with restricted scope:
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside#first');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('inside', $match->tagName);
+
+ }
+
+ public function testAnyElementInNS() {
+ $xml = '<?xml version="1.0" ?><ns1:test xmlns:ns1="urn:foo/bar"><ns1:inside/>Text<ns1:inside/></ns1:test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test handing it a DOM Document
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('ns1|*');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(3, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('ns1:test', $match->tagName);
+
+ // Test Issue #30:
+ $xml = '<?xml version="1.0" ?>
+ <ns1:test xmlns:ns1="urn:foo/bar">
+ <ns1:inside>
+ <ns1:insideInside>Test</ns1:insideInside>
+ </ns1:inside>
+ </ns1:test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('ns1|test>*');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('ns1:inside', $match->tagName);
+ }
+
+ public function testAnyElement() {
+ $xml = '<?xml version="1.0" ?><test><inside/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test handing it a DOM Document
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('*');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(3, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('test', $match->tagName);
+
+ $doc = new \DomDocument();
+ $doc->loadXML($this->xml);
+
+ // Test handing it a DOM Document
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#two *');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('three', $match->getAttribute('id'));
+
+ // Regression for issue #30
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#one>*');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(1, $matches->count(), 'Should match just top div.');
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('two', $match->getAttribute('id'), 'Should match ID #two');
+ }
+
+ public function testElementClass() {
+ $xml = '<?xml version="1.0" ?><test><inside class="foo" id="one"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test basic class
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('.foo');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test class in element
+ $doc = new \DomDocument();
+ $doc->loadXML($this->xml);
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('li.Odd');
+ $matches = $handler->getMatches();
+ $this->assertEquals(5, $matches->count());
+ $match = $this->nthMatch($matches, 4);
+ $this->assertEquals('li-nine', $match->getAttribute('id'));
+
+ // Test ID/class combo
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('.Odd#li-nine');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('li-nine', $match->getAttribute('id'));
+
+ }
+
+ public function testDirectDescendant() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <inside class="foo" id="one"/>
+ Text
+ <inside id="two">
+ <inside id="inner-one"/>
+ </inside>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test direct descendent
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('test > inside');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->nthMatch($matches, 1);
+ $this->assertEquals('two', $match->getAttribute('id'));
+
+ }
+
+ public function testAttribute() {
+ $xml = '<?xml version="1.0" ?><test><inside id="one" name="antidisestablishmentarianism"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test match on attr name
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test broken form
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[@name]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test match on attr name and equals value
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name="antidisestablishmentarianism"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test match on containsInString
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name*="disestablish"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test match on beginsWith
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name^="anti"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test match on endsWith
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name$="ism"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test containsWithSpace
+ $xml = '<?xml version="1.0" ?><test><inside id="one" name="anti dis establishment arian ism"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name~="dis"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ // Test containsWithHyphen
+ $xml = '<?xml version="1.0" ?><test><inside id="one" name="en-us"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside[name|="us"]');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ }
+
+ public function testPseudoClassLang() {
+
+ $xml = '<?xml version="1.0" ?><test><inside lang="en-us" id="one"/>Text<inside/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':lang(en-us)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside:lang(en)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside:lang(us)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ $xml = '<?xml version="1.0" ?><test><inside lang="en-us" id="one"/>Text<inside lang="us" id="two"/></test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':lang(us)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->nthMatch($matches, 1);
+ $this->assertEquals('two', $match->getAttribute('id'));
+
+ $xml = '<?xml version="1.0" ?>
+ <test xmlns="http://aleph-null.tv/xml" xmlns:xml="http://www.w3.org/XML/1998/namespace">
+ <inside lang="en-us" id="one"/>Text
+ <inside xml:lang="en-us" id="two"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':lang(us)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $match = $this->nthMatch($matches, 1);
+ $this->assertEquals('two', $match->getAttribute('id'));
+ }
+
+ public function testPseudoClassEnabledDisabledChecked() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <inside enabled="enabled" id="one"/>Text
+ <inside disabled="disabled" id="two"/>
+ <inside checked="FOOOOO" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':enabled');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('one', $match->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':disabled');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('two', $match->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':checked()');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $match = $this->firstMatch($matches);
+ $this->assertEquals('three', $match->getAttribute('id'));
+ }
+
+ public function testPseudoClassLink() {
+ $xml = '<?xml version="1.0"?><a><b href="foo"/><c href="foo"/></a>';
+ $qp = qp($xml, ':link');
+ $this->assertEquals(2, $qp->size());
+ }
+
+ public function testPseudoClassXReset() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <inside enabled="enabled" id="one"/>Text
+ <inside disabled="disabled" id="two"/>
+ <inside checked="FOOOOO" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('inside');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $handler->find(':x-reset');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('test', $this->firstMatch($matches)->tagName);
+ }
+
+ public function testPseudoClassRoot() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <inside enabled="enabled" id="one"/>Text
+ <inside disabled="disabled" id="two"/>
+ <inside checked="FOOOOO" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+ $start = $doc->getElementsByTagName('inside');
+
+ // Start "deep in the doc" and traverse backward.
+ $handler = new QueryPathEventHandler($start);
+ $handler->find(':root');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('test', $this->firstMatch($matches)->tagName);
+ }
+
+ // Test removed so I can re-declare
+ // listPeerElements as private.
+ public function xtestListPeerElements() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four"/>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test full list
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#one');
+ $matches = $handler->getMatches();
+ $peers = $handler->listPeerElements($this->firstMatch($matches));
+ $this->assertEquals(6, count($peers));
+ }
+ /*
+ public function testChildAtIndex() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four"/>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test full list
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('test:child-at-index(1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->nthMatch($matches, 1)->getAttribute('id'));
+ }*/
+
+ public function testPseudoClassNthChild() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four"/>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test full list
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':root :even');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('four', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test restricted to specific element
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:even');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('four', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test restricted to specific element, odd this time
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:odd');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(odd)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(odd)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(2n+1)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(2n+1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(2n) (even)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(2n)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('four', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Not totally sure what should be returned here
+ // Test nth-child(-2n)
+ // $handler = new QueryPathEventHandler($doc);
+ // $handler->find('i:nth-child(-2n)');
+ // $matches = $handler->getMatches();
+ // $this->assertEquals(2, $matches->count());
+ // $this->assertEquals('four', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(2n-1) (odd, equiv to 2n + 1)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(2n-1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(4n) (every fourth row)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(4n)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('four', $this->nthMatch($matches, 0)->getAttribute('id'));
+
+ // Test nth-child(4n+1) (first of every four rows)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(4n+1)');
+ $matches = $handler->getMatches();
+ // Should match rows one and five
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('five', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // Test nth-child(1) (First row)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ // Test nth-child(0n-0) (Empty list)
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(0n-0)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(0, $matches->count());
+
+ // Test nth-child(-n+3) (First three lines)
+ // $handler = new QueryPathEventHandler($doc);
+ // $handler->find('i:nth-child(-n+3)');
+ // $matches = $handler->getMatches();
+ // $this->assertEquals(3, $matches->count());
+
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three">
+ <i class="odd" id="inner-one"/>
+ <i class="even" id="inner-two"/>
+ </i>
+ <i class="even" id="four"/>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test nested items.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-child(odd)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(4, $matches->count());
+ $matchIDs = array();
+ foreach ($matches as $m) {
+ $matchIDs[] = $m->getAttribute('id');
+ }
+// $matchIDs = sort($matchIDs);
+ $this->assertEquals(array('one', 'three', 'inner-one', 'five'), $matchIDs);
+ //$this->assertEquals('inner-one', $matches[3]->getAttribute('id'));
+
+ }
+ public function testPseudoClassOnlyChild() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:only-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="odd" id="two"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:only-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(0, $matches->count());
+ //$this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassOnlyOfType() {
+ // TODO: Added this late (it was missing in original test),
+ // and I'm not sure if the assumed behavior is correct.
+
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:only-of-type');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="odd" id="two"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:only-of-type');
+ $matches = $handler->getMatches();
+ $this->assertEquals(0, $matches->count());
+ }
+
+ public function testPseudoClassFirstChild() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four">
+ <i class="odd" id="inner-one"/>
+ <i class="even" id="inner-two"/>
+ <i class="odd" id="inner-three"/>
+ <i class="even" id="inner-four"/>
+ </i>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#four > i:first-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('inner-one', $this->firstMatch($matches)->getAttribute('id'));
+
+ // Test for two last children
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:first-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-one', $this->nthMatch($matches, 1)->getAttribute('id'));
+ }
+
+ public function testPseudoClassLastChild() {
+ //print '----' . PHP_EOL;
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four">
+ <i class="odd" id="inner-one"/>
+ <i class="even" id="inner-two"/>
+ <i class="odd" id="inner-three"/>
+ <i class="even" id="inner-four"/>
+ </i>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test single last child.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#four > i:last-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('inner-four', $this->nthMatch($matches, 0)->getAttribute('id'));
+
+ // Test for two last children
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:last-child');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-four', $this->nthMatch($matches, 0)->getAttribute('id'));
+ $this->assertEquals('five', $this->nthMatch($matches, 1)->getAttribute('id'));
+ }
+
+ public function testPseudoClassNthLastChild() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ <i class="even" id="four">
+ <i class="odd" id="inner-one"/>
+ <i class="even" id="inner-two"/>
+ <i class="odd" id="inner-three"/>
+ <i class="even" id="inner-four"/>
+ </i>
+ <i class="odd" id="five"/>
+ <e class="even" id="six"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#four > i:nth-last-child(odd)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-two', $this->nthMatch($matches, 0)->getAttribute('id'));
+ $this->assertEquals('inner-four', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ // According to spec, this should be last two elements.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#four > i:nth-last-child(-1n+2)');
+ $matches = $handler->getMatches();
+ //print $this->firstMatch($matches)->getAttribute('id');
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-three', $this->nthMatch($matches, 0)->getAttribute('id'));
+ $this->assertEquals('inner-four', $this->nthMatch($matches, 1)->getAttribute('id'));
+ }
+
+ public function testPseudoClassFirstOfType() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <n class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:first-of-type(odd)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassNthFirstOfType() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <n class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:first-of-type(1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassLastOfType() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <n class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:last-of-type(odd)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('three', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoNthClassLastOfType() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <n class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-last-of-type(1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('three', $this->firstMatch($matches)->getAttribute('id'));
+
+
+ // Issue #56: an+b not working.
+ $xml = '<?xml version="1.0"?>
+ <root>
+ <div>I am the first div.</div>
+ <div>I am the second div.</div>
+ <div>I am the third div.</div>
+ <div>I am the fourth div.</div>
+ <div id="five">I am the fifth div.</div>
+ <div id="six">I am the sixth div.</div>
+ <div id="seven">I am the seventh div.</div>
+ </root>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('div:nth-last-of-type(-n+3)');
+ $matches = $handler->getMatches();
+
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('five', $this->firstMatch($matches)->getAttribute('id'));
+
+ }
+
+ public function testPseudoClassEmpty() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <n class="odd" id="one"/>
+ <i class="even" id="two"></i>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('n:empty');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:empty');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassFirst() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:first');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassLast() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:last');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('three', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassGT() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:gt(1)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+ public function testPseudoClassLT() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ // Test alternate rows from the end.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:lt(3)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('one', $this->nthMatch($matches,0)->getAttribute('id'));
+ $this->assertEquals('two', $this->nthMatch($matches,1)->getAttribute('id'));
+ }
+ public function testPseudoClassNTH() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth(2)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:eq(2)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+ public function testPseudoClassNthOfType() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <i class="odd" id="one"/>
+ <i class="even" id="two"/>
+ <i class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('i:nth-of-type(2)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassFormElements() {
+ $form = array('text', 'radio', 'checkbox', 'button', 'password');
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <input type="%s" class="odd" id="one"/>
+ </test>';
+
+ foreach ($form as $item) {
+ $doc = new \DomDocument();
+ $doc->loadXML(sprintf($xml, $item));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find(':' . $item);
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+ }
+
+ public function testPseudoClassHeader() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <h1 class="odd" id="one"/>
+ <h2 class="even" id="two"/>
+ <h6 class="odd" id="three"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('test :header');
+ $matches = $handler->getMatches();
+ $this->assertEquals(3, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 2)->getAttribute('id'));
+ }
+
+ public function testPseudoClassContains() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <p id="one">This is text.</p>
+ <p id="two"><i>More text</i></p>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains(This is text.)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('* :contains(More text)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count(), 'Matches two instance of same text?');
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains("This is text.")');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count(), 'Quoted text matches unquoted pcdata');
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains(\\\'This is text.\\\')');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count(), 'One match for quoted string.');
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ // Test for issue #32
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains(text)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count(), 'Two matches for fragment of string.');
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ }
+
+ public function testPseudoClassContainsExactly() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <p id="one">This is text.</p>
+ <p id="two"><i>More text</i></p>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains(This is text.)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('* :contains(More text)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count(), 'Matches two instance of same text.');
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains("This is text.")');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count(), 'Quoted text matches unquoted pcdata');
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('p:contains(\\\'This is text.\\\')');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count(), 'One match for quoted string.');
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassHas() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <outer id="one">
+ <inner/>
+ </outer>
+ <outer id="two"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer:has(inner)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoClassNot() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <outer id="one">
+ <inner/>
+ </outer>
+ <outer id="two" class="notMe"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer:not(#one)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer:not(inner)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer:not(.notMe)');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('one', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testPseudoElement() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <outer id="one">Texts
+
+ More text</outer>
+ <outer id="two" class="notMe"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer::first-letter');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('T', $this->firstMatch($matches)->textContent);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('outer::first-line');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('Texts', $this->firstMatch($matches)->textContent);
+ }
+
+ public function testAdjacent() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <li id="one"/><li id="two"/><li id="three">
+ <li id="inner-one">
+ <li id="inner-inner-one"/>
+ <li id="inner-inner-one"/>
+ </li>
+ <li id="inner-two"/>
+ </li>
+ <li id="four"/>
+ <li id="five"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#one + li');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('two', $this->firstMatch($matches)->getAttribute('id'));
+
+ // Tell it to ignore whitespace nodes.
+ $doc->loadXML($xml, LIBXML_NOBLANKS);
+
+ // Test with whitespace sensitivity weakened.
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#four + li');
+ $matches = $handler->getMatches();
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('five', $this->firstMatch($matches)->getAttribute('id'));
+ }
+ public function testAnotherSelector() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <li id="one"/><li id="two"/><li id="three">
+ <li id="inner-one">
+ <li id="inner-inner-one"/>
+ <li id="inner-inner-one"/>
+ </li>
+ <li id="inner-two"/>
+ </li>
+ <li id="four"/>
+ <li id="five"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#one, #two');
+ $matches = $handler->getMatches();
+ //print $this->firstMatch($matches)->getAttribute('id') . PHP_EOL;
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('two', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ }
+ public function testSibling() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <li id="one"/><li id="two"/><li id="three">
+ <li id="inner-one">
+ <li id="inner-inner-one"/>
+ <il id="inner-inner-two"/>
+ <li id="dont-match-me"/>
+ </li>
+ <li id="inner-two"/>
+ </li>
+ <li id="four"/>
+ <li id="five"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#one ~ li');
+ $matches = $handler->getMatches();
+ //print $this->firstMatch($matches)->getAttribute('id') . PHP_EOL;
+ $this->assertEquals(4, $matches->count());
+ $this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#two ~ li');
+ $matches = $handler->getMatches();
+ //print $this->firstMatch($matches)->getAttribute('id') . PHP_EOL;
+ $this->assertEquals(3, $matches->count());
+ //$this->assertEquals('three', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('#inner-one > li ~ il');
+ $matches = $handler->getMatches();
+ //print $this->firstMatch($matches)->getAttribute('id') . PHP_EOL;
+ $this->assertEquals(1, $matches->count());
+ $this->assertEquals('inner-inner-two', $this->firstMatch($matches)->getAttribute('id'));
+ }
+
+ public function testAnyDescendant() {
+ $xml = '<?xml version="1.0" ?>
+ <test>
+ <li id="one"/><li id="two"/><li id="three">
+ <li id="inner-one" class="foo">
+ <li id="inner-inner-one" class="foo"/>
+ <il id="inner-inner-two"/>
+ <li id="dont-match-me"/>
+ </li>
+ <li id="inner-two"/>
+ </li>
+ <li id="four"/>
+ <li id="five"/>
+ </test>';
+ $doc = new \DomDocument();
+ $doc->loadXML($xml);
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('*');
+ $matches = $handler->getMatches();
+ $this->assertEquals(11, $matches->count());
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('*.foo');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-inner-one', $this->nthMatch($matches, 1)->getAttribute('id'));
+
+ $handler = new QueryPathEventHandler($doc);
+ $handler->find('test > li *.foo');
+ $matches = $handler->getMatches();
+ $this->assertEquals(2, $matches->count());
+ $this->assertEquals('inner-inner-one', $this->nthMatch($matches, 1)->getAttribute('id'));
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/SelectorTest.php b/lib/querypath/test/Tests/QueryPath/CSS/SelectorTest.php
new file mode 100644
index 0000000..495f1f5
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/SelectorTest.php
@@ -0,0 +1,120 @@
+<?php
+namespace QueryPath\Tests;
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\Selector,
+ \QueryPath\CSS\SimpleSelector,
+ \QueryPath\CSS\EventHandler;
+
+class SelectorTest extends TestCase {
+
+ protected function parse($selector) {
+ $handler = new \QueryPath\CSS\Selector();
+ $parser = new \QueryPath\CSS\Parser($selector, $handler);
+ $parser->parse();
+ return $handler;
+ }
+
+ public function testElement() {
+ $selector = $this->parse('test')->toArray();
+
+ $this->assertEquals(1, count($selector));
+ $this->assertEquals('test', $selector[0]['0']->element);
+ }
+
+ public function testElementNS() {
+ $selector = $this->parse('foo|test')->toArray();
+
+ $this->assertEquals(1, count($selector));
+ $this->assertEquals('test', $selector[0]['0']->element);
+ $this->assertEquals('foo', $selector[0]['0']->ns);
+ }
+
+ public function testId() {
+ $selector = $this->parse('#test')->toArray();
+
+ $this->assertEquals(1, count($selector));
+ $this->assertEquals('test', $selector[0][0]->id);
+ }
+
+ public function testClasses() {
+ $selector = $this->parse('.test')->toArray();
+
+ $this->assertEquals(1, count($selector));
+ $this->assertEquals('test', $selector[0][0]->classes[0]);
+
+ $selector = $this->parse('.test.foo.bar')->toArray();
+ $this->assertEquals('test', $selector[0][0]->classes[0]);
+ $this->assertEquals('foo', $selector[0][0]->classes[1]);
+ $this->assertEquals('bar', $selector[0][0]->classes[2]);
+
+ }
+
+ public function testAttributes() {
+ $selector = $this->parse('foo[bar=baz]')->toArray();
+ $this->assertEquals(1, count($selector));
+ $attrs = $selector[0][0]->attributes;
+
+ $this->assertEquals(1, count($attrs));
+
+ $attr = $attrs[0];
+ $this->assertEquals('bar', $attr['name']);
+ $this->assertEquals(EventHandler::isExactly, $attr['op']);
+ $this->assertEquals('baz', $attr['value']);
+
+ $selector = $this->parse('foo[bar=baz][size=one]')->toArray();
+ $attrs = $selector[0][0]->attributes;
+
+ $this->assertEquals('one', $attrs[1]['value']);
+ }
+
+ public function testAttributesNS() {
+ $selector = $this->parse('[myns|foo=bar]')->toArray();
+
+ $attr = $selector[0][0]->attributes[0];
+
+ $this->assertEquals('myns', $attr['ns']);
+ $this->assertEquals('foo', $attr['name']);
+ }
+
+ public function testPseudoClasses() {
+ $selector = $this->parse('foo:first')->toArray();
+ $pseudo = $selector[0][0]->pseudoClasses;
+
+ $this->assertEquals(1, count($pseudo));
+
+ $this->assertEquals('first', $pseudo[0]['name']);
+ }
+
+ public function testPseudoElements() {
+ $selector = $this->parse('foo::bar')->toArray();
+ $pseudo = $selector[0][0]->pseudoElements;
+
+ $this->assertEquals(1, count($pseudo));
+
+ $this->assertEquals('bar', $pseudo[0]);
+ }
+
+ public function testCombinators() {
+ // This implies *>foo
+ $selector = $this->parse('>foo')->toArray();
+
+ $this->assertEquals(SimpleSelector::directDescendant, $selector[0][1]->combinator);
+
+ // This will be a selector with three simples:
+ // 'bar'
+ // 'foo '
+ // '*>'
+ $selector = $this->parse('>foo bar')->toArray();
+ $this->assertNull($selector[0][0]->combinator);
+ $this->assertEquals(SimpleSelector::anyDescendant, $selector[0][1]->combinator);
+ $this->assertEquals(SimpleSelector::directDescendant, $selector[0][2]->combinator);
+ }
+
+ public function testIterator() {
+ $selector = $this->parse('foo::bar');
+
+ $iterator = $selector->getIterator();
+ $this->assertInstanceOf('\Iterator', $iterator);
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/TokenTest.php b/lib/querypath/test/Tests/QueryPath/CSS/TokenTest.php
new file mode 100644
index 0000000..64a2261
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/TokenTest.php
@@ -0,0 +1,23 @@
+<?php
+/** @file
+ * CSS Event handling tests
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\Token;
+
+/**
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class TokenTest extends TestCase {
+ public function testName() {
+
+ $this->assertEquals('character', (Token::name(0)));
+ $this->assertEquals('a legal non-alphanumeric character', (Token::name(99)));
+ $this->assertEquals('end of file', (Token::name(FALSE)));
+ $this->assertEquals(0, strpos(Token::name(22),'illegal character'));
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/CSS/UtilTest.php b/lib/querypath/test/Tests/QueryPath/CSS/UtilTest.php
new file mode 100644
index 0000000..bdb8533
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/CSS/UtilTest.php
@@ -0,0 +1,51 @@
+<?php
+/** @file
+ * CSS Event handling tests
+ */
+namespace QueryPath\Tests;
+
+require_once __DIR__ . '/../TestCase.php';
+
+use \QueryPath\CSS\DOMTraverser\Util;
+
+/**
+ * @ingroup querypath_tests
+ * @group CSS
+ */
+class UtilTest extends TestCase {
+ public function testRemoveQuotes() {
+ $this->assertEquals('foo', Util::removeQuotes('"foo"'));
+ $this->assertEquals('foo', Util::removeQuotes("'foo'"));
+ $this->assertEquals('"foo\'', Util::removeQuotes("\"foo'"));
+ $this->assertEquals('f"o"o', Util::removeQuotes('f"o"o'));
+
+ }
+ public function testParseAnB() {
+ // even
+ $this->assertEquals(array(2, 0), Util::parseAnB('even'));
+ // odd
+ $this->assertEquals(array(2, 1), Util::parseAnB('odd'));
+ // 5
+ $this->assertEquals(array(0, 5), Util::parseAnB('5'));
+ // +5
+ $this->assertEquals(array(0, 5), Util::parseAnB('+5'));
+ // n
+ $this->assertEquals(array(1, 0), Util::parseAnB('n'));
+ // 2n
+ $this->assertEquals(array(2, 0), Util::parseAnB('2n'));
+ // -234n
+ $this->assertEquals(array(-234, 0), Util::parseAnB('-234n'));
+ // -2n+1
+ $this->assertEquals(array(-2, 1), Util::parseAnB('-2n+1'));
+ // -2n + 1
+ $this->assertEquals(array(-2, 1), Util::parseAnB(' -2n + 1 '));
+ // +2n-1
+ $this->assertEquals(array(2, -1), Util::parseAnB('2n-1'));
+ $this->assertEquals(array(2, -1), Util::parseAnB('2n - 1'));
+ // -n + 3
+ $this->assertEquals(array(-1, 3), Util::parseAnB('-n+3'));
+
+ // Test invalid values
+ $this->assertEquals(array(0, 0), Util::parseAnB('obviously + invalid'));
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/DOMQueryTest.php b/lib/querypath/test/Tests/QueryPath/DOMQueryTest.php
new file mode 100644
index 0000000..238555e
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/DOMQueryTest.php
@@ -0,0 +1,1865 @@
+<?php
+/** @file
+ * Tests for the DOMQuery class.
+ *
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+
+namespace QueryPath\Tests;
+
+/** @addtogroup querypath_tests Tests
+ * Unit tests and regression tests for DOMQuery.
+ */
+
+use QueryPath\DOMQuery;
+use \Masterminds\HTML5;
+
+/** */
+//require_once 'PHPUnit/Autoload.php';
+require __DIR__ . '/../../../vendor/autoload.php';
+require_once __DIR__ . '/TestCase.php';
+
+define('DATA_FILE', __DIR__ . '/../../data.xml');
+define('DATA_HTML_FILE', __DIR__ . '/../../data.html');
+define('NO_WRITE_FILE', __DIR__ . '/../../no-write.xml');
+define('MEDIUM_FILE', __DIR__ . '/../../amplify.xml');
+define('HTML_IN_XML_FILE', __DIR__ . '/../../html.xml');
+
+/**
+ * Tests for DOM Query. Primarily, this is focused on the DomQueryImpl
+ * class which is exposed through the DomQuery interface and the dq()
+ * factory function.
+ * @ingroup querypath_tests
+ */
+class DOMQueryTest extends TestCase {
+
+ /**
+ * @group basic
+ */
+ public function testDOMQueryConstructors() {
+
+ // From XML file
+ $file = DATA_FILE;
+ $qp = qp($file);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // From XML file with context
+ $cxt = stream_context_create();
+ $qp = qp($file, NULL, array('context' => $cxt));
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // From XML string
+ $str = '<?xml version="1.0" ?><root><inner/></root>';
+ $qp = qp($str);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // From SimpleXML
+ $str = '<?xml version="1.0" ?><root><inner/></root>';
+ $qp = qp(simplexml_load_string($str));
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // Test from DOMDocument
+ $qp = qp(\DOMDocument::loadXML($str));
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // Now with a selector:
+ $qp = qp($file, '#head');
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertEquals($qp->get(0)->tagName, 'head');
+
+ // Test HTML:
+ $htmlFile = DATA_HTML_FILE;
+ $qp = qp($htmlFile);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // Test with another DOMQuery.
+ $qp = qp($qp);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // Test from array of DOMNodes
+ $array = $qp->get();
+ $qp = qp($array);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ }
+ /**
+ * Test alternate constructors.
+ * @group basic
+ */
+ public function testDOMQueryHtmlConstructors() {
+ $qp = htmlqp(\QueryPath::HTML_STUB);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // Bad BR tag.
+ $borken = '<html><head></head><body><br></body></html>';
+ $qp = htmlqp($borken);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // XHTML Faker
+ $borken = '<?xml version="1.0"?><html><head></head><body><br></body></html>';
+ $qp = htmlqp($borken);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // HTML in a file that looks like XML.
+ $qp = htmlqp(HTML_IN_XML_FILE);
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertTrue($qp->get(0) instanceof \DOMNode);
+
+ // HTML5
+ $html5 = new \Masterminds\HTML5();
+ $dom = $html5->loadHTML(\QueryPath::HTML_STUB);
+ qp($dom,'html');
+
+ // Stripping #13 (CR) from HTML.
+ $borken = '<html><head></head><body><p>' . chr(13) . '</p><div id="after"/></body></html>';
+ $this->assertFalse(strpos(htmlqp($borken)->html(), '&#13;'), 'Test that CRs are not encoded.');
+
+ // Regression for #58: Make sure we aren't getting &#10; encoded.
+ $borken = '<html><head><style>
+ .BlueText {
+ color:red;
+ }</style><body></body></html>';
+
+ $this->assertFalse(strpos(htmlqp($borken)->html(), '&#10;'), 'Test that LF is not encoded.');
+
+ // Low ASCII in a file
+ $borken = '<html><head></head><body><p>' . chr(27) . '</p><div id="after"/></body></html>';
+ $this->assertEquals(1, htmlqp($borken, '#after')->size());
+ }
+
+ public function testForTests() {
+ $qp_methods = get_class_methods('\QueryPath\DOMQuery');
+ $test_methods = get_class_methods('\QueryPath\Tests\DOMQueryTest');
+
+ $ignore = array("__construct", "__call", "__clone", "get", "getOptions", "setMatches", "toArray", "getIterator");
+
+ $test_methods = array_map('strtolower', $test_methods);
+
+ foreach($qp_methods as $q) {
+ if(in_array($q, $ignore)) continue;
+ $this->assertTrue(in_array(strtolower("test".$q), $test_methods), $q . ' does not have a test method.');
+ }
+ }
+
+ public function testOptionXMLEncoding() {
+ $xml = qp(NULL, NULL, array('encoding' => 'iso-8859-1'))->append('<test/>')->xml();
+ $iso_found = preg_match('/iso-8859-1/', $xml) == 1;
+
+ $this->assertTrue($iso_found, 'Encoding should be iso-8859-1 in ' . $xml . 'Found ' . $iso_found);
+
+ $iso_found = preg_match('/utf-8/', $xml) == 1;
+ $this->assertFalse($iso_found, 'Encoding should not be utf-8 in ' . $xml);
+
+ $xml = qp('<?xml version="1.0" encoding="utf-8"?><test/>', NULL, array('encoding' => 'iso-8859-1'))->xml();
+ $iso_found = preg_match('/utf-8/', $xml) == 1;
+ $this->assertTrue($iso_found, 'Encoding should be utf-8 in ' . $xml);
+
+ $iso_found = preg_match('/iso-8859-1/', $xml) == 1;
+ $this->assertFalse($iso_found, 'Encoding should not be utf-8 in ' . $xml);
+
+ }
+
+ public function testQPAbstractFactory() {
+ $options = array('QueryPath_class' => '\QueryPath\Tests\QueryPathExtended');
+ $qp = qp(NULL, NULL, $options);
+ $this->assertTrue($qp instanceof QueryPathExtended, 'Is instance of extending class.');
+ $this->assertTrue($qp->foonator(), 'Has special foonator() function.');
+ }
+
+ public function testQPAbstractFactoryIterating() {
+ $xml = '<?xml version="1.0"?><l><i/><i/><i/><i/><i/></l>';
+ $options = array('QueryPath_class' => '\QueryPath\Tests\QueryPathExtended');
+ foreach(qp($xml, 'i', $options) as $item) {
+ $this->assertTrue($item instanceof QueryPathExtended, 'Is instance of extending class.');
+ }
+
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testFailedCall() {
+ // This should hit __call() and then fail.
+ qp()->fooMethod();
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testFailedObjectConstruction() {
+ qp(new \stdClass());
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedHTTPLoad() {
+ try {
+ qp('http://localhost:8877/no_such_file.xml');
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedHTTPLoadWithContext() {
+ try {
+ qp('http://localhost:8877/no_such_file.xml', NULL, array('foo' => 'bar'));
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedParseHTMLElement() {
+ try {
+ qp('<foo>&foonator;</foo>', NULL);
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedParseXMLElement() {
+ try {
+ qp('<?xml version="1.0"?><foo><bar>foonator;</foo>', NULL);
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+ public function testIgnoreParserWarnings() {
+ $qp = @qp('<html><body><b><i>BAD!</b></i></body>', NULL, array('ignore_parser_warnings' => TRUE));
+ $this->assertTrue(strpos($qp->html(), '<i>BAD!</i>') !== FALSE);
+
+ \QueryPath\Options::merge(array('ignore_parser_warnings' => TRUE));
+ $qp = @qp('<html><body><b><i>BAD!</b></i></body>');
+ $this->assertTrue(strpos($qp->html(), '<i>BAD!</i>') !== FALSE);
+
+ $qp = @qp('<html><body><blarg>BAD!</blarg></body>');
+ $this->assertTrue(strpos($qp->html(), '<blarg>BAD!</blarg>') !== FALSE, $qp->html());
+ \QueryPath\Options::set(array()); // Reset to empty options.
+ }
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedParseNonMarkup() {
+ try {
+ qp('<23dfadf', NULL);
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testFailedParseEntity() {
+ try {
+ qp('<?xml version="1.0"?><foo>&foonator;</foo>', NULL);
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ public function testReplaceEntitiesOption() {
+ $path = '<?xml version="1.0"?><root/>';
+ $xml = qp($path, NULL, array('replace_entities' => TRUE))->xml('<foo>&</foo>')->xml();
+ $this->assertTrue(strpos($xml, '<foo>&amp;</foo>') !== FALSE);
+
+ $xml = qp($path, NULL, array('replace_entities' => TRUE))->html('<foo>&</foo>')->xml();
+ $this->assertTrue(strpos($xml, '<foo>&amp;</foo>') !== FALSE);
+
+ $xml = qp($path, NULL, array('replace_entities' => TRUE))->xhtml('<foo>&</foo>')->xml();
+ $this->assertTrue(strpos($xml, '<foo>&amp;</foo>') !== FALSE);
+
+ \QueryPath\Options::set(array('replace_entities' => TRUE));
+ $this->assertTrue(strpos($xml, '<foo>&amp;</foo>') !== FALSE);
+ \QueryPath\Options::set(array());
+ }
+
+ /**
+ * @group basic
+ */
+ public function testFind() {
+ $file = DATA_FILE;
+ $qp = qp($file)->find('#head');
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertEquals($qp->get(0)->tagName, 'head');
+
+ $this->assertEquals('inner', qp($file)->find('.innerClass')->tag());
+
+ $string = '<?xml version="1.0"?><root><a/>Test</root>';
+ $qp = qp($string)->find('root');
+ $this->assertEquals(1, count($qp->get()), 'Check tag.');
+ $this->assertEquals($qp->get(0)->tagName, 'root');
+
+ $string = '<?xml version="1.0"?><root class="findme">Test</root>';
+ $qp = qp($string)->find('.findme');
+ $this->assertEquals(1, count($qp->get()), 'Check class.');
+ $this->assertEquals($qp->get(0)->tagName, 'root');
+ }
+ public function testFindInPlace() {
+ $file = DATA_FILE;
+ $qp = qp($file)->find('#head');
+ $this->assertEquals(1, count($qp->get()));
+ $this->assertEquals($qp->get(0)->tagName, 'head');
+
+ $this->assertEquals('inner', qp($file)->find('.innerClass')->tag());
+
+ $string = '<?xml version="1.0"?><root><a/>Test</root>';
+ $qp = qp($string)->find('root');
+ $this->assertEquals(1, count($qp->get()), 'Check tag.');
+ $this->assertEquals($qp->get(0)->tagName, 'root');
+
+ $string = '<?xml version="1.0"?><root class="findme">Test</root>';
+ $qp = qp($string)->find('.findme');
+ $this->assertEquals(1, count($qp->get()), 'Check class.');
+ $this->assertEquals($qp->get(0)->tagName, 'root');
+ }
+
+ /**
+ * @group basic
+ */
+ public function testTop() {
+ $file = DATA_FILE;
+ $qp = qp($file)->find('li');
+ $this->assertGreaterThan(2, $qp->size());
+ $this->assertEquals(1, $qp->top()->size());
+
+ // Added for QP 2.0
+ $xml = '<?xml version="1.0"?><root><u><l/><l/><l/></u><u/></root>';
+ $qp = qp($xml, 'l');
+ $this->assertEquals(3, $qp->size());
+ $this->assertEquals(2, $qp->top('u')->size());
+ }
+
+ /**
+ * @group basic
+ */
+ public function testAttr() {
+ $file = DATA_FILE;
+
+ $qp = qp($file)->find('#head');
+ $this->assertEquals(1, $qp->size());
+ $this->assertEquals($qp->get(0)->getAttribute('id'), $qp->attr('id'));
+
+ $qp->attr('foo', 'bar');
+ $this->assertEquals('bar', $qp->attr('foo'));
+
+ $qp->attr(array('foo2' => 'bar', 'foo3' => 'baz'));
+ $this->assertEquals('baz', $qp->attr('foo3'));
+
+ // Check magic nodeType attribute:
+ $this->assertEquals(XML_ELEMENT_NODE, qp($file)->find('#head')->attr('nodeType'));
+
+ // Since QP 2.1
+ $xml = '<?xml version="1.0"?><root><one a1="1" a2="2" a3="3"/></root>';
+ $qp = qp($xml, 'one');
+ $attrs = $qp->attr();
+ $this->assertEquals(3, count($attrs), 'Three attributes');
+ $this->assertEquals('1', $attrs['a1'], 'Attribute a1 has value 1.');
+ }
+
+ /**
+ * @group basic
+ */
+ public function testHasAttr() {
+ $xml = '<?xml version="1.0"?><root><div foo="bar"/></root>';
+
+ $this->assertFalse(qp($xml, 'root')->hasAttr('foo'));
+ $this->assertTrue(qp($xml, 'div')->hasAttr('foo'));
+
+ $xml = '<?xml version="1.0"?><root><div foo="bar"/><div foo="baz"></div></root>';
+ $this->assertTrue(qp($xml, 'div')->hasAttr('foo'));
+
+ $xml = '<?xml version="1.0"?><root><div bar="bar"/><div foo="baz"></div></root>';
+ $this->assertFalse(qp($xml, 'div')->hasAttr('foo'));
+
+ $xml = '<?xml version="1.0"?><root><div bar="bar"/><div bAZ="baz"></div></root>';
+ $this->assertFalse(qp($xml, 'div')->hasAttr('foo'));
+ }
+
+ public function testVal() {
+ $qp = qp('<?xml version="1.0"?><foo><bar value="test"/></foo>', 'bar');
+ $this->assertEquals('test', $qp->val());
+
+ $qp = qp('<?xml version="1.0"?><foo><bar/></foo>', 'bar')->val('test');
+ $this->assertEquals('test', $qp->attr('value'));
+ }
+
+ public function testCss() {
+ $file = DATA_FILE;
+ $this->assertEquals('foo: bar;', qp($file, 'unary')->css('foo', 'bar')->attr('style'));
+ $this->assertEquals('foo: bar;', qp($file, 'unary')->css('foo', 'bar')->css());
+ $this->assertEquals('foo: bar;', qp($file, 'unary')->css(array('foo' =>'bar'))->css());
+
+ // Issue #28: Setting styles in sequence should not result in the second
+ // style overwriting the first style:
+ $qp = qp($file, 'unary')->css('color', 'blue')->css('background-color', 'white');
+
+ $expects = 'color: blue;background-color: white;';
+ $actual = $qp->css();
+ $this->assertEquals(bin2hex($expects), bin2hex($actual), 'Two css calls should result in two attrs.');
+
+ // Make sure array merges work.
+ $qp = qp($file, 'unary')->css('a','a')->css(array('b'=>'b', 'c'=>'c'));
+ $this->assertEquals('a: a;b: b;c: c;', $qp->css());
+
+ // Make sure that second assignment overrides first assignment.
+ $qp = qp($file, 'unary')->css('a','a')->css(array('b'=>'b', 'a'=>'c'));
+ $this->assertEquals('a: c;b: b;', $qp->css());
+ }
+
+ public function testRemoveAttr() {
+ $file = DATA_FILE;
+
+ $qp = qp($file, 'inner')->removeAttr('class');
+ $this->assertEquals(2, $qp->size());
+ $this->assertFalse($qp->get(0)->hasAttribute('class'));
+
+ }
+
+ public function testEq() {
+ $file = DATA_FILE;
+ $qp = qp($file)->find('li')->eq(0);
+ $this->assertEquals(1, $qp->size());
+ $this->assertEquals($qp->attr('id'), 'one');
+ $this->assertEquals(1, qp($file, 'inner')->eq(0)->size());
+ $this->assertEquals(1, qp($file, 'li')->eq(0)->size());
+ $this->assertEquals("Hello", qp($file, 'li')->eq(0)->text());
+ $this->assertEquals("Last", qp($file, 'li')->eq(3)->text());
+ }
+
+ public function testIs() {
+ $file = DATA_FILE;
+ $this->assertTrue(qp($file)->find('#one')->is('#one'));
+ $this->assertTrue(qp($file)->find('li')->is('#one'));
+
+ $qp = qp($file)->find('#one');
+ $ele = $qp->get(0);
+ $this->assertTrue($qp->top('#one')->is($ele));
+
+ $qp = qp($file)->find('#one');
+ $ele = $qp->get(0);
+ $ele2 = $qp->top('#two')->get(0);
+
+ $list = new \SplDoublyLinkedList();
+ $list->push($ele);
+ $list->push($ele2);
+ $this->assertEquals(2, count($list));
+ //$this->assertEquals(2, )
+ $this->assertTrue($qp->top('#one,#two')->is($list));
+
+ }
+
+ public function testIndex() {
+ $xml = '<?xml version="1.0"?><foo><bar id="one"/><baz id="two"/></foo>';
+ $qp = qp($xml, 'bar');
+ $e1 = $qp->get(0);
+ $this->assertEquals(0, $qp->find('bar')->index($e1));
+ $this->assertFalse($qp->top()->find('#two')->index($e1));
+ }
+
+ public function testFilter() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file)->filter('li')->size());
+ $this->assertEquals(2, qp($file, 'inner')->filter('li')->size());
+ $this->assertEquals('inner-two', qp($file, 'inner')->filter('li')->eq(1)->attr('id'));
+ }
+
+ public function testFilterPreg() {
+ $xml = '<?xml version="1.0"?><root><div id="one">Foo</div><div>Moo</div></root>';
+ $qp = qp($xml, 'div')->filterPreg('/Foo/');
+
+ $this->assertEquals(1, $qp->Size());
+
+ // Check to make sure textContent is collected correctly.
+ $xml = '<?xml version="1.0"?><root><div>Hello <i>World</i></div></root>';
+ $qp = qp($xml, 'div')->filterPreg('/Hello\sWorld/');
+
+ $this->assertEquals(1, $qp->Size());
+ }
+
+ public function testFilterLambda() {
+ $file = DATA_FILE;
+ // Get all evens:
+ $l = 'return (($index + 1) % 2 == 0);';
+ $this->assertEquals(2, qp($file, 'li')->filterLambda($l)->size());
+ }
+
+ public function filterCallbackFunction($index, $item) {
+ return (($index + 1) % 2 == 0);
+ }
+
+
+ public function testFilterCallback() {
+ $file = DATA_FILE;
+ $cb = array($this, 'filterCallbackFunction');
+ $this->assertEquals(2, qp($file, 'li')->filterCallback($cb)->size());
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testFailedFilterCallback() {
+ $file = DATA_FILE;
+ $cb = array($this, 'noSuchFunction');
+ qp($file, 'li')->filterCallback($cb)->size();
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testFailedMapCallback() {
+ $file = DATA_FILE;
+ $cb = array($this, 'noSuchFunction');
+ qp($file, 'li')->map($cb)->size();
+ }
+
+
+ public function testNot() {
+ $file = DATA_FILE;
+
+ // Test with selector
+ $qp = qp($file, 'li:odd')->not('#one');
+ $this->assertEquals(2, $qp->size());
+
+ // Test with DOM Element
+ $qp = qp($file, 'li');
+ $el = $qp->branch()->filter('#one')->get(0);
+ $this->assertTrue($el instanceof \DOMElement, "Is DOM element.");
+ $this->assertEquals(4, $qp->not($el)->size());
+
+ // Test with array of DOM Elements
+ $qp = qp($file, 'li');
+ $arr = $qp->get();
+ $this->assertEquals(count($arr), $qp->size());
+ array_shift($arr);
+ $this->assertEquals(1, $qp->not($arr)->size());
+ }
+
+ public function testSlice() {
+ $file = DATA_FILE;
+ // There are five <li> elements
+ $qp = qp($file, 'li')->slice(1);
+ $this->assertEquals(4, $qp->size());
+
+ // The first item in the matches should be #two.
+ $this->assertEquals('two', $qp->attr('id'));
+
+ // THe last item should be #five
+ $this->assertEquals('five', $qp->eq(3)->attr('id'));
+
+ // This should not throw an error.
+ $this->assertEquals(4, qp($file, 'li')->slice(1, 9)->size());
+
+ $this->assertEquals(0, qp($file, 'li')->slice(9)->size());
+
+ // The first item should be #two, the last #three
+ $qp = qp($file, 'li')->slice(1, 2);
+ $this->assertEquals(2, $qp->size());
+ $this->assertEquals('two', $qp->attr('id'));
+ $this->assertEquals('three', $qp->eq(1)->attr('id'));
+ }
+
+ public function mapCallbackFunction($index, $item) {
+ if ($index == 1) {
+ return FALSE;
+ }
+ if ($index == 2) {
+ return array(1, 2, 3);
+ }
+ return $index;
+ }
+
+ public function testMap() {
+ $file = DATA_FILE;
+ $fn = 'mapCallbackFunction';
+ $this->assertEquals(7, qp($file, 'li')->map(array($this, $fn))->size());
+ }
+
+ public function eachCallbackFunction($index, $item) {
+ if ($index < 2) {
+ qp($item)->attr('class', 'test');
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ public function testEach() {
+ $file = DATA_FILE;
+ $fn = 'eachCallbackFunction';
+ $res = qp($file, 'li')->each(array($this, $fn));
+ $this->assertEquals(5, $res->size());
+ $this->assertFalse($res->get(4)->getAttribute('class') === NULL);
+ $this->assertEquals('test', $res->eq(1)->attr('class'));
+
+ // Test when each runs out of things to test before returning.
+ $res = qp($file, '#one')->each(array($this, $fn));
+ $this->assertEquals(1, $res->size());
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testEachOnInvalidCallback() {
+ $file = DATA_FILE;
+ $fn = 'eachCallbackFunctionFake';
+ $res = qp($file, 'li')->each(array($this, $fn));
+ }
+
+ public function testEachLambda() {
+ $file = DATA_FILE;
+ $fn = 'qp($item)->attr("class", "foo");';
+ $res = qp($file, 'li')->eachLambda($fn);
+ $this->assertEquals('foo', $res->eq(1)->attr('class'));
+ }
+
+ public function testDeepest() {
+ $str = '<?xml version="1.0" ?>
+ <root>
+ <one/>
+ <one><two/></one>
+ <one><two><three/></two></one>
+ <one><two><three><four/></three></two></one>
+ <one/>
+ <one><two><three><banana/></three></two></one>
+ </root>';
+ $deepest = qp($str)->deepest();
+ $this->assertEquals(2, $deepest->size());
+ $this->assertEquals('four', $deepest->get(0)->tagName);
+ $this->assertEquals('banana', $deepest->get(1)->tagName);
+
+ $deepest = qp($str, 'one')->deepest();
+ $this->assertEquals(2, $deepest->size());
+ $this->assertEquals('four', $deepest->get(0)->tagName);
+ $this->assertEquals('banana', $deepest->get(1)->tagName);
+
+ $str = '<?xml version="1.0" ?>
+ <root>
+ CDATA
+ </root>';
+ $this->assertEquals(1, qp($str)->deepest()->size());
+ }
+
+ public function testTag() {
+ $file = DATA_FILE;
+ $this->assertEquals('li', qp($file, 'li')->tag());
+ }
+
+ public function testAppend() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file,'unary')->append('<test/>')->find(':root > unary > test')->size());
+ $qp = qp($file,'#inner-one')->append('<li id="appended"/>');
+
+ $appended = $qp->find('#appended');
+ $this->assertEquals(1, $appended->size());
+ $this->assertNull($appended->get(0)->nextSibling);
+
+ $this->assertEquals(2, qp($file, 'inner')->append('<test/>')->top()->find('test')->size());
+ $this->assertEquals(2, qp($file, 'inner')->append(qp('<?xml version="1.0"?><test/>'))->top()->find('test')->size());
+ $this->assertEquals(4, qp($file, 'inner')->append(qp('<?xml version="1.0"?><root><test/><test/></root>', 'test'))->top()->find('test')->size());
+
+ // Issue #6: This seems to break on Debian Etch systems... no idea why.
+ $this->assertEquals('test', qp()->append('<test/>')->top()->tag());
+
+ // Issue #7: Failure issues warnings
+ // This seems to be working as expected -- libxml emits
+ // parse errors.
+ //$this->assertEquals(NULL, qp()->append('<test'));
+
+ // Test loading SimpleXML.
+ $simp = simplexml_load_file($file);
+ $qp = qp('<?xml version="1.0"?><foo/>')->append($simp);
+ $this->assertEquals(1, $qp->find('root')->size());
+
+ // Test with replace entities turned on:
+ $qp = qp($file, 'root', array('replace_entities' => TRUE))->append('<p>&raquo;</p>');
+ // Note that we are using a UTF-8 » character, not an ASCII 187. This seems to cause
+ // problems on some Windows IDEs. So here we do it the ugly way.
+ $utf8raquo = '<p>' . mb_convert_encoding(chr(187), 'utf-8', 'iso-8859-1') . '</p>';
+ //$this->assertEquals('<p>»</p>', $qp->find('p')->html(), 'Entities are decoded to UTF-8 correctly.');
+ $this->assertEquals($utf8raquo, $qp->find('p')->html(), 'Entities are decoded to UTF-8 correctly.');
+
+ // Test with empty, mainly to make sure it doesn't explode.
+ $this->assertTrue(qp($file)->append('') instanceof DOMQuery);
+ }
+
+ /**
+ * @expectedException \QueryPath\ParseException
+ */
+ public function testAppendBadMarkup() {
+ $file = DATA_FILE;
+ try{
+ qp($file, 'root')->append('<foo><bar></foo>');
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testAppendBadObject() {
+ $file = DATA_FILE;
+ try{
+ qp($file, 'root')->append(new \stdClass);
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+ }
+
+ public function testAppendTo() {
+ $file = DATA_FILE;
+ $dest = qp('<?xml version="1.0"?><root><dest/></root>', 'dest');
+ $qp = qp($file,'li')->appendTo($dest);
+ $this->assertEquals(5, $dest->find(':root li')->size());
+ }
+
+ public function testPrepend() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file,'unary')->prepend('<test/>')->find(':root > unary > test')->size());
+ $qp = qp($file,'#inner-one')->prepend('<li id="appended"/>')->find('#appended');
+ $this->assertEquals(1, $qp->size());
+ $this->assertNull($qp->get(0)->previousSibling);
+
+ // Test repeated insert
+ $this->assertEquals(2, qp($file,'inner')->prepend('<test/>')->top()->find('test')->size());
+ $this->assertEquals(4, qp($file,'inner')->prepend(qp('<?xml version="1.0"?><root><test/><test/></root>', 'test'))->top()->find('test')->size());
+ }
+
+ public function testPrependTo() {
+ $file = DATA_FILE;
+ $dest = qp('<?xml version="1.0"?><root><dest/></root>', 'dest');
+ $qp = qp($file,'li')->prependTo($dest);
+ $this->assertEquals(5, $dest->find(':root li')->size());
+ }
+
+ public function testBefore() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file,'unary')->before('<test/>')->find(':root > test ~ unary')->size());
+ $this->assertEquals(1, qp($file,'unary')->before('<test/>')->top('head ~ test')->size());
+ $this->assertEquals('unary', qp($file,'unary')->before('<test/>')->top(':root > test')->get(0)->nextSibling->tagName);
+
+ // Test repeated insert
+ $this->assertEquals(2, qp($file,'inner')->before('<test/>')->top()->find('test')->size());
+ $this->assertEquals(4, qp($file,'inner')->before(qp('<?xml version="1.0"?><root><test/><test/></root>', 'test'))->top()->find('test')->size());
+ }
+
+ public function testAfter() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file,'unary')->after('<test/>')->top(':root > unary ~ test')->size());
+ $this->assertEquals('unary', qp($file,'unary')->after('<test/>')->top(':root > test')->get(0)->previousSibling->tagName);
+
+ $this->assertEquals(2, qp($file,'inner')->after('<test/>')->top()->find('test')->size());
+ $this->assertEquals(4, qp($file,'inner')->after(qp('<?xml version="1.0"?><root><test/><test/></root>', 'test'))->top()->find('test')->size());
+ }
+
+ public function testInsertBefore() {
+ $file = DATA_FILE;
+ $dest = qp('<?xml version="1.0"?><root><dest/></root>', 'dest');
+ $qp = qp($file,'li')->insertBefore($dest);
+ $this->assertEquals(5, $dest->top(':root > li')->size());
+ $this->assertEquals('li', $dest->end()->find('dest')->get(0)->previousSibling->tagName);
+ }
+ public function testInsertAfter() {
+ $file = DATA_FILE;
+ $dest = qp('<?xml version="1.0"?><root><dest/></root>', 'dest');
+ $qp = qp($file,'li')->insertAfter($dest);
+ //print $dest->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(5, $dest->top(':root > li')->size());
+ }
+ public function testReplaceWith() {
+ $file = DATA_FILE;
+ $qp = qp($file,'unary')->replaceWith('<test><foo/></test>')->top('test');
+ //print $qp->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(1, $qp->size());
+
+ $qp = qp($file,'unary')->replaceWith(qp('<?xml version="1.0"?><root><test/><test/></root>', 'test'));
+ //print $qp->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(2, $qp->top()->find('test')->size());
+ }
+
+ public function testReplaceAll() {
+ $qp1 = qp('<?xml version="1.0"?><root><l/><l/></root>');
+ $doc = qp('<?xml version="1.0"?><bob><m/><m/></bob>')->get(0)->ownerDocument;
+
+ $qp2 = $qp1->find('l')->replaceAll('m', $doc);
+
+ $this->assertEquals(2, $qp2->top()->find('l')->size());
+ }
+
+ public function testUnwrap() {
+
+ // Unwrap center, and make sure junk goes away.
+ $xml = '<?xml version="1.0"?><root><wrapper><center/><junk/></wrapper></root>';
+ $qp = qp($xml, 'center')->unwrap();
+ $this->assertEquals('root', $qp->top('center')->parent()->tag());
+ $this->assertEquals(0, $qp->top('junk')->size());
+
+ // Make sure it works on two nodes in the same parent.
+ $xml = '<?xml version="1.0"?><root><wrapper><center id="1"/><center id="2"/></wrapper></root>';
+ $qp = qp($xml, 'center')->unwrap();
+
+ // Make sure they were unwrapped
+ $this->assertEquals('root', $qp->top('center')->parent()->tag());
+
+ // Make sure both get copied.
+ $this->assertEquals(2, $qp->top('center')->size());
+
+ // Make sure they are in order.
+ $this->assertEquals('2', $qp->top('center:last')->attr('id'));
+
+ // Test on root element.
+ $xml = '<?xml version="1.0"?><root><center/></root>';
+ $qp = qp($xml, 'center')->unwrap();
+ $this->assertEquals('center', $qp->top()->tag());
+
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testFailedUnwrap() {
+ // Cannot unwrap the root element.
+ $xml = '<?xml version="1.0"?><root></root>';
+ $qp = qp($xml, 'root')->unwrap();
+ $this->assertEquals('center', $qp->top()->tag());
+ }
+
+ public function testWrap() {
+ $file = DATA_FILE;
+ $xml = qp($file,'unary')->wrap('');
+ $this->assertTrue($xml instanceof DOMQuery);
+
+ $xml = qp($file,'unary')->wrap('<test id="testWrap"></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(1, qp($xml, '#testWrap')->get(0)->childNodes->length);
+
+ $xml = qp($file,'unary')->wrap(qp('<?xml version="1.0"?><root><test id="testWrap"></test><test id="ignored"></test></root>', 'test'))->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(1, qp($xml, '#testWrap')->get(0)->childNodes->length);
+
+ $xml = qp($file,'li')->wrap('<test class="testWrap"></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(5, qp($xml, '.testWrap')->size());
+
+ $xml = qp($file,'li')->wrap('<test class="testWrap"><inside><center/></inside></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(5, qp($xml, '.testWrap > inside > center > li')->size());
+ }
+
+ public function testWrapAll() {
+ $file = DATA_FILE;
+
+ $xml = qp($file,'unary')->wrapAll('');
+ $this->assertTrue($xml instanceof DOMQuery);
+
+ $xml = qp($file,'unary')->wrapAll('<test id="testWrap"></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(1, qp($xml, '#testWrap')->get(0)->childNodes->length);
+
+ $xml = qp($file,'unary')->wrapAll(qp('<?xml version="1.0"?><root><test id="testWrap"></test><test id="ignored"></test></root>', 'test'))->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(1, qp($xml, '#testWrap')->get(0)->childNodes->length);
+
+ $xml = qp($file,'li')->wrapAll('<test class="testWrap"><inside><center/></inside></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(5, qp($xml, '.testWrap > inside > center > li')->size());
+
+ }
+
+ public function testWrapInner() {
+ $file = DATA_FILE;
+
+ $this->assertTrue(qp($file,'#inner-one')->wrapInner('') instanceof DOMQuery);
+
+ $xml = qp($file,'#inner-one')->wrapInner('<test class="testWrap"></test>')->get(0)->ownerDocument->saveXML();
+ // FIXME: 9 includes text nodes. Should fix this.
+ $this->assertEquals(9, qp($xml, '.testWrap')->get(0)->childNodes->length);
+
+ $xml = qp($file,'inner')->wrapInner('<test class="testWrap"></test>')->get(0)->ownerDocument->saveXML();
+ $this->assertEquals(9, qp($xml, '.testWrap')->get(0)->childNodes->length);
+ $this->assertEquals(3, qp($xml, '.testWrap')->get(1)->childNodes->length);
+
+ $qp = qp($file,'inner')->wrapInner(qp('<?xml version="1.0"?><root><test class="testWrap"/><test class="ignored"/></root>', 'test'));
+ $this->assertEquals(2, $qp->find('inner > .testWrap')->size());
+ $this->assertEquals(0, $qp->find('.ignore')->size());
+ }
+
+ public function testRemove() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $start = $qp->size();
+ $finish = $qp->remove()->size();
+ $this->assertEquals($start, $finish);
+ $this->assertEquals(0, $qp->find(':root li')->size());
+
+ // Test for Issue #55
+ $data = '<?xml version="1.0"?><root><a>test</a><b> FAIL</b></root>';
+ $qp = qp($data);
+ $rem = $qp->remove('b');
+
+
+ $this->assertEquals(' FAIL', $rem->text());
+ $this->assertEquals('test', $qp->text());
+
+ // Test for Issue #63
+ $qp = qp($data);
+ $rem = $qp->remove('noSuchElement');
+ $this->assertEquals(0, count($rem));
+ }
+
+ public function testHasClass() {
+ $file = DATA_FILE;
+ $this->assertTrue(qp($file, '#inner-one')->hasClass('innerClass'));
+
+ $file = DATA_FILE;
+ $this->assertFalse(qp($file, '#inner-one')->hasClass('noSuchClass'));
+ }
+
+ public function testAddClass() {
+ $file = DATA_FILE;
+ $this->assertTrue(qp($file, '#inner-one')->addClass('testClass')->hasClass('testClass'));
+ }
+ public function testRemoveClass() {
+ $file = DATA_FILE;
+ // The add class tests to make sure that this works with multiple values.
+ $this->assertFalse(qp($file, '#inner-one')->removeClass('innerClass')->hasClass('innerClass'));
+ $this->assertTrue(qp($file, '#inner-one')->addClass('testClass')->removeClass('innerClass')->hasClass('testClass'));
+ }
+
+ public function testAdd() {
+ $file = DATA_FILE;
+ $this->assertEquals(7, qp($file, 'li')->add('inner')->size());
+ }
+
+ public function testEnd() {
+ $file = DATA_FILE;
+ $this->assertEquals(2, qp($file, 'inner')->find('li')->end()->size());
+ }
+
+ public function testAndSelf() {
+ $file = DATA_FILE;
+ $this->assertEquals(7, qp($file, 'inner')->find('li')->andSelf()->size());
+ }
+
+ public function testChildren() {
+ $file = DATA_FILE;
+ $this->assertEquals(5, qp($file, 'inner')->children()->size());
+ foreach (qp($file, 'inner')->children('li') as $kid) {
+ $this->assertEquals('li', $kid->tag());
+ }
+ $this->assertEquals(5, qp($file, 'inner')->children('li')->size());
+ $this->assertEquals(1, qp($file, ':root')->children('unary')->size());
+ }
+ public function testRemoveChildren() {
+ $file = DATA_FILE;
+ $this->assertEquals(0, qp($file, '#inner-one')->removeChildren()->find('li')->size());
+ }
+
+ public function testContents() {
+ $file = DATA_FILE;
+ $this->assertGreaterThan(5, qp($file, 'inner')->contents()->size());
+ // Two cdata nodes and one element node.
+ $this->assertEquals(3, qp($file, '#inner-two')->contents()->size());
+
+ // Issue #51: Under certain recursive conditions, this returns error.
+ // Warning: Whitespace is important in the markup beneath.
+ $xml = '<html><body><div>Hello
+ <div>how are you
+ <div>fine thank you
+ <div>and you ?</div>
+ </div>
+ </div>
+ </div>
+ </body></html>';
+ $cr = $this->contentsRecurse(qp($xml));
+ $this->assertEquals(14, count($cr), implode("\n", $cr));
+ }
+
+ public function testNS() {
+ $xml = '<?xml version="1.0"?><root xmlns="foo:bar"><e>test</e></root>';
+
+ $q = qp($xml, "e");
+
+ $this->assertEquals(1, $q->size());
+
+ $this->assertEquals("foo:bar", $q->ns());
+ }
+
+ /**
+ * Helper function for testContents().
+ * Based on problem reported in issue 51.
+ */
+ private function contentsRecurse($source, &$pack = array()) {
+ //static $i = 0;
+ //static $filter = "%d. Node type: %d, Content: '%s'\n";
+ $children = $source->contents();
+ //$node = $source->get(0);
+ $pack[] = 1; //sprintf($filter, ++$i, $node->nodeType, $source->html());
+
+ foreach ($children as $child) {
+ $pack += $this->contentsRecurse($child, $pack);
+ }
+
+ return $pack;
+ }
+
+ public function testSiblings() {
+ $file = DATA_FILE;
+ $this->assertEquals(3, qp($file, '#one')->siblings()->size());
+ $this->assertEquals(2, qp($file, 'unary')->siblings('inner')->size());
+ }
+
+ public function testXinclude() {
+
+ }
+
+ public function testHTML() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'unary');
+ $html = '<b>test</b>';
+ $this->assertEquals($html, $qp->html($html)->find('b')->html());
+
+ $html = '<html><head><title>foo</title></head><body>bar</body></html>';
+ // We expect a DocType to be prepended:
+ $this->assertEquals('<!DOCTYPE', substr(qp($html)->html(), 0, 9));
+
+ // Check that HTML is not added to empty finds. Note the # is for a special
+ // case.
+ $this->assertEquals('', qp($html, '#nonexistant')->html('<p>Hello</p>')->html());
+ $this->assertEquals('', qp($html, 'nonexistant')->html('<p>Hello</p>')->html());
+
+ // We expect NULL if the document is empty.
+ $this->assertNull(qp()->html());
+
+ // Non-DOMNodes should not be rendered:
+ $fn = 'mapCallbackFunction';
+ $this->assertNull(qp($file, 'li')->map(array($this, $fn))->html());
+ }
+
+ public function testInnerHTML() {
+ $html = '<html><head></head><body><div id="me">Test<p>Again</p></div></body></html>';
+
+ $this->assertEquals('Test<p>Again</p>', qp($html,'#me')->innerHTML());
+ }
+
+ public function testInnerXML() {
+ $html = '<?xml version="1.0"?><root><div id="me">Test<p>Again1</p></div></root>';
+ $test = 'Test<p>Again1</p>';
+
+ $this->assertEquals($test, qp($html,'#me')->innerXML());
+
+ $html = '<?xml version="1.0"?><root><div id="me">Test<p>Again2<br/></p><![CDATA[Hello]]><?pi foo ?></div></root>';
+ $test = 'Test<p>Again2<br/></p><![CDATA[Hello]]><?pi foo ?>';
+
+ $this->assertEquals($test, qp($html,'#me')->innerXML());
+
+ $html = '<?xml version="1.0"?><root><div id="me"/></root>';
+ $test = '';
+ $this->assertEquals($test, qp($html,'#me')->innerXML());
+
+ $html = '<?xml version="1.0"?><root id="me">test</root>';
+ $test = 'test';
+ $this->assertEquals($test, qp($html,'#me')->innerXML());
+ }
+
+ public function testInnerXHTML() {
+ $html = '<?xml version="1.0"?><html><head></head><body><div id="me">Test<p>Again</p></div></body></html>';
+
+ $this->assertEquals('Test<p>Again</p>', qp($html,'#me')->innerXHTML());
+
+ // Regression for issue #10: Tags should not be unary (e.g. we want <script></script>, not <script/>)
+ $xml = '<html><head><title>foo</title></head><body><div id="me">Test<p>Again<br/></p></div></body></html>';
+ // Look for a closing </br> tag
+ $regex = '/<\/br>/';
+ $this->assertRegExp($regex, qp($xml, '#me')->innerXHTML(), 'BR should have a closing tag.');
+ }
+
+ public function testXML() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'unary');
+ $xml = '<b>test</b>';
+ $this->assertEquals($xml, $qp->xml($xml)->find('b')->xml());
+
+ $xml = '<html><head><title>foo</title></head><body>bar</body></html>';
+ // We expect an XML declaration to be prepended:
+ $this->assertEquals('<?xml', substr(qp($xml, 'html')->xml(), 0, 5));
+
+ // We don't want an XM/L declaration if xml(TRUE).
+ $xml = '<?xml version="1.0"?><foo/>';
+ $this->assertFalse(strpos(qp($xml)->xml(TRUE), '<?xml'));
+
+ // We expect NULL if the document is empty.
+ $this->assertNull(qp()->xml());
+
+ // Non-DOMNodes should not be rendered:
+ $fn = 'mapCallbackFunction';
+ $this->assertNull(qp($file, 'li')->map(array($this, $fn))->xml());
+ }
+
+ public function testXHTML() {
+ // throw new Exception();
+
+ $file = DATA_FILE;
+ $qp = qp($file, 'unary');
+ $xml = '<b>test</b>';
+ $this->assertEquals($xml, $qp->xml($xml)->find('b')->xhtml());
+
+ $xml = '<html><head><title>foo</title></head><body>bar</body></html>';
+ // We expect an XML declaration to be prepended:
+ $this->assertEquals('<?xml', substr(qp($xml, 'html')->xhtml(), 0, 5));
+
+ // We don't want an XM/L declaration if xml(TRUE).
+ $xml = '<?xml version="1.0"?><foo/>';
+ $this->assertFalse(strpos(qp($xml)->xhtml(TRUE), '<?xml'));
+
+ // We expect NULL if the document is empty.
+ $this->assertNull(qp()->xhtml());
+
+ // Non-DOMNodes should not be rendered:
+ $fn = 'mapCallbackFunction';
+ $this->assertNull(qp($file, 'li')->map(array($this, $fn))->xhtml());
+
+ // Regression for issue #10: Tags should not be unary (e.g. we want <script></script>, not <script/>)
+ $xml = '<html><head><title>foo</title></head>
+ <body>
+ bar<br/><hr width="100">
+ <script></script>
+ <script>
+ alert("Foo");
+ </script>
+ <frameset id="fooframeset"></frameset>
+ </body></html>';
+
+ $xhtml = qp($xml)->xhtml();
+
+ //throw new Exception($xhtml);
+
+ // Look for a properly formatted BR unary tag:
+ $regex = '/<br \/>/';
+ $this->assertRegExp($regex, $xhtml, 'BR should have a closing tag.');
+
+ // Look for a properly formatted HR tag:
+ $regex = '/<hr width="100" \/>/';
+ $this->assertRegExp($regex, $xhtml, 'BR should have a closing tag.');
+
+ // Ensure that script tag is not collapsed:
+ $regex = '/<script><\/script>/';
+ $this->assertRegExp($regex, $xhtml, 'BR should have a closing tag.');
+
+ // Ensure that frameset tag is not collapsed (it looks like <frame>):
+ $regex = '/<frameset id="fooframeset"><\/frameset>/';
+ $this->assertRegExp($regex, $xhtml, 'BR should have a closing tag.');
+
+ // Ensure that script gets wrapped in CDATA:
+ $find = '/* <![CDATA[ ';
+ $this->assertTrue(strpos($xhtml, $find) > 0, 'CDATA section should be escaped.');
+
+ // Regression: Make sure it parses.
+ $xhtml = qp('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html><head></head><body><br /></body></html>')->xhtml();
+
+ qp($xhtml);
+
+ }
+
+ public function testWriteXML() {
+ $xml = '<?xml version="1.0"?><html><head><title>foo</title></head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeXML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect an XML declaration at the top.
+ $this->assertEquals('<?xml', substr($out, 0, 5));
+
+ $xml = '<?xml version="1.0"?><html><head><script>
+ <!--
+ 1 < 2;
+ -->
+ </script>
+ <![CDATA[This is CDATA]]>
+ <title>foo</title></head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeXML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect an XML declaration at the top.
+ $this->assertEquals('<?xml', substr($out, 0, 5));
+
+ // Test writing to a file:
+ $name = './' . __FUNCTION__ . '.xml';
+ qp($xml)->writeXML($name);
+ $this->assertTrue(file_exists($name));
+ $this->assertTrue(qp($name) instanceof DOMQuery);
+ unlink($name);
+ }
+
+ public function testWriteXHTML() {
+ $xml = '<?xml version="1.0"?><html><head><title>foo</title></head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeXHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect an XML declaration at the top.
+ $this->assertEquals('<?xml', substr($out, 0, 5));
+
+ $xml = '<?xml version="1.0"?><html><head><script>
+ <!--
+ 1 < 2;
+ -->
+ </script>
+ <![CDATA[This is CDATA]]>
+ <title>foo</title></head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'html')->writeXHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect an XML declaration at the top.
+ $this->assertEquals('<?xml', substr($out, 0, 5));
+
+ // Test writing to a file:
+ $name = './' . __FUNCTION__ . '.xml';
+ qp($xml)->writeXHTML($name);
+ $this->assertTrue(file_exists($name));
+ $this->assertTrue(qp($name) instanceof DOMQuery);
+ unlink($name);
+
+ // Regression for issue #10 (keep closing tags in XHTML)
+ $xhtml = '<?xml version="1.0"?><html><head><title>foo</title><script></script><br/></head><body>bar</body></html>';
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xhtml, 'html')->writeXHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ $pattern = '/<\/script>/';
+ $this->assertRegExp($pattern, $out, 'Should be closing script tag.');
+
+ $pattern = '/<\/br>/';
+ $this->assertRegExp($pattern, $out, 'Should be closing br tag.');
+ }
+
+ /**
+ * @expectedException \QueryPath\IOException
+ */
+ public function testFailWriteXML() {
+ try {
+ qp()->writeXML('./test/no-writing.xml');
+ }
+ catch (Exception $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+
+ }
+
+ /**
+ * @expectedException \QueryPath\IOException
+ */
+ public function testFailWriteXHTML() {
+ try {
+ qp()->writeXHTML('./test/no-writing.xml');
+ }
+ catch (\QueryPath\IOException $e) {
+ //print $e->getMessage();
+ throw $e;
+ }
+
+ }
+
+ /**
+ * @expectedException \QueryPath\IOException
+ */
+ public function testFailWriteHTML() {
+ try {
+ qp('<?xml version="1.0"?><foo/>')->writeXML('./test/no-writing.xml');
+ }
+ catch (\QueryPath\IOException $e) {
+ // print $e->getMessage();
+ throw $e;
+ }
+
+ }
+
+ public function testWriteHTML() {
+ $xml = '<html><head><title>foo</title></head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect a doctype declaration at the top.
+ $this->assertEquals('<!DOC', substr($out, 0, 5));
+
+ $xml = '<html><head><title>foo</title>
+ <script><!--
+ var foo = 1 < 5;
+ --></script>
+ </head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect a doctype declaration at the top.
+ $this->assertEquals('<!DOC', substr($out, 0, 5));
+
+ $xml = '<html><head><title>foo</title>
+ <script><![CDATA[
+ var foo = 1 < 5;
+ ]]></script>
+ </head><body>bar</body></html>';
+
+ if (!ob_start()) die ("Could not start OB.");
+ qp($xml, 'tml')->writeHTML();
+ $out = ob_get_contents();
+ ob_end_clean();
+
+ // We expect a doctype declaration at the top.
+ $this->assertEquals('<!DOC', substr($out, 0, 5));
+
+ // Test writing to a file:
+ $name = './' . __FUNCTION__ . '.html';
+ qp($xml)->writeXML($name);
+ $this->assertTrue(file_exists($name));
+ $this->assertTrue(qp($name) instanceof DOMQuery);
+ unlink($name);
+ }
+
+ public function testText() {
+ $xml = '<?xml version="1.0"?><root><div>Text A</div><div>Text B</div></root>';
+ $this->assertEquals('Text AText B', qp($xml)->text());
+ $this->assertEquals('Foo', qp($xml, 'div')->eq(0)->text('Foo')->text());
+ $this->assertEquals('BarBar', qp($xml, 'div')->text('Bar')->text());
+ }
+
+ public function testTextAfter() {
+ $xml = '<?xml version="1.0"?><root><br/>After<foo/><br/>After2<div/>After3</root>';
+ $this->assertEquals('AfterAfter2', qp($xml, 'br')->textAfter());
+ $this->assertEquals('Blarg', qp($xml, 'foo')->textAfter('Blarg')->top('foo')->textAfter());
+ }
+
+ public function testTextBefore() {
+ $xml = '<?xml version="1.0"?><root>Before<br/><foo/>Before2<br/>Before3<div/></root>';
+ $this->assertEquals('BeforeBefore2', qp($xml, 'br')->textBefore());
+ $this->assertEquals('Blarg', qp($xml, 'foo')->textBefore('Blarg')->top('foo')->textBefore());
+
+ }
+
+ public function testTextImplode() {
+ $xml = '<?xml version="1.0"?><root><div>Text A</div><div>Text B</div></root>';
+ $this->assertEquals('Text A, Text B', qp($xml, 'div')->textImplode());
+ $this->assertEquals('Text A--Text B', qp($xml, 'div')->textImplode('--'));
+
+ $xml = '<?xml version="1.0"?><root><div>Text A </div><div>Text B</div></root>';
+ $this->assertEquals('Text A , Text B', qp($xml, 'div')->textImplode());
+
+ $xml = '<?xml version="1.0"?><root><div>Text A </div>
+ <div>
+ </div><div>Text B</div></root>';
+ $this->assertEquals('Text A , Text B', qp($xml, 'div')->textImplode(', ', TRUE));
+
+ // Test with empties
+ $xml = '<?xml version="1.0"?><root><div>Text A</div><div> </div><div>Text B</div></root>';
+ $this->assertEquals('Text A- -Text B', qp($xml, 'div')->textImplode('-', FALSE));
+ }
+
+ public function testChildrenText() {
+ $xml = '<?xml version="1.0"?><root><wrapper>
+ NOT ME!
+ <div>Text A </div>
+ <div>
+ </div><div>Text B</div></wrapper></root>';
+ $this->assertEquals('Text A , Text B', qp($xml, 'div')->childrenText(', ', TRUE), 'Just inner text.');
+ }
+
+ public function testNext() {
+ $file = DATA_FILE;
+ $this->assertEquals('inner', qp($file, 'unary')->next()->tag());
+ $this->assertEquals('foot', qp($file, 'inner')->next()->eq(1)->tag());
+
+ $this->assertEquals('foot', qp($file, 'unary')->next('foot')->tag());
+
+ // Regression test for issue eabrand identified:
+
+ $qp = qp(\QueryPath::HTML_STUB, 'body')->append('<div></div><p>Hello</p><p>Goodbye</p>')
+ ->children('p')
+ ->after('<p>new paragraph</p>');
+
+ $testarray = array('new paragraph', 'Goodbye', 'new paragraph');
+
+ //throw new Exception($qp->top()->xml());
+
+ $qp = $qp->top('p:first-of-type');
+ $this->assertEquals('Hello', $qp->text(), "Test First P " . $qp->top()->html());
+ $i = 0;
+ while($qp->next('p')->html() != null) {
+ $qp = $qp->next('p');
+ $this->assertEquals(1, count($qp));
+ $this->assertEquals($testarray[$i], $qp->text(), $i . " didn't match " . $qp->top()->xml() );
+ $i++;
+ }
+ $this->assertEquals(3, $i);
+// $this->assertEquals('new paragraph', $qp->next()->text(), "Test Newly Added P");
+// $this->assertEquals('Goodbye', $qp->next()->text(), "Test third P");
+// $this->assertEquals('new paragraph', $qp->next()->text(), "Test Other Newly Added P");
+ }
+ public function testPrev() {
+ $file = DATA_FILE;
+ $this->assertEquals('head', qp($file, 'unary')->prev()->tag());
+ $this->assertEquals('inner', qp($file, 'inner')->prev()->eq(1)->tag());
+ $this->assertEquals('head', qp($file, 'foot')->prev('head')->tag());
+ }
+ public function testNextAll() {
+ $file = DATA_FILE;
+ $this->assertEquals(3, qp($file, '#one')->nextAll()->size());
+ $this->assertEquals(2, qp($file, 'unary')->nextAll('inner')->size());
+ }
+ public function testPrevAll() {
+ $file = DATA_FILE;
+ $this->assertEquals(3, qp($file, '#four')->prevAll()->size());
+ $this->assertEquals(2, qp($file, 'foot')->prevAll('inner')->size());
+ }
+ public function testParent() {
+ $file = DATA_FILE;
+ $this->assertEquals('root', qp($file, 'unary')->parent()->tag());
+ $this->assertEquals('root', qp($file, 'li')->parent('root')->tag());
+ $this->assertEquals(2, qp($file, 'li')->parent()->size());
+ }
+ public function testClosest() {
+ $file = DATA_FILE;
+ $this->assertEquals('root', qp($file, 'li')->parent('root')->tag());
+
+ $xml = '<?xml version="1.0"?>
+ <root>
+ <a class="foo">
+ <b/>
+ </a>
+ <b class="foo"/>
+ </root>';
+ $this->assertEquals(2, qp($xml, 'b')->closest('.foo')->size());
+ }
+
+ public function testParents() {
+ $file = DATA_FILE;
+
+ // Three: two inners and a root.
+ $this->assertEquals(3, qp($file, 'li')->parents()->size());
+ $this->assertEquals('root', qp($file, 'li')->parents('root')->tag());
+ }
+
+ public function testCloneAll() {
+ $file = DATA_FILE;
+
+ // Shallow test
+ $qp = qp($file, 'unary');
+ $one = $qp->get(0);
+ $two = $qp->cloneAll()->get(0);
+ $this->assertTrue($one !== $two);
+ $this->assertEquals('unary', $two->tagName);
+
+ // Deep test: make sure children are also cloned.
+ $qp = qp($file, 'inner');
+ $one = $qp->find('li')->get(0);
+ $two = $qp->top('inner')->cloneAll(TRUE)->findInPlace('li')->get(0);
+ $this->assertEquals('li', $two->tagName);
+ $this->assertTrue($one !== $two);
+ }
+
+ public function testBranch() {
+ $qp = qp(\QueryPath::HTML_STUB);
+ $branch = $qp->branch();
+ $branch->top('title')->text('Title');
+ $qp->top('title')->text('FOOOOO')->top();
+ $qp->find('body')->text('This is the body');
+
+ $this->assertEquals($qp->top('title')->text(), $branch->top('title')->text(), $branch->top()->html());
+
+ $qp = qp(\QueryPath::HTML_STUB);
+ $branch = $qp->branch('title');
+ $branch->find('title')->text('Title');
+ $qp->find('body')->text('This is the body');
+ $this->assertEquals($qp->top()->find('title')->text(), $branch->text());
+ }
+
+ public function testXpath() {
+ $file = DATA_FILE;
+
+ $this->assertEquals('head', qp($file)->xpath("//*[@id='head']")->tag());
+ }
+
+ public function test__clone() {
+ $file = DATA_FILE;
+
+ $qp = qp($file, 'inner:first-of-type');
+ $qp2 = clone $qp;
+ $this->assertFalse($qp === $qp2);
+ $qp2->findInPlace('li')->attr('foo', 'bar');
+ $this->assertEquals('', $qp->find('li')->attr('foo'));
+ $this->assertEquals('bar', $qp2->attr('foo'), $qp2->top()->xml());
+ }
+
+ public function testStub() {
+ $this->assertEquals(1, qp(\QueryPath::HTML_STUB)->find('title')->size());
+ }
+
+ public function testIterator() {
+
+ $qp = qp(\QueryPath::HTML_STUB, 'body')->append('<li/><li/><li/><li/>');
+
+ $this->assertEquals(4, $qp->find('li')->size());
+ $i = 0;
+ foreach ($qp->find('li') as $li) {
+ ++$i;
+ $li->text('foo');
+ }
+ $this->assertEquals(4, $i);
+ $this->assertEquals('foofoofoofoo', $qp->top()->find('li')->text());
+ }
+
+ public function testModeratelySizedDocument() {
+
+ $this->assertEquals(1, qp(MEDIUM_FILE)->size());
+
+ $contents = file_get_contents(MEDIUM_FILE);
+ $this->assertEquals(1, qp($contents)->size());
+ }
+
+ /**
+ * @deprecated
+ */
+ public function testSize() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $this->assertEquals(5, $qp->size());
+ }
+
+ public function testCount() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $this->assertEquals(5, $qp->count());
+
+ // Test that this is exposed to PHP's Countable logic.
+ $this->assertEquals(5, count(qp($file, 'li')));
+
+ }
+
+ public function testLength() {
+
+ // Test that the length attribute works exactly the same as size.
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $this->assertEquals(5, $qp->length);
+
+
+ }
+
+ public function testDocument() {
+ $file = DATA_FILE;
+ $doc1 = new \DOMDocument('1.0');
+ $doc1->load($file);
+ $qp = qp($doc1);
+
+ $this->assertEquals($doc1, $qp->document());
+
+ // Ensure that adding to the DOMDocument is accessible to QP:
+ $ele = $doc1->createElement('testDocument');
+ $doc1->documentElement->appendChild($ele);
+
+ $this->assertEquals(1, $qp->find('testDocument')->size());
+ }
+
+ /*
+ public function test__get() {
+ // Test that other properties are not interferred with by __get().
+ $file = DATA_FILE;
+ $options = array('QueryPath_class' => 'QueryPathExtended');
+ $foo = qp($file,'li', $options)->foo;
+
+ $this->assertEquals('bar', $foo);
+ }
+ */
+
+ /**
+ * @ expectedException \QueryPath\Exception
+ */
+ /*
+ public function testFailed__get() {
+ // This should generate an error because 'last' is protected.
+ qp(DATA_FILE)->last;
+ }
+ */
+
+ public function testDetach() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $start = $qp->size();
+ $finish = $qp->detach()->size();
+ $this->assertEquals($start, $finish);
+ $this->assertEquals(0, $qp->find(':root li')->size());
+ }
+
+ public function testAttach() {
+ $file = DATA_FILE;
+ $qp = qp($file, 'li');
+ $start = $qp->size();
+ $finish = $qp->detach()->size();
+ $dest = qp('<?xml version="1.0"?><root><dest/></root>', 'dest');
+ $qp = $qp->attach($dest);
+ $this->assertEquals(5, $dest->find(':root li')->size());
+ }
+
+ public function testEmptyElement() {
+ $file = DATA_FILE;
+ $this->assertEquals(0, qp($file, '#inner-two')->emptyElement()->find('li')->size());
+ $this->assertEquals('<inner id="inner-two"/>', qp($file, '#inner-two')->emptyElement()->html());
+
+ // Make sure text children get wiped out, too.
+ $this->assertEquals('', qp($file, 'foot')->emptyElement()->text());
+ }
+
+ public function testHas() {
+ $file = DATA_FILE;
+
+ // Test with DOMNode object
+ $qp = qp($file, 'foot');
+ $selector = $qp->get(0);
+ $qp = $qp->top('root')->has($selector);
+
+ // This should have one element named 'root'.
+ $this->assertEquals(1, $qp->size(), 'One element is a parent of foot');
+ $this->assertEquals('root', $qp->tag(), 'Root has foot.');
+
+ // Test with CSS selector
+ $qp = qp($file, 'root')->has('foot');
+
+ // This should have one element named 'root'.
+ $this->assertEquals(1, $qp->size(), 'One element is a parent of foot');
+ $this->assertEquals('root', $qp->tag(), 'Root has foot.');
+
+ // Test multiple matches.
+ $qp = qp($file, '#docRoot, #inner-two')->has('#five');
+ $this->assertEquals(2, $qp->size(), 'Two elements are parents of #five');
+ $this->assertEquals('inner', $qp->get(0)->tagName, 'Inner has li.');
+
+ /*
+ $this->assertEquals(qp($file, '#one')->children()->get(), qp($file, '#inner-one')->has($selector)->get(), "Both should be empty/false");
+ $qp = qp($file, 'root')->children("inner");
+ $selector = qp($file, '#two');
+ $this->assertNotEquals(qp($file, '#head'), qp($file, '#inner-one')->has($selector));
+ $this->assertEquals(qp($file, 'root'), qp($file, 'root')->has($selector), "Should both have 1 element - root");
+ */
+ }
+
+ public function testNextUntil() {
+ $file = DATA_FILE;
+ $this->assertEquals(3, qp($file, '#one')->nextUntil()->size());
+ $this->assertEquals(2, qp($file, 'li')->nextUntil('#three')->size());
+ }
+
+ public function testPrevUntil() {
+ $file = DATA_FILE;
+ $this->assertEquals(3, qp($file, '#four')->prevUntil()->size());
+ $this->assertEquals(2, qp($file, 'foot')->prevUntil('unary')->size());
+ }
+
+ public function testEven() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, 'inner')->even()->size());
+ $this->assertEquals(2, qp($file, 'li')->even()->size());
+ }
+
+ public function testOdd() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, 'inner')->odd()->size());
+ $this->assertEquals(3, qp($file, 'li')->odd()->size());
+ }
+
+ public function testFirst() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, 'inner')->first()->size());
+ $this->assertEquals(1, qp($file, 'li')->first()->size());
+ $this->assertEquals("Hello", qp($file, 'li')->first()->text());
+ }
+
+ public function testFirstChild() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, '#inner-one')->firstChild()->size());
+ $this->assertEquals("Hello", qp($file, '#inner-one')->firstChild()->text());
+ }
+
+ public function testLast() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, 'inner')->last()->size());
+ $this->assertEquals(1, qp($file, 'li')->last()->size());
+ $this->assertEquals('', qp($file, 'li')->last()->text());
+ }
+
+ public function testLastChild() {
+ $file = DATA_FILE;
+ $this->assertEquals(1, qp($file, '#inner-one')->lastChild()->size());
+ $this->assertEquals("Last", qp($file, '#inner-one')->lastChild()->text());
+ }
+
+ public function testParentsUntil() {
+ $file = DATA_FILE;
+
+ // Three: two inners and a root.
+ $this->assertEquals(3, qp($file, 'li')->parentsUntil()->size());
+ $this->assertEquals(2, qp($file, 'li')->parentsUntil('root')->size());
+ }
+
+ public function testSort() {
+ $xml = '<?xml version="1.0"?><r><s/><i>1</i><i>5</i><i>2</i><i>1</i><e/></r>';
+
+ // Canary.
+ $qp = qp($xml, 'i');
+ $expect = array(1, 5, 2, 1);
+ foreach($qp as $item) {
+ $this->assertEquals(array_shift($expect), $item->text());
+ }
+
+ // Test simple ordering.
+ $comp = function (\DOMNode $a, \DOMNode $b) {
+ if ($a->textContent == $b->textContent) {
+ return 0;
+ }
+ return $a->textContent > $b->textContent ? 1 : -1;
+ };
+ $qp = qp($xml, 'i')->sort($comp);
+ $expect = array(1, 1, 2, 5);
+ foreach($qp as $item) {
+ $this->assertEquals(array_shift($expect), $item->text());
+ }
+
+ $comp = function (\DOMNode $a, \DOMNode $b) {
+ $qpa = qp($a);
+ $qpb = qp($b);
+
+ if ($qpa->text() == $qpb->text()) {
+ return 0;
+ }
+ return $qpa->text()> $qpb->text()? 1 : -1;
+ };
+ $qp = qp($xml, 'i')->sort($comp);
+ $expect = array(1, 1, 2, 5);
+ foreach($qp as $item) {
+ $this->assertEquals(array_shift($expect), $item->text());
+ }
+
+ // Test DOM re-ordering
+ $comp = function (\DOMNode $a, \DOMNode $b) {
+ if ($a->textContent == $b->textContent) {
+ return 0;
+ }
+ return $a->textContent > $b->textContent ? 1 : -1;
+ };
+ $qp = qp($xml, 'i')->sort($comp, TRUE);
+ $expect = array(1, 1, 2, 5);
+ foreach($qp as $item) {
+ $this->assertEquals(array_shift($expect), $item->text());
+ }
+ $res = $qp->top()->xml();
+ $expect_xml = '<?xml version="1.0"?><r><s/><i>1</i><i>1</i><i>2</i><i>5</i><e/></r>';
+ $this->assertXmlStringEqualsXmlString($expect_xml, $res);
+ }
+
+ /**
+ * Regression test for issue #14.
+ */
+ public function testRegressionFindOptimizations() {
+ $xml = '<?xml version="1.0"?><root>
+ <item id="outside">
+ <item>
+ <item id="inside">Test</item>
+ </item>
+ </item>
+ </root>';
+
+ // From inside, should not be able to find outside.
+ $this->assertEquals(0, qp($xml, '#inside')->find('#outside')->size());
+
+ $xml = '<?xml version="1.0"?><root>
+ <item class="outside">
+ <item>
+ <item class="inside">Test</item>
+ </item>
+ </item>
+ </root>';
+ // From inside, should not be able to find outside.
+ $this->assertEquals(0, qp($xml, '.inside')->find('.outside')->size());
+ }
+
+ public function testDataURL() {
+
+ $text = 'Hi!'; // Base-64 encoded value would be SGkh
+ $xml = '<?xml version="1.0"?><root><item/></root>';
+
+ $qp = qp($xml, 'item')->dataURL('secret', $text, 'text/plain');
+
+ $this->assertEquals(1, $qp->top('item[secret]')->size(), 'One attr should be added.');
+
+ $this->assertEquals('data:text/plain;base64,SGkh', $qp->attr('secret'), 'Attr value should be data URL.');
+
+ $result = $qp->dataURL('secret');
+ $this->assertEquals(2, count($result), 'Should return two-array.');
+ $this->assertEquals($text, $result['data'] , 'Should return original data, decoded.');
+ $this->assertEquals('text/plain', $result['mime'], 'Should return the original MIME');
+ }
+
+ public function testEncodeDataURL() {
+ $data = \QueryPath::encodeDataURL('Hi!', 'text/plain');
+ $this->assertEquals('data:text/plain;base64,SGkh', $data);
+ }
+}
+
+/**
+ * A simple mock for testing qp()'s abstract factory.
+ *
+ * @ingroup querypath_tests
+ */
+class QueryPathExtended extends DOMQuery {
+ public $foo = 'bar';
+ public function foonator() {
+ return TRUE;
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/EntitiesTest.php b/lib/querypath/test/Tests/QueryPath/EntitiesTest.php
new file mode 100644
index 0000000..62a3426
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/EntitiesTest.php
@@ -0,0 +1,54 @@
+<?php
+/** @file
+ * Tests for the QueryPath library.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+namespace QueryPath\Tests;
+require_once __DIR__ . '/TestCase.php';
+
+/**
+ * @ingroup querypath_tests
+ */
+class EntitiesTest extends TestCase {
+ public function testReplaceEntity() {
+ $entity = 'amp';
+ $this->assertEquals('38', \QueryPath\Entities::replaceEntity($entity));
+
+ $entity = 'lceil';
+ $this->assertEquals('8968', \QueryPath\Entities::replaceEntity($entity));
+ }
+
+ public function testReplaceAllEntities() {
+ $test = '<?xml version="1.0"?><root>&amp;&copy;&#38;& nothing.</root>';
+ $expect = '<?xml version="1.0"?><root>&#38;&#169;&#38;&#38; nothing.</root>';
+ $this->assertEquals($expect, \QueryPath\Entities::replaceAllEntities($test));
+
+ $test = '&&& ';
+ $expect = '&#38;&#38;&#38; ';
+ $this->assertEquals($expect, \QueryPath\Entities::replaceAllEntities($test));
+
+ $test = "&eacute;\n";
+ $expect = "&#233;\n";
+ $this->assertEquals($expect, \QueryPath\Entities::replaceAllEntities($test));
+ }
+
+ public function testReplaceHexEntities() {
+ $test = '&#xA9;';
+ $expect = '&#xA9;';
+ $this->assertEquals($expect, \QueryPath\Entities::replaceAllEntities($test));
+ }
+
+ public function testQPEntityReplacement() {
+ $test = '<?xml version="1.0"?><root>&amp;&copy;&#38;& nothing.</root>';
+ /*$expect = '<?xml version="1.0"?><root>&#38;&#169;&#38;&#38; nothing.</root>';*/
+ // We get this because the DOM serializer re-converts entities.
+ $expect = '<?xml version="1.0"?>
+<root>&amp;&#xA9;&amp;&amp; nothing.</root>';
+
+ $qp = qp($test, NULL, array('replace_entities' => TRUE));
+ // Interestingly, the XML serializer converts decimal to hex and ampersands
+ // to &amp;.
+ $this->assertEquals($expect, trim($qp->xml()));
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/ExtensionTest.php b/lib/querypath/test/Tests/QueryPath/ExtensionTest.php
new file mode 100644
index 0000000..5f98612
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/ExtensionTest.php
@@ -0,0 +1,153 @@
+<?php
+/**
+ * Tests for the QueryPath library.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+namespace QueryPath\Tests;
+//require_once 'PHPUnit/Autoload.php';
+require_once __DIR__ . '/TestCase.php';
+require_once __DIR__ . '/../../../src/QueryPath/Extension.php';
+//require_once __DIR__ . '/../../../src/QueryPath.php';
+//require_once 'QueryPathTest.php';
+
+use \QueryPath\Extension;
+use \QueryPath\ExtensionRegistry;
+
+/**
+ *
+ */
+//define('self::DATA_FILE', 'test/data.xml');
+
+/**
+ * Run all of the usual tests, plus some extras, with some extensions loaded.
+ * @ingroup querypath_tests
+ * @group extension
+ */
+class QueryPathExtensionTest extends TestCase {
+
+ public static function setUpBeforeClass() {
+ ExtensionRegistry::extend('\QueryPath\Tests\StubExtensionOne');
+ ExtensionRegistry::extend('\QueryPath\Tests\StubExtensionTwo');
+ }
+
+ public function testExtensions() {
+ $this->assertNotNull(qp());
+ }
+
+ public function testHasExtension() {
+ $this->assertTrue(ExtensionRegistry::hasExtension('\QueryPath\Tests\StubExtensionOne'));
+ }
+
+ public function testStubToe() {
+ $this->assertEquals(1, qp(self::DATA_FILE, 'unary')->stubToe()->top(':root > toe')->size());
+ }
+
+ public function testStuble() {
+ $this->assertEquals('arg1arg2', qp(self::DATA_FILE)->stuble('arg1', 'arg2'));
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testNoRegistry() {
+ ExtensionRegistry::$useRegistry = FALSE;
+ try {
+ qp(self::DATA_FILE)->stuble('arg1', 'arg2');
+ }
+ catch (\QueryPath\Exception $e) {
+ ExtensionRegistry::$useRegistry = TRUE;
+ throw $e;
+ }
+
+ }
+
+ public function testExtend() {
+ $this->assertFalse(ExtensionRegistry::hasExtension('\QueryPath\Tests\StubExtensionThree'));
+ ExtensionRegistry::extend('\QueryPath\Tests\StubExtensionThree');
+ $this->assertTrue(ExtensionRegistry::hasExtension('\QueryPath\Tests\StubExtensionThree'));
+ }
+
+ public function tearDown() {
+ ExtensionRegistry::$useRegistry = TRUE;
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testAutoloadExtensions() {
+ // FIXME: This isn't really much of a test.
+ ExtensionRegistry::autoloadExtensions(FALSE);
+ try {
+ qp()->stubToe();
+ }
+ catch (Exception $e) {
+ ExtensionRegistry::autoloadExtensions(TRUE);
+ throw $e;
+ }
+ }
+
+ /**
+ * @expectedException \QueryPath\Exception
+ */
+ public function testCallFailure() {
+ qp()->foo();
+ }
+
+ // This does not (and will not) throw an exception.
+ // /**
+ // * @expectedException QueryPathException
+ // */
+ // public function testExtendNoSuchClass() {
+ // ExtensionRegistry::extend('StubExtensionFour');
+ // }
+
+}
+// Create a stub extension:
+/**
+ * Create a stub extension
+ *
+ * @ingroup querypath_tests
+ */
+class StubExtensionOne implements Extension {
+ private $qp = NULL;
+ public function __construct(\QueryPath\Query $qp) {
+ $this->qp = $qp;
+ }
+
+ public function stubToe() {
+ $this->qp->top()->append('<toe/>')->end();
+ return $this->qp;
+ }
+}
+/**
+ * Create a stub extension
+ *
+ * @ingroup querypath_tests
+ */
+class StubExtensionTwo implements Extension {
+ private $qp = NULL;
+ public function __construct(\QueryPath\Query $qp) {
+ $this->qp = $qp;
+ }
+ public function stuble($arg1, $arg2) {
+ return $arg1 . $arg2;
+ }
+}
+/**
+ * Create a stub extension
+ *
+ * @ingroup querypath_tests
+ */
+class StubExtensionThree implements Extension {
+ private $qp = NULL;
+ public function __construct(\QueryPath\Query $qp) {
+ $this->qp = $qp;
+ }
+ public function stuble($arg1, $arg2) {
+ return $arg1 . $arg2;
+ }
+}
+
+//ExtensionRegistry::extend('StubExtensionOne');
+//ExtensionRegistry::extend('StubExtensionTwo');
diff --git a/lib/querypath/test/Tests/QueryPath/Extensions/QPXMLTest.php b/lib/querypath/test/Tests/QueryPath/Extensions/QPXMLTest.php
new file mode 100644
index 0000000..266003e
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/Extensions/QPXMLTest.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Tests for the QueryPath library.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+namespace QueryPath\Tests;
+
+//require_once 'PHPUnit/Autoload.php';
+require_once __DIR__ . '/../TestCase.php';
+require_once 'src/QueryPath/Extension/QPXML.php';
+/**
+ * @ingroup querypath_tests
+ * @group extension
+ */
+class QPXMLTests extends TestCase {
+
+ protected $file = './test/advanced.xml';
+ public static function setUpBeforeClass() {
+ \QueryPath::enable('\QueryPath\Extension\QPXML');
+ }
+
+ public function testCDATA() {
+ $this->assertEquals('This is a CDATA section.', qp($this->file, 'first')->cdata());
+
+ $msg = 'Another CDATA Section';
+ $this->assertEquals($msg, qp($this->file, 'second')->cdata($msg)->top()->find('second')->cdata());
+ }
+
+ public function testComment(){
+ $this->assertEquals('This is a comment.', trim(qp($this->file, 'root')->comment()));
+ $msg = "Message";
+ $this->assertEquals($msg, qp($this->file, 'second')->comment($msg)->top()->find('second')->comment());
+ }
+
+ public function testProcessingInstruction() {
+ $this->assertEquals('This is a processing instruction.', trim(qp($this->file, 'third')->pi()));
+ $msg = "Message";
+ $this->assertEquals($msg, qp($this->file, 'second')->pi('qp', $msg)->top()->find('second')->pi());
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/Extensions/QPXSLTest.php b/lib/querypath/test/Tests/QueryPath/Extensions/QPXSLTest.php
new file mode 100644
index 0000000..f0c5ed1
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/Extensions/QPXSLTest.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Tests for the QueryPath library.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+
+namespace QueryPath\Tests;
+
+//require_once 'PHPUnit/Autoload.php';
+require_once 'src/QueryPath/Extension/QPXSL.php';
+require_once __DIR__ . '/../TestCase.php';
+/**
+ * @ingroup querypath_tests
+ * @extension
+ */
+class QPXSLTests extends TestCase {
+
+ protected $file = './test/advanced.xml';
+
+ public static function setUpBeforeClass() {
+ \QueryPath::enable('\QueryPath\Extension\QPXSL');
+ }
+ public function testXSLT() {
+ // XML and XSLT taken from http://us.php.net/manual/en/xsl.examples-collection.php
+ // and then modified to be *actually welformed* XML.
+ $orig = '<?xml version="1.0"?><collection>
+ <cd>
+ <title>Fight for your mind</title>
+ <artist>Ben Harper</artist>
+ <year>1995</year>
+ </cd>
+ <cd>
+ <title>Electric Ladyland</title>
+ <artist>Jimi Hendrix</artist>
+ <year>1997</year>
+ </cd>
+ </collection>';
+
+ $template = '<?xml version="1.0"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+ <xsl:param name="owner" select="\'Nicolas Eliaszewicz\'"/>
+ <xsl:output method="html" encoding="iso-8859-1" indent="no"/>
+ <xsl:template match="collection">
+ <div>
+ Hey! Welcome to <xsl:value-of select="$owner"/>\'s sweet CD collection!
+ <xsl:apply-templates/>
+ </div>
+ </xsl:template>
+ <xsl:template match="cd">
+ <h1><xsl:value-of select="title"/></h1>
+ <h2>by <xsl:value-of select="artist"/> - <xsl:value-of select="year"/></h2>
+ <hr />
+ </xsl:template>
+ </xsl:stylesheet>
+ ';
+
+ $qp = qp($orig)->xslt($template);
+ $this->assertEquals(2, $qp->top('h1')->size(), 'Make sure that data was formatted');
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/OptionsTest.php b/lib/querypath/test/Tests/QueryPath/OptionsTest.php
new file mode 100644
index 0000000..d2efbc9
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/OptionsTest.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Tests for the QueryPath library.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+namespace QueryPath\Tests;
+require_once __DIR__ . '/TestCase.php';
+use \QueryPath\Options;
+
+/**
+ * @ingroup querypath_tests
+ */
+class OptionsTest extends TestCase {
+
+ public function testOptions() {
+ $expect = array('test1' => 'val1', 'test2' => 'val2');
+ $options = array('test1' => 'val1', 'test2' => 'val2');
+
+ Options::set($options);
+
+ $results = Options::get();
+ $this->assertEquals($expect, $results);
+
+ $this->assertEquals('val1', $results['test1']);
+ }
+
+ public function testQPOverrideOrder() {
+ $expect = array('test1' => 'val3', 'test2' => 'val2');
+ $options = array('test1' => 'val1', 'test2' => 'val2');
+
+ Options::set($options);
+ $qpOpts = qp(NULL, NULL, array('test1'=>'val3', 'replace_entities' => TRUE))->getOptions();
+
+ $this->assertEquals($expect['test1'], $qpOpts['test1']);
+ $this->assertEquals(TRUE, $qpOpts['replace_entities']);
+ $this->assertNull($qpOpts['parser_flags']);
+ $this->assertEquals($expect['test2'], $qpOpts['test2']);
+ }
+
+ public function testQPHas() {
+ $options = array('test1' => 'val1', 'test2' => 'val2');
+
+ Options::set($options);
+ $this->assertTrue(Options::has('test1'));
+ $this->assertFalse(Options::has('test3'));
+ }
+ public function testQPMerge() {
+ $options = array('test1' => 'val1', 'test2' => 'val2');
+ $options2 = array('test1' => 'val3', 'test4' => 'val4');
+
+ Options::set($options);
+ Options::merge($options2);
+
+ $results = Options::get();
+ $this->assertTrue(Options::has('test4'));
+ $this->assertEquals('val3', $results['test1']);
+ }
+
+}
diff --git a/lib/querypath/test/Tests/QueryPath/QueryPathTest.php b/lib/querypath/test/Tests/QueryPath/QueryPathTest.php
new file mode 100644
index 0000000..71fd225
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/QueryPathTest.php
@@ -0,0 +1,56 @@
+<?php
+namespace QueryPath\Tests;
+
+//require_once 'PHPUnit/Autoload.php';
+require_once __DIR__ . '/TestCase.php';
+//require_once __DIR__ . '/../../../src/qp.php';
+require_once __DIR__ . '/../../../src/QueryPath.php';
+
+class QueryPathTest extends TestCase {
+
+ public function testWith() {
+ $qp = \QueryPath::with(\QueryPath::XHTML_STUB);
+
+ $this->assertInstanceOf('\QueryPath\DOMQuery', $qp);
+
+ }
+
+ public function testWithHTML() {
+ $qp = \QueryPath::with(\QueryPath::HTML_STUB);
+
+ $this->assertInstanceOf('\QueryPath\DOMQuery', $qp);
+ }
+ public function testWithHTML5() {
+ $qp = \QueryPath::withHTML5(\QueryPath::HTML5_STUB);
+
+ $this->assertInstanceOf('\QueryPath\DOMQuery', $qp);
+ }
+
+ public function testWithXML() {
+ $qp = \QueryPath::with(\QueryPath::XHTML_STUB);
+
+ $this->assertInstanceOf('\QueryPath\DOMQuery', $qp);
+ }
+
+ public function testEnable() {
+ \QueryPath::enable('\QueryPath\Tests\DummyExtension');
+
+ $qp = \QueryPath::with(\QueryPath::XHTML_STUB);
+
+ $this->assertTrue($qp->grrrrrrr());
+
+ }
+
+}
+
+class DummyExtension implements \QueryPath\Extension {
+
+ public function __construct(\QueryPath\Query $qp) {
+ $this->qp = $qp;
+ }
+
+ public function grrrrrrr() {
+ return TRUE;
+ }
+
+}
diff --git a/lib/querypath/test/Tests/QueryPath/TestCase.php b/lib/querypath/test/Tests/QueryPath/TestCase.php
new file mode 100644
index 0000000..7e212b0
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/TestCase.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * @file
+ *
+ * The master test case.
+ */
+
+namespace QueryPath\Tests;
+
+//require_once 'PHPUnit/Autoload.php';
+//require_once __DIR__ . '/../../../src/qp.php';
+require __DIR__ . '/../../../vendor/autoload.php';
+
+class TestCase extends \PHPUnit_Framework_TestCase {
+ const DATA_FILE = 'test/data.xml';
+ public static function setUpBeforeClass(){
+ }
+
+ public function testFoo() {
+ // Placeholder. Why is PHPUnit emitting warnings about no tests?
+ }
+}
diff --git a/lib/querypath/test/Tests/QueryPath/XMLIshTest.php b/lib/querypath/test/Tests/QueryPath/XMLIshTest.php
new file mode 100644
index 0000000..c55810f
--- /dev/null
+++ b/lib/querypath/test/Tests/QueryPath/XMLIshTest.php
@@ -0,0 +1,63 @@
+<?php
+/** @file
+ * Tests for the QueryPath library.
+ *
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+
+namespace QueryPath\Tests;
+
+/** @addtogroup querypath_tests Tests
+ * Unit tests and regression tests for QueryPath.
+ */
+
+/** */
+//require_once 'PHPUnit/Autoload.php';
+require_once __DIR__ . '/TestCase.php';
+
+/**
+ * Test the XMLish functions of QueryPath.
+ *
+ * This uses a testing harness, XMLishMock, to test
+ * a protected method of QueryPath.
+ *
+ * @ingroup querypath_test
+ */
+class XMLishTest extends TestCase {
+ public function testXMLishMock() {
+ $tests = array(
+ 'this/is/a/path' => FALSE,
+ "this is just some plain\ntext with a line break." => FALSE,
+ '2 > 1' => FALSE,
+ '1 < 2' => FALSE,
+ //'1 < 2 > 1' => FALSE,
+ '<html/>' => TRUE,
+ '<?xml version="1.0"?><root/>' => TRUE,
+ '<tag/><tag/><tag/>' => TRUE, // It's not valid, but HTML parser will try it.
+ );
+ foreach ($tests as $test => $correct) {
+ $mock = new XMLishMock();
+ $this->assertEquals($correct, $mock->exposedIsXMLish($test), "Testing $test");
+ }
+ }
+
+ public function testXMLishWithBrokenHTML() {
+ $html = '<div id="qp-top"><div class=header>Abe H. Rosenbloom Field<br></div> <p> Located in a natural bowl north of 10th Avenue, Rosenbloom Field was made possible by a gift from Virginia Whitney Rosenbloom \'36 and Abe H. Rosenbloom \'34. The Pioneers observed the occasion of the field\'s dedication on Oct. 4, 1975, by defeating Carleton 36-26. Rosenbloom Field has a seating capacity of 1,500. <br> <br> A former member of the Grinnell Advisory Board and other college committees, Abe Rosenbloom played football at Grinnell from 1931 to 1933. He played guard and was one of the Missouri Valley Conference\'s smallest gridders (5\'6" and 170 pounds). He averaged more than 45 minutes a game playing time during a 24-game varsity career and was named to the Des Moines Register\'s all-Missouri Valley Conference squad in 1932 and 1933. <br> <br> On the south side of the field, a memorial recalls the 100th anniversary of the first intercollegiate football game played west of the Mississippi. The game took place on the Grinnell campus on Nov. 16, 1889. On the north side, a marker commemorates the first 50 years of football in the west, and recalls the same game, played in 1889, Grinnell College vs. the University of Iowa. Grinnell won, 24-0. </p></div>';
+ $mock = new XMLishMock();
+ $this->assertEquals(TRUE, $mock->exposedIsXMLish($html), "Testing broken HTML");
+ }
+
+}
+
+/**
+ * A testing class for XMLish tests.
+ *
+ * @ingroup querypath_tests
+ */
+class XMLishMock extends \QueryPath\DOMQuery {
+ public function exposedIsXMLish($str) {
+ return $this->isXMLish($str);
+ }
+}
diff --git a/lib/querypath/test/advanced.xml b/lib/querypath/test/advanced.xml
new file mode 100644
index 0000000..e6c2eb2
--- /dev/null
+++ b/lib/querypath/test/advanced.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<root>
+ <first>
+ <![CDATA[This is a CDATA section.]]>
+ </first>
+ <second>
+ <inner/>
+ <inner/>
+ </second>
+ <third>
+ <?pi This is a processing instruction. ?>
+ </third>
+ <!-- This is a comment. -->
+</root> \ No newline at end of file
diff --git a/lib/querypath/test/amplify.xml b/lib/querypath/test/amplify.xml
new file mode 100644
index 0000000..70638b0
--- /dev/null
+++ b/lib/querypath/test/amplify.xml
@@ -0,0 +1,4370 @@
+<?xml version="1.0"?>
+<ns1:AmplifyResponse xmlns:ns1="http://amplify.hapax.com">
+ <!--
+ This document was retrieved from OpenAmplify.
+ It is here to represent a moderately sized document for testing.
+ -->
+ <AmplifyReturn>
+ <Topics>
+ <Domains>
+ <DomainResult>
+ <Domain>
+ <Name>Computers</Name>
+ <Value>18.000000</Value>
+ </Domain>
+ <Subdomains>
+ <SubdomainResult>
+ <Subdomain>
+ <Name>Computer programming</Name>
+ <Value>11.000000</Value>
+ </Subdomain>
+ <Scores>
+ <TopicResult>
+ <Topic>
+ <Name>debugger</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>script</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ </Scores>
+ </SubdomainResult>
+ </Subdomains>
+ </DomainResult>
+ </Domains>
+ <TopTopics>
+ <TopicResult>
+ <Topic>
+ <Name>RSS</Name>
+ <Value>53.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.666667</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>feed</Name>
+ <Value>31.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.250000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.511993</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Apache</Name>
+ <Value>31.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.378744</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.845782</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>IE</Name>
+ <Value>29.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.200000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Xmlhttprequest</Name>
+ <Value>21.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.900000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.050000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>mime type</Name>
+ <Value>20.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.220367</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.738899</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Drupal</Name>
+ <Value>19.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.898987</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>XML</Name>
+ <Value>15.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.200000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>object</Name>
+ <Value>15.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>request</Name>
+ <Value>14.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.150000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Javascript</Name>
+ <Value>10.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>mod_rewrite</Name>
+ <Value>9.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.233333</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>AJAX</Name>
+ <Value>9.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>error</Name>
+ <Value>8.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.795959</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.795959</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>application</Name>
+ <Value>8.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.900000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rewrite rule</Name>
+ <Value>8.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.859900</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.859900</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>IE Xmlhttprequest</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Firefox</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>server</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.250000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.125000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Technosophos</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>header</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>data inline</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>debugger</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>line</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>configuration</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>jQuery</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>header x-requested-with</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>2.x</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>widget</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Content-type</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>HTTP</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Jscript</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>XMLHttpRequest</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>X-requested-with</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>xml</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.600000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.600000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>research</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>htaccess file</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>ie javascript jquery mime mod_rewrite programming rss xmlhttprequest</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>javascript jquery mac mamp os x pecl</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>tag</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>safari</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.600000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.600000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>matt blog</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>code</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>script</Name>
+ <Value>5.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>other</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.250000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.250000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>web server</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>symptom</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.100508</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>solution</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.347939</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.347939</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>server restart</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>+ xml</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>display area</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Querypath Twitterpated IE Xmlhttprequest</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>RSS Javascript</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.150000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>behavior</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Aleph-null</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>webserver</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.900000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.613015</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Http:x-requested-with</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Rewriterule</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Solution</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>T=text</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Webclips</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rss+xml</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>^</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>^XMLHttpRequest $</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>aleph-null.tv</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.150000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>apache drupal</Name>
+ <Value>2.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>occasion</Name>
+ <Value>2.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>text</Name>
+ <Value>2.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>configuration file</Name>
+ <Value>2.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>matt</Name>
+ <Value>2.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>failure</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.100508</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>extension rss</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.300000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.300000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>jQuery use jQuery</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>English</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>description</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.465534</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.465534</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>content type setting</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>line turn</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>client</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>mime type text</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.075000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rss|xml|rdf</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rss+xml mime type</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>box</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.347939</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.347939</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rewrite engine</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rescue</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.845782</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.845782</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>rdf</Name>
+ <Value>1.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.600000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.600000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ </TopTopics>
+ <ProperNouns>
+ <TopicResult>
+ <Topic>
+ <Name>RSS</Name>
+ <Value>53.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.666667</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Apache</Name>
+ <Value>31.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.300000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.378744</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.845782</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>IE</Name>
+ <Value>29.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.200000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Xmlhttprequest</Name>
+ <Value>21.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.900000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-0.050000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Drupal</Name>
+ <Value>19.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.898987</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>1.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>XML</Name>
+ <Value>15.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.400000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.200000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.800000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Javascript</Name>
+ <Value>10.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.066667</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>AJAX</Name>
+ <Value>9.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>IE Xmlhttprequest</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Firefox</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Negative</Name>
+ <Value>-1.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Technosophos</Name>
+ <Value>7.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>HTTP</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Jscript</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.397979</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Content-type</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>XMLHttpRequest</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>X-requested-with</Name>
+ <Value>6.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Querypath Twitterpated IE Xmlhttprequest</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Mean>
+ <Max>
+ <Name>Positive</Name>
+ <Value>0.500000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>RSS Javascript</Name>
+ <Value>4.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Negative</Name>
+ <Value>-0.150000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Aleph-null</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Webclips</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>T=text</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Solution</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Rewriterule</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ <TopicResult>
+ <Topic>
+ <Name>Http:x-requested-with</Name>
+ <Value>3.000000</Value>
+ </Topic>
+ <Polarity>
+ <Min>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Min>
+ <Mean>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Mean>
+ <Max>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Max>
+ </Polarity>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </TopicResult>
+ </ProperNouns>
+ <Locations/>
+ </Topics>
+ <Actions>
+ <Scores>
+ <ActionResult>
+ <Action>
+ <Name>feed TechnoSophos</Name>
+ <Value>4.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>not work</Name>
+ <Value>4.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>write jQuery</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>work with other webservers</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium Low</Name>
+ <Value>2.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>turn out</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>try the feed</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>serve out its RSS feed</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>help</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>send the X-Requested-With</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>report problems</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>communicate</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>replicate the error</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>refine the problem</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium Low</Name>
+ <Value>1.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>NA</Name>
+ <Value>0.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>put in Apache's configuration file</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium Low</Name>
+ <Value>1.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>A Lot</Name>
+ <Value>3.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>parse the contents</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>not use</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>use</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>not treat files</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>match those requests</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium Low</Name>
+ <Value>1.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>adjust the MIME type</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium</Name>
+ <Value>3.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>define by IE's XMLHTTPRequest object</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium</Name>
+ <Value>2.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>emit no errors</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>feed</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>find the problem</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>learn</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>learn</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>get from the server</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>fix</Name>
+ <Value>3.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>be the problem</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>work out of the box</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium</Name>
+ <Value>2.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>work</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>confirm for me</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>communicate</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>load up to my server</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>submit ie javascript jquery mime mod_rewrite programming rss xmlhttprequest</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>communicate</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>spit out RSS files</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>set the Content-type</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium Low</Name>
+ <Value>2.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>NA</Name>
+ <Value>0.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>devise out a solution</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Medium</Name>
+ <Value>2.500000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>download</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>require a server restart</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>request</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Future</Name>
+ <Value>4.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>download their feeds</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Past</Name>
+ <Value>1.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>label drupal javascript jquery mac mamp os x pecl</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>handle the next part</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>feed ie javascript jquery mime mod_rewrite programming rss xmlhttprequest</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ <ActionResult>
+ <Action>
+ <Name>not work IE 7</Name>
+ <Value>2.000000</Value>
+ </Action>
+ <ActionType>
+ <Result>
+ <Name>other</Name>
+ <Value>-1.000000</Value>
+ </Result>
+ </ActionType>
+ <Decisiveness>
+ <Name>Low</Name>
+ <Value>1.000000</Value>
+ </Decisiveness>
+ <Temporality>
+ <TemporalityResult>
+ <Name>Present</Name>
+ <Value>3.000000</Value>
+ </TemporalityResult>
+ </Temporality>
+ <OfferingGuidance>
+ <Name>To Some Extent</Name>
+ <Value>2.000000</Value>
+ </OfferingGuidance>
+ <RequestingGuidance>
+ <Name>Not At All</Name>
+ <Value>1.000000</Value>
+ </RequestingGuidance>
+ </ActionResult>
+ </Scores>
+ </Actions>
+ <Demographics>
+ <Age>
+ <Name>Adult</Name>
+ <Value>-0.007067</Value>
+ </Age>
+ <Gender>
+ <Name>Neutral</Name>
+ <Value>0.000000</Value>
+ </Gender>
+ <Education>
+ <Name>Secondary</Name>
+ <Value>2.000000</Value>
+ </Education>
+ </Demographics>
+ <Styles>
+ <Slang>
+ <Name>No Slang</Name>
+ <Value>0.114679</Value>
+ </Slang>
+ <Flamboyance>
+ <Name>Not Very Flamboyant</Name>
+ <Value>2.000000</Value>
+ </Flamboyance>
+ </Styles>
+ </AmplifyReturn>
+</ns1:AmplifyResponse>
diff --git a/lib/querypath/test/coverage.sh b/lib/querypath/test/coverage.sh
new file mode 100644
index 0000000..b4d7316
--- /dev/null
+++ b/lib/querypath/test/coverage.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+phpunit=/Applications/MAMP/bin/php5/bin/phpunit
+$phpunit --coverage-html coverage Tests
+rm "db/qpTest.db"
+rm "db/qpTest2.db" \ No newline at end of file
diff --git a/lib/querypath/test/data.html b/lib/querypath/test/data.html
new file mode 100644
index 0000000..85d15c5
--- /dev/null
+++ b/lib/querypath/test/data.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>data</title>
+ <meta name="generator" content="TextMate http://macromates.com/">
+ <meta name="author" content="Matthew Butcher">
+ <!-- Date: 2008-12-17 -->
+</head>
+<body>
+ <h1 id="title">This is the title</h1>
+</body>
+</html>
diff --git a/lib/querypath/test/data.xml b/lib/querypath/test/data.xml
new file mode 100644
index 0000000..b4eec32
--- /dev/null
+++ b/lib/querypath/test/data.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" ?>
+<root id="docRoot">
+ <head id="head"></head>
+ <unary id="unary"/>
+ <inner id="inner-one" class="innerClass">
+ <li id="one">Hello</li>
+ <li id="two"></li>
+ <li id="three"></li>
+ <li id="four">Last</li>
+ </inner>
+ <inner id="inner-two">
+ <li id="five"/>
+ </inner>
+ <foot id="footer">This is footer text.</foot>
+</root> \ No newline at end of file
diff --git a/lib/querypath/test/html.xml b/lib/querypath/test/html.xml
new file mode 100644
index 0000000..2d014b6
--- /dev/null
+++ b/lib/querypath/test/html.xml
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>data</title>
+ <meta name="generator" content="TextMate http://macromates.com/">
+ <meta name="author" content="Matthew Butcher">
+ <!-- Date: 2008-12-17 -->
+</head>
+<body>
+ <h1 id="title">This is the title</h1>
+</body>
+</html> \ No newline at end of file
diff --git a/lib/querypath/test/no-writing.xml b/lib/querypath/test/no-writing.xml
new file mode 100644
index 0000000..f4e5164
--- /dev/null
+++ b/lib/querypath/test/no-writing.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0"?>
+<foo/>
diff --git a/lib/querypath/test/runTests.sh b/lib/querypath/test/runTests.sh
new file mode 100755
index 0000000..1868c66
--- /dev/null
+++ b/lib/querypath/test/runTests.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+phpunit=/Applications/MAMP/bin/php5/bin/phpunit
+cd ..
+$phpunit test/Tests/
+cd -
+rm "db/qpTest.db"
+rm "db/qpTest2.db"
diff --git a/lib/querypath/test/test.php b/lib/querypath/test/test.php
new file mode 100755
index 0000000..a5fdd32
--- /dev/null
+++ b/lib/querypath/test/test.php
@@ -0,0 +1,153 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Generic CLI parser tests.
+ *
+ * These are not unit tests. They are just plain parser tests.
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license The GNU Lesser GPL (LGPL) or an MIT-like license.
+ */
+require '../src/QueryPath/QueryPath.php';
+//$str = 'abc > def.g |: hi(jk)[lmn]*op';
+//$str = '&abc.def';
+
+/**
+ * Testing harness for the CssEventHandler.
+ * @ingroup querypath_tests
+ */
+class SimpleTestCssEventHandler implements CssEventHandler {
+ var $stack = NULL;
+ var $expect = array();
+
+ public function __construct() {
+ $this->stack = array();
+ }
+
+ public function getStack() {
+ return $this->stack;
+ }
+
+ public function dumpStack() {
+ print "\nExpected:\n";
+ $format = "Element %d: %s\n";
+ foreach ($this->expect as $item) {
+ printf($format, $item->eventType(), implode(',', $item->params()));
+ }
+
+ print "Got:\n";
+ foreach($this->stack as $item){
+ printf($format, $item->eventType(), implode(',', $item->params()));
+ }
+ }
+
+ public function expects($stack) {
+ $this->expect = $stack;
+ }
+
+ public function success() {
+ return ($this->expect == $this->stack);
+ }
+
+ public function elementID($id) {
+ $this->stack[] = new TestEvent(TestEvent::elementID, $id);
+ }
+ public function element($name) {
+ $this->stack[] = new TestEvent(TestEvent::element, $name);
+ }
+ public function elementNS($name, $namespace = NULL){
+ $this->stack[] = new TestEvent(TestEvent::elementNS, $name, $namespace);
+ }
+ public function anyElement(){
+ $this->stack[] = new TestEvent(TestEvent::anyElement);
+ }
+ public function anyElementInNS($ns){
+ $this->stack[] = new TestEvent(TestEvent::anyElementInNS, $ns);
+ }
+ public function elementClass($name){
+ $this->stack[] = new TestEvent(TestEvent::elementClass, $name);
+ }
+ public function attribute($name, $value = NULL, $operation = CssEventHandler::isExactly){
+ $this->stack[] = new TestEvent(TestEvent::attribute, $name, $value, $operation);
+ }
+ public function attributeNS($name, $ns, $value = NULL, $operation = CssEventHandler::isExactly){
+ $this->stack[] = new TestEvent(TestEvent::attributeNS, $name, $ns, $value, $operation);
+ }
+ public function pseudoClass($name, $value = NULL){
+ $this->stack[] = new TestEvent(TestEvent::pseudoClass, $name, $value);
+ }
+ public function pseudoElement($name){
+ $this->stack[] = new TestEvent(TestEvent::pseudoElement, $name);
+ }
+ public function directDescendant(){
+ $this->stack[] = new TestEvent(TestEvent::directDescendant);
+ }
+ public function anyDescendant() {
+ $this->stack[] = new TestEvent(TestEvent::anyDescendant);
+ }
+ public function adjacent(){
+ $this->stack[] = new TestEvent(TestEvent::adjacent);
+ }
+ public function anotherSelector(){
+ $this->stack[] = new TestEvent(TestEvent::anotherSelector);
+ }
+ public function sibling(){
+ $this->stack[] = new TestEvent(TestEvent::sibling);
+ }
+}
+
+/**
+ * Simple utility object for use with the TestCssEventHandler.
+ * @ingroup querypath_tests
+ */
+class TestEvent {
+ const elementID = 0;
+ const element = 1;
+ const elementNS = 2;
+ const anyElement = 3;
+ const elementClass = 4;
+ const attribute = 5;
+ const attributeNS = 6;
+ const pseudoClass = 7;
+ const pseudoElement = 8;
+ const directDescendant = 9;
+ const adjacent = 10;
+ const anotherSelector = 11;
+ const sibling = 12;
+ const anyElementInNS = 13;
+ const anyDescendant = 14;
+
+ var $type = NULL;
+ var $params = NULL;
+
+ public function __construct($event_type) {
+
+
+
+ $this->type = $event_type;
+ $args = func_get_args();
+ array_shift($args);
+ $this->params = $args;
+
+ print "Event " . $event_type;
+ print_r($args);
+ }
+
+ public function eventType() {
+ return $this->type;
+ }
+
+ public function params() {
+ return $this->params;
+ }
+}
+
+print ord('"');
+#$str = 'tag.class #id :test (test) + anotherElement > yetAnother[test] more[test="ing"]';
+$str = 'tag.class #id :test (test)';
+print "Now testing: $str\n";
+
+$c = new SimpleTestCssEventHandler();
+
+$p = new CssParser($str, $c);
+$p->parse();
+
diff --git a/providers/Motorsport.php b/providers/Motorsport.php
new file mode 100644
index 0000000..f055ad6
--- /dev/null
+++ b/providers/Motorsport.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Providers;
+
+require_once('Twitter.php');
+require_once('Item.php');
+require_once('../lib/querypath/src/qp.php');
+
+class Motorsport extends \Providers\Twitter {
+
+ protected $_cacheTimeout = '15 minutes';
+
+ public function __construct($feed, $options=[]) {
+ parent::__construct($feed, $options);
+ }
+
+ protected function _getCachePath() {
+ return '../cache/motorsport.%s';
+ }
+
+ protected function _fetchItems() {
+ $content = parent::_fetchItems();
+ $links = [];
+ foreach ($content as $item) {
+ foreach ($item->entities->urls as $url) {
+ $links[$url->expanded_url] = $item;
+ }
+ }
+ $links = array_filter(
+ $links,
+ function($link) {
+ $linkParts = parse_url($link);
+ return $linkParts['host'] != 'twitter.com';
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ $content = [];
+ foreach ($links as $link => $i) {
+ $item = new \stdClass();
+ $item->title = $i->text;
+ $item->time = $i->created_at;
+ $item->link = $link;
+ if (isset($i->user)) {
+ $item->author = $i->user->screen_name;
+ }
+ $item->content = $this->_getLinkMetadata($this->_getLinkContent($link));
+ $content[] = $item;
+ }
+ return $content;
+ }
+
+ protected function _getLinkContent($link) {
+ $cacheHash = md5($link);
+ $cacheFile = sprintf($this->_getCachePath() . '.%s', $this->_feed, $cacheHash);
+ if (file_exists($cacheFile)) {
+ return file_get_contents($cacheFile);
+ } else {
+ $content = file_get_contents($link);
+ file_put_contents($cacheFile, $content);
+ return $content;
+ }
+ }
+
+ protected function _getLinkMetadata($content) {
+ $tree = htmlqp($content);
+ $meta = $tree->find('meta');
+ $metadata = [];
+ foreach ($meta as $tag) {
+ $attributes = $tag->attr();
+ if (isset($attributes['property'])) {
+ $metadata[$attributes['property']] = $attributes['content'];
+ }
+ }
+ if (isset($metadata['og:url'])) {
+ $originalUrl = parse_url($metadata['og:url']);
+ if (substr($originalUrl['host'], -22) != 'motorsportmagazine.com') {
+ unset($metadata['og:url']);
+ }
+ }
+ return $metadata;
+ }
+
+ protected function _spamFilter($items) {
+ return array_filter(
+ $items,
+ function ($item) {
+ return isset($item->content['article:published_time']);
+ }
+ );
+ }
+
+ protected function _mapItems($content) {
+ $items = [];
+ foreach ($content as $i) {
+ $url = isset($i->content['og:url']) ? $i->content['og:url'] : $i->link;
+ $item = new Item();
+ $item->ID = md5($url);
+ $item->Title = isset($i->content['article:published_time']) ? $i->content['og:title'] : $i->title;
+ $item->Link = $url;
+ $item->Text = isset($i->content['article:published_time']) ? $i->content['og:description'] : $i->content['og:title'];
+ $item->Time = isset($i->content['article:published_time']) ? $i->content['article:published_time'] : $i->time;
+ if (isset($i->author)) {
+ $item->Author = $i->author;
+ }
+ $items[] = $item;
+ }
+ return $items;
+ }
+
+ protected function _sortContent($content) {
+ return $content;
+ }
+
+}
+
+?>