Browse Source

Passage trunk/branches pour touti de querypath

v1
marcimat@rezo.net 7 years ago
commit
cc06909a19
  1. 17
      .gitattributes
  2. 117
      inc/querypath.php
  3. 16
      lang/paquet-querypath_fr.php
  4. 12
      lang/querypath_fr.php
  5. 1432
      lib/QueryPath/CssEventHandler.php
  6. 1108
      lib/QueryPath/CssParser.php
  7. 711
      lib/QueryPath/Extension/QPDB.php
  8. 213
      lib/QueryPath/Extension/QPList.php
  9. 275
      lib/QueryPath/Extension/QPTPL.php
  10. 209
      lib/QueryPath/Extension/QPXML.php
  11. 75
      lib/QueryPath/Extension/QPXSL.php
  12. 4543
      lib/QueryPath/QueryPath.php
  13. 195
      lib/QueryPath/QueryPathExtension.php
  14. 16
      paquet.xml
  15. BIN
      prive/themes/spip/images/querypath-128.png
  16. BIN
      prive/themes/spip/images/querypath-32.png
  17. BIN
      prive/themes/spip/images/querypath-64.png

17
.gitattributes

@ -0,0 +1,17 @@
* text=auto !eol
inc/querypath.php -text
lang/paquet-querypath_fr.php -text
lang/querypath_fr.php -text
lib/QueryPath/CssEventHandler.php -text
lib/QueryPath/CssParser.php -text
lib/QueryPath/Extension/QPDB.php -text
lib/QueryPath/Extension/QPList.php -text
lib/QueryPath/Extension/QPTPL.php -text
lib/QueryPath/Extension/QPXML.php -text
lib/QueryPath/Extension/QPXSL.php -text
lib/QueryPath/QueryPath.php -text
lib/QueryPath/QueryPathExtension.php -text
/paquet.xml -text
prive/themes/spip/images/querypath-128.png -text
prive/themes/spip/images/querypath-32.png -text
prive/themes/spip/images/querypath-64.png -text

117
inc/querypath.php

@ -0,0 +1,117 @@
<?php
/**
* Ce fichier Gère le chargement de la librairie QueryPath
*
* Une fois ce fichier chargé, vous avez accès à la fonction
* spip_query_path(), mais également si vous préférez aux fonctions
* de la librairie directement : qp(), htmlqp() ou encore directement
* la classe QueryPath.
*
* @link http://querypath.org/
* @version 2.1.2
**/
/**
* Plugin Query Path
* (c) 2012 Marcillaud Matthieu
* Licence GNU/GPL
*/
if (!defined('_ECRIRE_INC_VERSION')) return;
/**
* Charger la librairie QueryPath
*
* Permet de chercher et modifier du HTML
* http://http://querypath.org/
**/
include_spip('lib/QueryPath/QueryPath');
/**
* Retourne un objet QueryPath
*
* QueryPath est lancé, mais avec les modifications suivantes :
* - l'option replace_entities est passée par défaut à TRUE
* - lorsque $document est un texte, il est transformé en unicode
* et les CDATA sont enlevés. Cela est plus pratique pour manipuler du HTML.
*
* @api
* @param string|null $document
* URL ou texte du document XML/HTML
* @param string|null $string
* Position sur laquelle se placer, exemple 'body'
* @param array $options
* Option de la classe QueryPath
* @return SpipQueryPath
* Objet QueryPath adapté pour SPIP
*
**/
function spip_query_path($document = NULL, $string = NULL, $options = array()) {
// convertir automatiquement si le document est une chaine
// en tenant compte du charset du site et des CDATA
if (is_string($document)) {
// domDocument a du mal avec l'UTF, c'est pourquoi
// QueryPath a des options pour transcoder la source
// en utilisant la librairie mb.
// Comme SPIP possède sa propre fonction, autant l'utiliser.
$document = charset2unicode($document);
// le chargement d'un HTML ayant déjà des CDATA est problématique
// car ils seront automatiquement doublés (cf. https://bugs.php.net/bug.php?id=54429)
// On les échappe ici automatiquement.
// Il ne faudrait peut être pas le faire si <?xml est là...
$document = querypath_echappe_CDATA($document);
}
// indiquer que les ajouts tel que ->after()
// doivent transformer les entités HTML présentes,
// sinon le XML est rarement correct et domDocument râle.
$options += array('replace_entities' => true);
// lancer
return qp($document, $string, $options);
}
/**
* Enlève les cdata présents dans un texte...
*
* domDocument les ajoute automatiquement
* sans possibilité de déconnecter la fonctionnalité.
* Du coup, lorsque les CDATA sont déjà présents, ce qui est le cas
* en général chez SPIP, ils se retrouvent doublés.
* Cf. https://bugs.php.net/bug.php?id=54429
*
* @param string $html
* Contenu du document html
* @return string
* Contenu sans les CDATA
**/
function querypath_echappe_CDATA($html) {
static $cdata_on = '<!\[CDATA\[';
static $cdata_off = '\]\]>';
if (false !== strpos($html, '<![')) {
// echapper \\<![CDATA[ \\]]>
$html = preg_replace('/'
. '\/\/' . $cdata_on #ouverture
. '(.*?)' #contenu
. '\/\/' . $cdata_off #fermuture
. '/is', '$1', $html);
// echapper /* <![CDATA[ */ /* ]]> */
$html = preg_replace('/'
. '\/\*\s*' . $cdata_on . '\s*\*\/' #ouverture
. '(.*?)' #contenu
. '\/\*\s*' . $cdata_off . '\s*\*\/' #fermuture
. '/is', '$1', $html);
// echapper <![CDATA[ ]]>
$html = preg_replace('/'
. $cdata_on #ouverture
. '(.*?)' #contenu
. $cdata_off #fermuture
. '/is', '$1', $html);
}
return $html;
}

16
lang/paquet-querypath_fr.php

@ -0,0 +1,16 @@
<?php
// This is a SPIP language file -- Ceci est un fichier langue de SPIP
if (!defined('_ECRIRE_INC_VERSION')) return;
$GLOBALS[$GLOBALS['idx_lang']] = array(
// Q
'querypath_description' => 'Query Path (http://querypath.org/) est une librairie PHP permettant de récupérer, sélectionner et manipuler un contenu HTML comme le fait la librairie javascript jQuery.
Ce plugin permet d\'utiliser cette librairie dans SPIP.',
'querypath_nom' => 'Query Path',
'querypath_slogan' => 'Du jQuery en PHP, et bien plus encore',
);
?>

12
lang/querypath_fr.php

@ -0,0 +1,12 @@
<?php
// This is a SPIP language file -- Ceci est un fichier langue de SPIP
if (!defined('_ECRIRE_INC_VERSION')) return;
$GLOBALS[$GLOBALS['idx_lang']] = array(
// Q
'querypath_titre' => 'Query Path',
);
?>

1432
lib/QueryPath/CssEventHandler.php
File diff suppressed because it is too large
View File

1108
lib/QueryPath/CssParser.php
File diff suppressed because it is too large
View File

711
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');

213
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;
}
}

275
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');

209
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.