summaryrefslogtreecommitdiff
path: root/lib/querypath/Extension
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2016-10-11 14:01:29 +0200
committeremkael <emkael@tlen.pl>2016-10-11 14:01:29 +0200
commit51609351f2c4b5082b7e6f0744cd3811c325303f (patch)
tree739015e9ec69bc185ebe30db21369ae0b8b692ce /lib/querypath/Extension
parent8d1b0dad63e3906efa9393ef01d08b77d83417b5 (diff)
* initial template
Diffstat (limited to 'lib/querypath/Extension')
-rw-r--r--lib/querypath/Extension/QPDB.php711
-rw-r--r--lib/querypath/Extension/QPList.php213
-rw-r--r--lib/querypath/Extension/QPTPL.php275
-rw-r--r--lib/querypath/Extension/QPXML.php209
-rw-r--r--lib/querypath/Extension/QPXSL.php75
5 files changed, 1483 insertions, 0 deletions
diff --git a/lib/querypath/Extension/QPDB.php b/lib/querypath/Extension/QPDB.php
new file mode 100644
index 0000000..1a41657
--- /dev/null
+++ b/lib/querypath/Extension/QPDB.php
@@ -0,0 +1,711 @@
+<?php
+/** @file
+ * This package contains classes for handling database transactions from
+ * within QueryPath.
+ *
+ * The tools here use the PDO (PHP Data Objects) library to execute database
+ * functions.
+ *
+ * Using tools in this package, you can write QueryPath database queries
+ * that query an RDBMS and then insert the results into the document.
+ *
+ * Example:
+ *
+ * @code
+ * <?php
+ * $template = '<?xml version="1.0"?><tr><td class="colOne"/><td class="colTwo"/><td class="colThree"/></tr>';
+ * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>
+ * ->append('<table><tbody/></table>')
+ * ->dbInit($this->dsn)
+ * ->queryInto('SELECT * FROM qpdb_test WHERE 1', array(), $template)
+ * ->doneWithQuery()
+ * ->writeHTML();
+ * ?>
+ * @endcode
+ *
+ * The code above will take the results of a SQL query and insert them into a n
+ * HTML table.
+ *
+ * If you are doing many database operations across multiple QueryPath objects,
+ * it is better to avoid using {@link QPDB::dbInit()}. Instead, you should
+ * call the static {@link QPDB::baseDB()} method to configure a single database
+ * connection that can be shared by all {@link QueryPath} instances.
+ *
+ * Thus, we could rewrite the above to look like this:
+ * @code
+ * <?php
+ * QPDB::baseDB($someDN);
+ *
+ * $template = '<?xml version="1.0"?><tr><td class="colOne"/><td class="colTwo"/><td class="colThree"/></tr>';
+ * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>
+ * ->append('<table><tbody/></table>')
+ * ->queryInto('SELECT * FROM qpdb_test WHERE 1', array(), $template)
+ * ->doneWithQuery()
+ * ->writeHTML();
+ * ?>
+ * @endcode
+ *
+ * Note that in this case, the QueryPath object doesn't need to call a method to
+ * activate the database. There is no call to {@link dbInit()}. Instead, it checks
+ * the base class to find the shared database.
+ *
+ * (Note that if you were to add a dbInit() call to the above, it would create
+ * a new database connection.)
+ *
+ * The result of both of these examples will be identical.
+ * The output looks 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>Untitled</title>
+ * </head>
+ *<body>
+ *<table>
+ * <tbody>
+ * <tr>
+ * <td class="colOne">Title 0</td>
+ * <td class="colTwo">Body 0</td>
+ * <td class="colThree">Footer 0</td>
+ * </tr>
+ * <tr>
+ * <td class="colOne">Title 1</td>
+ * <td class="colTwo">Body 1</td>
+ * <td class="colThree">Footer 1</td>
+ * </tr>
+ * <tr>
+ * <td class="colOne">Title 2</td>
+ * <td class="colTwo">Body 2</td>
+ * <td class="colThree">Footer 2</td>
+ * </tr>
+ * <tr>
+ * <td class="colOne">Title 3</td>
+ * <td class="colTwo">Body 3</td>
+ * <td class="colThree">Footer 3</td>
+ * </tr>
+ * <tr>
+ * <td class="colOne">Title 4</td>
+ * <td class="colTwo">Body 4</td>
+ * <td class="colThree">Footer 4</td>
+ * </tr>
+ * </tbody>
+ *</table>
+ *</body>
+ *</html>
+ * @endcode
+ *
+ * Note how the CSS classes are used to correlate DB table names to template
+ * locations.
+ *
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
+ * @see QueryPathExtension
+ * @see QueryPathExtensionRegistry::extend()
+ * @see QPDB
+ */
+
+/**
+ * Provide DB access to a QueryPath object.
+ *
+ * This extension provides tools for communicating with a database using the
+ * QueryPath library. It relies upon PDO for underlying database communiction. This
+ * means that it supports all databases that PDO supports, including MySQL,
+ * PostgreSQL, and SQLite.
+ *
+ * Here is an extended example taken from the unit tests for this library.
+ *
+ * Let's say we create a database with code like this:
+ * @code
+ *<?php
+ * public function setUp() {
+ * $this->db = new PDO($this->dsn);
+ * $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ * $this->db->exec('CREATE TABLE IF NOT EXISTS qpdb_test (colOne, colTwo, colThree)');
+ *
+ * $stmt = $this->db->prepare(
+ * 'INSERT INTO qpdb_test (colOne, colTwo, colThree) VALUES (:one, :two, :three)'
+ * );
+ *
+ * for ($i = 0; $i < 5; ++$i) {
+ * $vals = array(':one' => 'Title ' . $i, ':two' => 'Body ' . $i, ':three' => 'Footer ' . $i);
+ * $stmt->execute($vals);
+ * $stmt->closeCursor();
+ * }
+ * }
+ * ?>
+ * @endcode
+ *
+ * From QueryPath with QPDB, we can now do very elaborate DB chains like this:
+ *
+ * @code
+ * <?php
+ * $sql = 'SELECT * FROM qpdb_test';
+ * $args = array();
+ * $qp = qp(QueryPath::HTML_STUB, 'body') // Open a stub HTML doc and select <body/>
+ * ->append('<h1></h1>') // Add <h1/>
+ * ->children() // Select the <h1/>
+ * ->dbInit($this->dsn) // Connect to the database
+ * ->query($sql, $args) // Execute the SQL query
+ * ->nextRow() // Select a row. By default, no row is selected.
+ * ->appendColumn('colOne') // Append Row 1, Col 1 (Title 0)
+ * ->parent() // Go back to the <body/>
+ * ->append('<p/>') // Append a <p/> to the body
+ * ->find('p') // Find the <p/> we just created.
+ * ->nextRow() // Advance to row 2
+ * ->prependColumn('colTwo') // Get row 2, col 2. (Body 1)
+ * ->columnAfter('colThree') // Get row 2 col 3. (Footer 1)
+ * ->doneWithQuery() // Let QueryPath clean up. YOU SHOULD ALWAYS DO THIS.
+ * ->writeHTML(); // Write the output as HTML.
+ * ?>
+ * @endcode
+ *
+ * With the code above, we step through the document, selectively building elements
+ * as we go, and then populating this elements with data from our initial query.
+ *
+ * When the last command, {@link QueryPath:::writeHTML()}, is run, we will get output
+ * 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" />
+ * <title>Untitled</title>
+ * </head>
+ * <body>
+ * <h1>Title 0</h1>
+ * <p>Body 1</p>
+ * Footer 1</body>
+ * </html>
+ * @endcode
+ *
+ * Notice the body section in particular. This is where the data has been
+ * inserted.
+ *
+ * Sometimes you want to do something a lot simpler, like give QueryPath a
+ * template and have it navigate a query, inserting the data into a template, and
+ * then inserting the template into the document. This can be done simply with
+ * the {@link queryInto()} function.
+ *
+ * Here's an example from another unit test:
+ *
+ * @code
+ * <?php
+ * $template = '<?xml version="1.0"?><li class="colOne"/>';
+ * $sql = 'SELECT * FROM qpdb_test';
+ * $args = array();
+ * $qp = qp(QueryPath::HTML_STUB, 'body')
+ * ->append('<ul/>') // Add a new <ul/>
+ * ->children() // Select the <ul/>
+ * ->dbInit($this->dsn) // Initialize the DB
+ * // BIG LINE: Query the results, run them through the template, and insert them.
+ * ->queryInto($sql, $args, $template)
+ * ->doneWithQuery()
+ * ->writeHTML(); // Write the results as HTML.
+ * ?>
+ * @endcode
+ *
+ * The simple code above puts the first column of the select statement
+ * into an unordered list. The example output looks 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>Untitled</title>
+ * </head>
+ * <body>
+ * <ul>
+ * <li class="colOne">Title 0</li>
+ * <li class="colOne">Title 1</li>
+ * <li class="colOne">Title 2</li>
+ * <li class="colOne">Title 3</li>
+ * <li class="colOne">Title 4</li>
+ * </ul>
+ * </body>
+ * </html>
+ * @endcode
+ *
+ * Typical starting methods for this class are {@link QPDB::baseDB()},
+ * {@link QPDB::query()}, and {@link QPDB::queryInto()}.
+ *
+ * @ingroup querypath_extensions
+ */
+class QPDB implements QueryPathExtension {
+ protected $qp;
+ protected $dsn;
+ protected $db;
+ protected $opts;
+ protected $row = NULL;
+ protected $stmt = NULL;
+
+ protected static $con = NULL;
+
+ /**
+ * Create a new database instance for all QueryPath objects to share.
+ *
+ * This method need be called only once. From there, other QPDB instances
+ * will (by default) share the same database instance.
+ *
+ * Normally, a DSN should be passed in. Username, password, and db params
+ * are all passed in using the options array.
+ *
+ * On rare occasions, it may be more fitting to pass in an existing database
+ * connection (which must be a {@link PDO} object). In such cases, the $dsn
+ * parameter can take a PDO object instead of a DSN string. The standard options
+ * will be ignored, though.
+ *
+ * <b>Warning:</b> If you pass in a PDO object that is configured to NOT throw
+ * exceptions, you will need to handle error checking differently.
+ *
+ * <b>Remember to always use {@link QPDB::doneWithQuery()} when you are done
+ * with a query. It gives PDO a chance to clean up open connections that may
+ * prevent other instances from accessing or modifying data.</b>
+ *
+ * @param string $dsn
+ * The DSN of the database to connect to. You can also pass in a PDO object, which
+ * will set the QPDB object's database to the one passed in.
+ * @param array $options
+ * An array of configuration options. The following options are currently supported:
+ * - username => (string)
+ * - password => (string)
+ * - db params => (array) These will be passed into the new PDO object.
+ * See the PDO documentation for a list of options. By default, the
+ * only flag set is {@link PDO::ATTR_ERRMODE}, which is set to
+ * {@link PDO::ERRMODE_EXCEPTION}.
+ * @throws PDOException
+ * An exception may be thrown if the connection cannot be made.
+ */
+ static function baseDB($dsn, $options = array()) {
+
+ $opts = $options + array(
+ 'username' => NULL,
+ 'password' => NULL,
+ 'db params' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+ );
+
+ // Allow this to handle the case where an outside
+ // connection does the initialization.
+ if ($dsn instanceof PDO) {
+ self::$con = $dsn;
+ return;
+ }
+ self::$con = new PDO($dsn, $opts['username'], $opts['password'], $opts['db params']);
+ }
+
+ /**
+ *
+ * This method may be used to share the connection with other,
+ * non-QueryPath objects.
+ */
+ static function getBaseDB() {return self::$con;}
+
+ /**
+ * Used to control whether or not all rows in a result should be cycled through.
+ */
+ protected $cycleRows = FALSE;
+
+ /**
+ * Construct a new QPDB object. This is usually done by QueryPath itself.
+ */
+ public function __construct(QueryPath $qp) {
+ $this->qp = $qp;
+ // By default, we set it up to use the base DB.
+ $this->db = self::$con;
+ }
+
+ /**
+ * Create a new connection to the database. Use the PDO DSN syntax for a
+ * connection string.
+ *
+ * This creates a database connection that will last for the duration of
+ * the QueryPath object. This method ought to be used only in two cases:
+ * - When you will only run a couple of queries during the life of the
+ * process.
+ * - When you need to connect to a database that will only be used for
+ * a few things.
+ * Otherwise, you should use {@link QPDB::baseDB} to configure a single
+ * database connection that all of {@link QueryPath} can share.
+ *
+ * <b>Remember to always use {@link QPDB::doneWithQuery()} when you are done
+ * with a query. It gives PDO a chance to clean up open connections that may
+ * prevent other instances from accessing or modifying data.</b>
+ *
+ * @param string $dsn
+ * The PDO DSN connection string.
+ * @param array $options
+ * Connection options. The following options are supported:
+ * - username => (string)
+ * - password => (string)
+ * - db params => (array) These will be passed into the new PDO object.
+ * See the PDO documentation for a list of options. By default, the
+ * only flag set is {@link PDO::ATTR_ERRMODE}, which is set to
+ * {@link PDO::ERRMODE_EXCEPTION}.
+ * @return QueryPath
+ * The QueryPath object.
+ * @throws PDOException
+ * The PDO library is configured to throw exceptions, so any of the
+ * database functions may throw a PDOException.
+ */
+ public function dbInit($dsn, $options = array()) {
+ $this->opts = $options + array(
+ 'username' => NULL,
+ 'password' => NULL,
+ 'db params' => array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION),
+ );
+ $this->dsn = $dsn;
+ $this->db = new PDO($dsn, $this->opts['username'], $this->opts['password'], $this->opts['db params']);
+ /*
+ foreach ($this->opts['db params'] as $key => $val)
+ $this->db->setAttribute($key, $val);
+ */
+
+ return $this->qp;
+ }
+
+ /**
+ * Execute a SQL query, and store the results.
+ *
+ * This will execute a SQL query (as a prepared statement), and then store
+ * the results internally for later use. The data can be iterated using
+ * {@link nextRow()}. QueryPath can also be instructed to do internal iteration
+ * using the {@link withEachRow()} method. Finally, on the occasion that the
+ * statement itself is needed, {@link getStatement()} can be used.
+ *
+ * Use this when you need to access the results of a query, or when the
+ * parameter to a query should be escaped. If the query takes no external
+ * parameters and does not return results, you may wish to use the
+ * (ever so slightly faster) {@link exec()} function instead.
+ *
+ * Make sure you use {@link doneWithQuery()} after finishing with the database
+ * results returned by this method.
+ *
+ * <b>Usage</b>
+ *
+ * Here is a simple example:
+ * <code>
+ * <?php
+ * QPQDB::baseDB($someDSN);
+ *
+ * $args = array(':something' => 'myColumn');
+ * qp()->query('SELECT :something FROM foo', $args)->doneWithQuery();
+ * ?>
+ * </code>
+ *
+ * The above would execute the given query, substituting myColumn in place of
+ * :something before executing the query The {@link doneWithQuery()} method
+ * indicates that we are not going to use the results for anything. This method
+ * discards the results.
+ *
+ * A more typical use of the query() function would involve inserting data
+ * using {@link appendColumn()}, {@link prependColumn()}, {@link columnBefore()},
+ * or {@link columnAfter()}. See the main documentation for {@link QPDB} to view
+ * a more realistic example.
+ *
+ * @param string $sql
+ * The query to be executed.
+ * @param array $args
+ * An associative array of substitutions to make.
+ * @throws PDOException
+ * Throws an exception if the query cannot be executed.
+ */
+ public function query($sql, $args = array()) {
+ $this->stmt = $this->db->prepare($sql);
+ $this->stmt->execute($args);
+ return $this->qp;
+ }
+
+ /**
+ * Query and append the results.
+ *
+ * Run a query and inject the results directly into the
+ * elements in the QueryPath object.
+ *
+ * If the third argument is empty, the data will be inserted directly into
+ * the QueryPath elements unaltered. However, if a template is provided in
+ * the third parameter, the query data will be merged into that template
+ * and then be added to each QueryPath element.
+ *
+ * The template will be merged once for each row, even if no row data is
+ * appended into the template.
+ *
+ * A template is simply a piece of markup labeled for insertion of
+ * data. See {@link QPTPL} and {@link QPTPL.php} for more information.
+ *
+ * Since this does not use a stanard {@link query()}, there is no need
+ * to call {@link doneWithQuery()} after this method.
+ *
+ * @param string $sql
+ * The SQL query to execute. In this context, the query is typically a
+ * SELECT statement.
+ * @param array $args
+ * An array of arguments to be substituted into the query. See {@link query()}
+ * for details.
+ * @param mixed $template
+ * A template into which query results will be merged prior to being appended
+ * into the QueryPath. For details on the template, see {@link QPTPL::tpl()}.
+ * @see QPTPL.php
+ * @see QPTPL::tpl()
+ * @see query()
+ */
+ public function queryInto($sql, $args = array(), $template = NULL) {
+ $stmt = $this->db->prepare($sql);
+ $stmt->setFetchMode(PDO::FETCH_ASSOC);
+ $stmt->execute($args);
+
+ // If no template, put all values in together.
+ if (empty($template)) {
+ foreach ($stmt as $row) foreach ($row as $datum) $this->qp->append($datum);
+ }
+ // Otherwise, we run the results through a template, and then append.
+ else {
+ foreach ($stmt as $row) $this->qp->tpl($template, $row);
+ }
+
+ $stmt->closeCursor();
+ return $this->qp;
+ }
+
+ /**
+ * Free up resources when a query is no longer used.
+ *
+ * This function should <i>always</i> be called when the database
+ * results for a query are no longer needed. This frees up the
+ * database cursor, discards the data, and resets resources for future
+ * use.
+ *
+ * If this method is not called, some PDO database drivers will not allow
+ * subsequent queries, while others will keep tables in a locked state where
+ * writes will not be allowed.
+ *
+ * @return QueryPath
+ * The QueryPath object.
+ */
+ public function doneWithQuery() {
+ if (isset($this->stmt) && $this->stmt instanceof PDOStatement) {
+ // Some drivers choke if results haven't been iterated.
+ //while($this->stmt->fetch()) {}
+ $this->stmt->closeCursor();
+ }
+
+ unset($this->stmt);
+ $this->row = NULL;
+ $this->cycleRows = FALSE;
+ return $this->qp;
+ }
+
+ /**
+ * Execute a SQL query, but expect no value.
+ *
+ * If your SQL query will have parameters, you are encouraged to
+ * use {@link query()}, which includes built-in SQL Injection
+ * protection.
+ *
+ * @param string $sql
+ * A SQL statement.
+ * @throws PDOException
+ * An exception will be thrown if a query cannot be executed.
+ */
+ public function exec($sql) {
+ $this->db->exec($sql);
+ return $this->qp;
+ }
+
+ /**
+ * Advance the query results row cursor.
+ *
+ * In a result set where more than one row was returned, this will
+ * move the pointer to the next row in the set.
+ *
+ * The PDO library does not have a consistent way of determining how many
+ * rows a result set has. The suggested technique is to first execute a
+ * COUNT() SQL query and get the data from that.
+ *
+ * The {@link withEachRow()} method will begin at the next row after the
+ * currently selected one.
+ *
+ * @return QueryPath
+ * The QueryPath object.
+ */
+ public function nextRow() {
+ $this->row = $this->stmt->fetch(PDO::FETCH_ASSOC);
+ return $this->qp;
+ }
+
+ /**
+ * Set the object to use each row, instead of only one row.
+ *
+ * This is used primarily to instruct QPDB to iterate through all of the
+ * rows when appending, prepending, inserting before, or inserting after.
+ *
+ * @return QueryPath
+ * The QueryPath object.
+ * @see appendColumn()
+ * @see prependColumn()
+ * @see columnBefore()
+ * @see columnAfter()
+ */
+ public function withEachRow() {
+ $this->cycleRows = TRUE;
+ return $this->qp;
+ }
+
+ /**
+ * This is the implementation behind the append/prepend and before/after methods.
+ *
+ * @param mixed $columnName
+ * The name of the column whose data should be added to the currently selected
+ * elements. This can be either a string or an array of strings.
+ * @param string $qpFunc
+ * The name of the QueryPath function that should be executed to insert data
+ * into the object.
+ * @param string $wrap
+ * The HTML/XML markup that will be used to wrap around the column data before
+ * the data is inserted into the QueryPath object.
+ */
+ protected function addData($columnName, $qpFunc = 'append', $wrap = NULL) {
+ $columns = is_array($columnName) ? $columnName : array($columnName);
+ $hasWrap = !empty($wrap);
+ if ($this->cycleRows) {
+ while (($row = $this->stmt->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
+ foreach ($columns as $col) {
+ if (isset($row[$col])) {
+ $data = $row[$col];
+ if ($hasWrap)
+ $data = qp()->append($wrap)->deepest()->append($data)->top();
+ $this->qp->$qpFunc($data);
+ }
+ }
+ }
+ $this->cycleRows = FALSE;
+ $this->doneWithQuery();
+ }
+ else {
+ if ($this->row !== FALSE) {
+ foreach ($columns as $col) {
+ if (isset($this->row[$col])) {
+ $data = $this->row[$col];
+ if ($hasWrap)
+ $data = qp()->append($wrap)->deepest()->append($data)->top();
+ $this->qp->$qpFunc($data);
+ }
+ }
+ }
+ }
+ return $this->qp;
+ }
+
+ /**
+ * Get back the raw PDOStatement object after a {@link query()}.
+ *
+ * @return PDOStatement
+ * Return the PDO statement object. If this is called and no statement
+ * has been executed (or the statement has already been cleaned up),
+ * this will return NULL.
+ */
+ public function getStatement() {
+ return $this->stmt;
+ }
+
+ /**
+ * Get the last insert ID.
+ *
+ * This will only return a meaningful result when used after an INSERT.
+ *
+ * @return mixed
+ * Return the ID from the last insert. The value and behavior of this
+ * is database-dependent. See the official PDO driver documentation for
+ * the database you are using.
+ * @since 1.3
+ */
+ public function getLastInsertID() {
+ $con = self::$con;
+ return $con->lastInsertId();
+ }
+
+ /**
+ * Append the data in the given column(s) to the QueryPath.
+ *
+ * This appends data to every item in the current QueryPath. The data will
+ * be retrieved from the database result, using $columnName as the key.
+ *
+ * @param mixed $columnName
+ * Either a string or an array of strings. The value(s) here should match
+ * one or more column headers from the current SQL {@link query}'s results.
+ * @param string $wrap
+ * IF this is supplied, then the value or values retrieved from the database
+ * will be wrapped in this HTML/XML before being inserted into the QueryPath.
+ * @see QueryPath::wrap()
+ * @see QueryPath::append()
+ */
+ public function appendColumn($columnName, $wrap = NULL) {
+ return $this->addData($columnName, 'append', $wrap);
+ }
+
+ /**
+ * Prepend the data from the given column into the QueryPath.
+ *
+ * This takes the data from the given column(s) and inserts it into each
+ * element currently found in the QueryPath.
+ * @param mixed $columnName
+ * Either a string or an array of strings. The value(s) here should match
+ * one or more column headers from the current SQL {@link query}'s results.
+ * @param string $wrap
+ * IF this is supplied, then the value or values retrieved from the database
+ * will be wrapped in this HTML/XML before being inserted into the QueryPath.
+ * @see QueryPath::wrap()
+ * @see QueryPath::prepend()
+ */
+ public function prependColumn($columnName, $wrap = NULL) {
+ return $this->addData($columnName, 'prepend', $wrap);
+ }
+
+ /**
+ * Insert the data from the given column before each element in the QueryPath.
+ *
+ * This inserts the data before each element in the currently matched QueryPath.
+ *
+ * @param mixed $columnName
+ * Either a string or an array of strings. The value(s) here should match
+ * one or more column headers from the current SQL {@link query}'s results.
+ * @param string $wrap
+ * IF this is supplied, then the value or values retrieved from the database
+ * will be wrapped in this HTML/XML before being inserted into the QueryPath.
+ * @see QueryPath::wrap()
+ * @see QueryPath::before()
+ * @see prependColumn()
+ */
+ public function columnBefore($columnName, $wrap = NULL) {
+ return $this->addData($columnName, 'before', $wrap);
+ }
+
+ /**
+ * Insert data from the given column(s) after each element in the QueryPath.
+ *
+ * This inserts data from the given columns after each element in the QueryPath
+ * object. IF HTML/XML is given in the $wrap parameter, then the column data
+ * will be wrapped in that markup before being inserted into the QueryPath.
+ *
+ * @param mixed $columnName
+ * Either a string or an array of strings. The value(s) here should match
+ * one or more column headers from the current SQL {@link query}'s results.
+ * @param string $wrap
+ * IF this is supplied, then the value or values retrieved from the database
+ * will be wrapped in this HTML/XML before being inserted into the QueryPath.
+ * @see QueryPath::wrap()
+ * @see QueryPath::after()
+ * @see appendColumn()
+ */
+ public function columnAfter($columnName, $wrap = NULL) {
+ return $this->addData($columnName, 'after', $wrap);
+ }
+
+}
+
+// The define allows another class to extend this.
+if (!defined('QPDB_OVERRIDE'))
+ QueryPathExtensionRegistry::extend('QPDB'); \ No newline at end of file
diff --git a/lib/querypath/Extension/QPList.php b/lib/querypath/Extension/QPList.php
new file mode 100644
index 0000000..87fb860
--- /dev/null
+++ b/lib/querypath/Extension/QPList.php
@@ -0,0 +1,213 @@
+<?php
+/** @file
+ * This extension provides support for common HTML list operations.
+ */
+
+/**
+ * Provide list operations for QueryPath.
+ *
+ * The QPList class is an extension to QueryPath. It provides HTML list generators
+ * that take lists and convert them into bulleted lists inside of QueryPath.
+ *
+ * @deprecated This will be removed from a subsequent version of QueryPath. It will
+ * be released as a stand-alone extension.
+ * @ingroup querypath_extensions
+ */
+class QPList implements QueryPathExtension {
+ const UL = 'ul';
+ const OL = 'ol';
+ const DL = 'dl';
+
+ protected $qp = NULL;
+ public function __construct(QueryPath $qp) {
+ $this->qp = $qp;
+ }
+
+ public function appendTable($items, $options = array()) {
+ $opts = $options + array(
+ 'table class' => 'qptable',
+ );
+ $base = '<?xml version="1.0"?>
+ <table>
+ <tbody>
+ <tr></tr>
+ </tbody>
+ </table>';
+
+ $qp = qp($base, 'table')->addClass($opts['table class'])->find('tr');
+ if ($items instanceof TableAble) {
+ $headers = $items->getHeaders();
+ $rows = $items->getRows();
+ }
+ elseif ($items instanceof Traversable) {
+ $headers = array();
+ $rows = $items;
+ }
+ else {
+ $headers = $items['headers'];
+ $rows = $items['rows'];
+ }
+
+ // Add Headers:
+ foreach ($headers as $header) {
+ $qp->append('<th>' . $header . '</th>');
+ }
+ $qp->top()->find('tr:last');
+
+ // Add rows and cells.
+ foreach ($rows as $row) {
+ $qp->after('<tr/>')->next();
+ foreach($row as $cell) $qp->append('<td>' . $cell . '</td>');
+ }
+
+ $this->qp->append($qp->top());
+
+ return $this->qp;
+ }
+
+ /**
+ * Append a list of items into an HTML DOM using one of the HTML list structures.
+ * This takes a one-dimensional array and converts it into an HTML UL or OL list,
+ * <b>or</b> it can take an associative array and convert that into a DL list.
+ *
+ * In addition to arrays, this works with any Traversable or Iterator object.
+ *
+ * OL/UL arrays can be nested.
+ *
+ * @param mixed $items
+ * An indexed array for UL and OL, or an associative array for DL. Iterator and
+ * Traversable objects can also be used.
+ * @param string $type
+ * One of ul, ol, or dl. Predefined constants are available for use.
+ * @param array $options
+ * An associative array of configuration options. The supported options are:
+ * - 'list class': The class that will be assigned to a list.
+ */
+ public function appendList($items, $type = self::UL, $options = array()) {
+ $opts = $options + array(
+ 'list class' => 'qplist',
+ );
+ if ($type == self::DL) {
+ $q = qp('<?xml version="1.0"?><dl></dl>', 'dl')->addClass($opts['list class']);
+ foreach ($items as $dt => $dd) {
+ $q->append('<dt>' . $dt . '</dt><dd>' . $dd . '</dd>');
+ }
+ $q->appendTo($this->qp);
+ }
+ else {
+ $q = $this->listImpl($items, $type, $opts);
+ $this->qp->append($q->find(':root'));
+ }
+
+ return $this->qp;
+ }
+
+ /**
+ * Internal recursive list generator for appendList.
+ */
+ protected function listImpl($items, $type, $opts, $q = NULL) {
+ $ele = '<' . $type . '/>';
+ if (!isset($q))
+ $q = qp()->append($ele)->addClass($opts['list class']);
+
+ foreach ($items as $li) {
+ if ($li instanceof QueryPath) {
+ $q = $this->listImpl($li->get(), $type, $opts, $q);
+ }
+ elseif (is_array($li) || $li instanceof Traversable) {
+ $q->append('<li><ul/></li>')->find('li:last > ul');
+ $q = $this->listImpl($li, $type, $opts, $q);
+ $q->parent();
+ }
+ else {
+ $q->append('<li>' . $li . '</li>');
+ }
+ }
+ return $q;
+ }
+
+ /**
+ * Unused.
+ */
+ protected function isAssoc($array) {
+ // A clever method from comment on is_array() doc page:
+ return count(array_diff_key($array, range(0, count($array) - 1))) != 0;
+ }
+}
+QueryPathExtensionRegistry::extend('QPList');
+
+/**
+ * A TableAble object represents tabular data and can be converted to a table.
+ *
+ * The {@link QPList} extension to {@link QueryPath} provides a method for
+ * appending a table to a DOM ({@link QPList::appendTable()}).
+ *
+ * Implementing classes should provide methods for getting headers, rows
+ * of data, and the number of rows in the table ({@link TableAble::size()}).
+ * Implementors may also choose to make classes Iterable or Traversable over
+ * the rows of the table.
+ *
+ * Two very basic implementations of TableAble are provided in this package:
+ * - {@link QPTableData} provides a generic implementation.
+ * - {@link QPTableTextData} provides a generic implementation that also escapes
+ * all data.
+ */
+interface TableAble {
+ public function getHeaders();
+ public function getRows();
+ public function size();
+}
+
+/**
+ * Format data to be inserted into a simple HTML table.
+ *
+ * Data in the headers or rows may contain markup. If you want to
+ * disallow markup, use a {@see QPTableTextData} object instead.
+ */
+class QPTableData implements TableAble, IteratorAggregate {
+
+ protected $headers;
+ protected $rows;
+ protected $caption;
+ protected $p = -1;
+
+ public function setHeaders($array) {$this->headers = $array; return $this;}
+ public function getHeaders() {return $this->headers; }
+ public function setRows($array) {$this->rows = $array; return $this;}
+ public function getRows() {return $this->rows;}
+ public function size() {return count($this->rows);}
+ public function getIterator() {
+ return new ArrayIterator($rows);
+ }
+}
+
+/**
+ * Provides a table where all of the headers and data are treated as text data.
+ *
+ * This provents marked-up data from being inserted into the DOM as elements.
+ * Instead, the text is escaped using {@see htmlentities()}.
+ *
+ * @see QPTableData
+ */
+class QPTableTextData extends QPTableData {
+ public function setHeaders($array) {
+ $headers = array();
+ foreach ($array as $header) {
+ $headers[] = htmlentities($header);
+ }
+ parent::setHeaders($headers);
+ return $this;
+ }
+ public function setRows($array) {
+ $count = count($array);
+ for ($i = 0; $i < $count; ++$i) {
+ $cols = array();
+ foreach ($data[$i] as $datum) {
+ $cols[] = htmlentities($datum);
+ }
+ $data[$i] = $cols;
+ }
+ parent::setRows($array);
+ return $this;
+ }
+} \ No newline at end of file
diff --git a/lib/querypath/Extension/QPTPL.php b/lib/querypath/Extension/QPTPL.php
new file mode 100644
index 0000000..6e47935
--- /dev/null
+++ b/lib/querypath/Extension/QPTPL.php
@@ -0,0 +1,275 @@
+<?php
+/** @file
+ * QueryPath templates. See QPTPL.
+ */
+/**
+ * QPTPL is a template library for QueryPath.
+ *
+ * The QPTPL extension provides template tools that can be used in
+ * conjunction with QueryPath.
+ *
+ * There are two basic modes in which this tool operates. Both merge data into
+ * a pure HTML template. Both base their insertions on classes and IDs in the
+ * HTML data. Where they differ is in the kind of data merged into the template.
+ *
+ * One mode takes array data and does a deep (recursive) merge into the template.
+ * It can be used for simple substitutions, but it can also be used to loop through
+ * "rows" of data and create tables.
+ *
+ * The second mode takes a classed object and introspects that object to find out
+ * what CSS classes it is capable of filling. This is one way of bridging an object
+ * model and QueryPath data.
+ *
+ * The unit tests are a good place for documentation, as is the QueryPath webste.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
+ * @see QueryPathExtension
+ * @see QueryPathExtensionRegistry::extend()
+ * @see https://fedorahosted.org/querypath/wiki/QueryPathTemplate
+ * @ingroup querypath_extensions
+ */
+class QPTPL implements QueryPathExtension {
+ protected $qp;
+ public function __construct(QueryPath $qp) {
+ $this->qp = $qp;
+ }
+
+ /**
+ * Apply a template to an object and then insert the results.
+ *
+ * This takes a template (an arbitrary fragment of XML/HTML) and an object
+ * or array and inserts the contents of the object into the template. The
+ * template is then appended to all of the nodes in the current list.
+ *
+ * Note that the data in the object is *not* escaped before it is merged
+ * into the template. For that reason, an object can return markup (as
+ * long as it is well-formed).
+ *
+ * @param mixed $template
+ * The template. It can be of any of the types that {@link qp()} supports
+ * natively. Typically it is a string of XML/HTML.
+ * @param mixed $object
+ * Either an object or an associative array.
+ * - In the case where the parameter
+ * is an object, this will introspect the object, looking for getters (a la
+ * Java bean behavior). It will then search the document for CSS classes
+ * that match the method name. The function is then executed and its contents
+ * inserted into the document. (If the function returns NULL, nothing is
+ * inserted.)
+ * - In the case where the paramter is an associative array, the function will
+ * look through the template for CSS classes that match the keys of the
+ * array. When an array key is found, the array value is inserted into the
+ * DOM as a child of the currently matched element(s).
+ * @param array $options
+ * The options for this function. Valid options are:
+ * - <None defined yet>
+ * @return QueryPath
+ * Returns a QueryPath object with all of the changes from the template
+ * applied into the QueryPath elements.
+ * @see QueryPath::append()
+ */
+ public function tpl($template, $object, $options = array()) {
+ // Handle default options here.
+
+ //$tqp = ($template instanceof QueryPath) ? clone $template: qp($template);
+ $tqp = qp($template);
+
+ if (is_array($object) || $object instanceof Traversable) {
+ $this->tplArrayR($tqp, $object, $options);
+ return $this->qp->append($tqp->top());
+ }
+ elseif (is_object($object)) {
+ $this->tplObject($tqp, $object, $options);
+ }
+
+ return $this->qp->append($tqp->top());
+ }
+
+ /**
+ * Given one template, do substitutions for all objects.
+ *
+ * Using this method, one template can be populated from a variety of
+ * sources. That one template is then appended to the QueryPath object.
+ * @see tpl()
+ * @param mixed $template
+ * The template. It can be of any of the types that {@link qp()} supports
+ * natively. Typically it is a string of XML/HTML.
+ * @param array $objects
+ * An indexed array containing a list of objects or arrays (See {@link tpl()})
+ * that will be merged into the template.
+ * @param array $options
+ * An array of options. See {@link tpl()} for a list.
+ * @return QueryPath
+ * Returns the QueryPath object.
+ */
+ public function tplAll($template, $objects, $options = array()) {
+ $tqp = qp($template, ':root');
+ foreach ($objects as $object) {
+ if (is_array($object))
+ $tqp = $this->tplArrayR($tqp, $object, $options);
+ elseif (is_object($object))
+ $tqp = $this->tplObject($tqp, $object, $options);
+ }
+ return $this->qp->append($tqp->top());
+ }
+
+ /*
+ protected function tplArray($tqp, $array, $options = array()) {
+
+ // If we find something that's not an array, we try to handle it.
+ if (!is_array($array)) {
+ is_object($array) ? $this->tplObject($tqp, $array, $options) : $tqp->append($array);
+ }
+ // An assoc array means we have mappings of classes to content.
+ elseif ($this->isAssoc($array)) {
+ print 'Assoc array found.' . PHP_EOL;
+ foreach ($array as $key => $value) {
+ $first = substr($key,0,1);
+
+ // We allow classes and IDs if explicit. Otherwise we assume
+ // a class.
+ if ($first != '.' && $first != '#') $key = '.' . $key;
+
+ if ($tqp->top()->find($key)->size() > 0) {
+ print "Value: " . $value . PHP_EOL;
+ if (is_array($value)) {
+ //$newqp = qp($tqp)->cloneAll();
+ print $tqp->xml();
+ $this->tplArray($tqp, $value, $options);
+ print "Finished recursion\n";
+ }
+ else {
+ print 'QP is ' . $tqp->size() . " inserting value: " . $value . PHP_EOL;
+
+ $tqp->append($value);
+ }
+ }
+ }
+ }
+ // An indexed array means we have multiple instances of class->content matches.
+ // We copy the portion of the template and then call repeatedly.
+ else {
+ print "Array of arrays found..\n";
+ foreach ($array as $array2) {
+ $clone = qp($tqp->xml());
+ $this->tplArray($clone, $array2, $options);
+ print "Now appending clone.\n" . $clone->xml();
+ $tqp->append($clone->parent());
+ }
+ }
+
+
+ //return $tqp->top();
+ return $tqp;
+ }
+ */
+
+ /**
+ * Introspect objects to map their functions to CSS classes in a template.
+ */
+ protected function tplObject($tqp, $object, $options = array()) {
+ $ref = new ReflectionObject($object);
+ $methods = $ref->getMethods();
+ foreach ($methods as $method) {
+ if (strpos($method->getName(), 'get') === 0) {
+ $cssClass = $this->method2class($method->getName());
+ if ($tqp->top()->find($cssClass)->size() > 0) {
+ $tqp->append($method->invoke($object));
+ }
+ else {
+ // Revert to the find() that found something.
+ $tqp->end();
+ }
+ }
+ }
+ //return $tqp->top();
+ return $tqp;
+ }
+
+ /**
+ * Recursively merge array data into a template.
+ */
+ public function tplArrayR($qp, $array, $options = NULL) {
+ // If the value looks primitive, append it.
+ if (!is_array($array) && !($array instanceof Traversable)) {
+ $qp->append($array);
+ }
+ // If we are dealing with an associative array, traverse it
+ // and merge as we go.
+ elseif ($this->isAssoc($array)) {
+ // Do key/value substitutions
+ foreach ($array as $k => $v) {
+
+ // If no dot or hash, assume class.
+ $first = substr($k,0,1);
+ if ($first != '.' && $first != '#') $k = '.' . $k;
+
+ // If value is an array, recurse.
+ if (is_array($v)) {
+ // XXX: Not totally sure that starting at the
+ // top is right. Perhaps it should start
+ // at some other context?
+ $this->tplArrayR($qp->top($k), $v, $options);
+ }
+ // Otherwise, try to append value.
+ else {
+ $qp->branch()->children($k)->append($v);
+ }
+ }
+ }
+ // Otherwise we have an indexed array, and we iterate through
+ // it.
+ else {
+ // Get a copy of the current template and then recurse.
+ foreach ($array as $entry) {
+ $eles = $qp->get();
+ $template = array();
+
+ // We manually deep clone the template.
+ foreach ($eles as $ele) {
+ $template = $ele->cloneNode(TRUE);
+ }
+ $tpl = qp($template);
+ $tpl = $this->tplArrayR($tpl, $entry, $options);
+ $qp->before($tpl);
+ }
+ // Remove the original template without loosing a handle to the
+ // newly injected one.
+ $dead = $qp->branch();
+ $qp->parent();
+ $dead->remove();
+ unset($dead);
+ }
+ return $qp;
+ }
+
+ /**
+ * Check whether an array is associative.
+ * If the keys of the array are not consecutive integers starting with 0,
+ * this will return false.
+ *
+ * @param array $array
+ * The array to test.
+ * @return Boolean
+ * TRUE if this is an associative array, FALSE otherwise.
+ */
+ public function isAssoc($array) {
+ $i = 0;
+ foreach ($array as $k => $v) if ($k !== $i++) return TRUE;
+ // If we get here, all keys passed.
+ return FALSE;
+ }
+
+ /**
+ * Convert a function name to a CSS class selector (e.g. myFunc becomes '.myFunc').
+ * @param string $mname
+ * Method name.
+ * @return string
+ * CSS 3 Class Selector.
+ */
+ protected function method2class($mname) {
+ return '.' . substr($mname, 3);
+ }
+}
+QueryPathExtensionRegistry::extend('QPTPL'); \ No newline at end of file
diff --git a/lib/querypath/Extension/QPXML.php b/lib/querypath/Extension/QPXML.php
new file mode 100644
index 0000000..e91fa6e
--- /dev/null
+++ b/lib/querypath/Extension/QPXML.php
@@ -0,0 +1,209 @@
+<?php
+/** @file
+ * XML extensions. See QPXML.
+ */
+/**
+ * Provide QueryPath with additional XML tools.
+ *
+ * @author M Butcher <matt@aleph-null.tv>
+ * @author Xander Guzman <theshadow@shadowpedia.info>
+ * @license http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
+ * @see QueryPathExtension
+ * @see QueryPathExtensionRegistry::extend()
+ * @see QPXML
+ * @ingroup querypath_extensions
+ */
+class QPXML implements QueryPathExtension {
+
+ protected $qp;
+
+ public function __construct(QueryPath $qp) {
+ $this->qp = $qp;
+ }
+
+ public function schema($file) {
+ $doc = $this->qp->branch()->top()->get(0)->ownerDocument;
+
+ if (!$doc->schemaValidate($file)) {
+ throw new QueryPathException('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.
+ * @retval object
+ */
+ 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 QueryPathException("Undefined namespace for: " . $text);
+ }
+ }
+
+ $node = null;
+ if ($nsUri !== null) {
+ $node = $element->ownerDocument->createElementNS(
+ $nsUri,
+ $text
+ );
+ } else {
+ $node = $element->ownerDocument->createElement($text);
+ }
+ return qp($node);
+ }
+ }
+ return;
+ }
+
+ /**
+ * Append an element.
+ *
+ * @param string $text
+ * @retval object QueryPath
+ */
+ public function appendElement($text) {
+ if (isset ($text)) {
+ foreach ($this->qp->get() as $element) {
+ $node = $this->qp->createElement($text);
+ qp($element)->append($node);
+ }
+ }
+ return $this->qp;
+ }
+}
+QueryPathExtensionRegistry::extend('QPXML');
diff --git a/lib/querypath/Extension/QPXSL.php b/lib/querypath/Extension/QPXSL.php
new file mode 100644
index 0000000..4adb317
--- /dev/null
+++ b/lib/querypath/Extension/QPXSL.php
@@ -0,0 +1,75 @@
+<?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 http://opensource.org/licenses/lgpl-2.1.php LGPL or MIT-like license.
+ * @see QueryPathExtension
+ * @see QueryPathExtensionRegistry::extend()
+ * @see QPXSL
+ * @see QPXML
+ */
+
+/**
+ * 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 QueryPathExtension {
+
+ protected $src = NULL;
+
+ public function __construct(QueryPath $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 = qp($style);
+ }
+ $sourceDoc = $this->src->top()->get(0)->ownerDocument;
+ $styleDoc = $style->get(0)->ownerDocument;
+ $processor = new XSLTProcessor();
+ $processor->importStylesheet($styleDoc);
+ return qp($processor->transformToDoc($sourceDoc));
+ }
+}
+QueryPathExtensionRegistry::extend('QPXSL'); \ No newline at end of file