summaryrefslogtreecommitdiff
path: root/lib/phptal/PHPTAL/Dom
diff options
context:
space:
mode:
authoremkael <emkael@tlen.pl>2016-10-31 21:58:33 +0100
committeremkael <emkael@tlen.pl>2016-10-31 21:59:22 +0100
commitd216b3147bc3f37cf2337acab5767c6a4f74aa2e (patch)
tree6090989e5071db101a1112131e2b075a02dccbc4 /lib/phptal/PHPTAL/Dom
parentb23bfbb17d1d5f6852a1690f246a84c2d38ae969 (diff)
* PHPTAL library
Diffstat (limited to 'lib/phptal/PHPTAL/Dom')
-rw-r--r--lib/phptal/PHPTAL/Dom/Attr.php196
-rw-r--r--lib/phptal/PHPTAL/Dom/CDATASection.php49
-rw-r--r--lib/phptal/PHPTAL/Dom/Comment.php28
-rw-r--r--lib/phptal/PHPTAL/Dom/Defs.php246
-rw-r--r--lib/phptal/PHPTAL/Dom/DocumentBuilder.php63
-rw-r--r--lib/phptal/PHPTAL/Dom/DocumentType.php33
-rw-r--r--lib/phptal/PHPTAL/Dom/Element.php521
-rw-r--r--lib/phptal/PHPTAL/Dom/Node.php105
-rw-r--r--lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php167
-rw-r--r--lib/phptal/PHPTAL/Dom/ProcessingInstruction.php34
-rw-r--r--lib/phptal/PHPTAL/Dom/SaxXmlParser.php480
-rw-r--r--lib/phptal/PHPTAL/Dom/Text.php31
-rw-r--r--lib/phptal/PHPTAL/Dom/XmlDeclaration.php29
-rw-r--r--lib/phptal/PHPTAL/Dom/XmlnsState.php95
14 files changed, 2077 insertions, 0 deletions
diff --git a/lib/phptal/PHPTAL/Dom/Attr.php b/lib/phptal/PHPTAL/Dom/Attr.php
new file mode 100644
index 0000000..64ffe03
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Attr.php
@@ -0,0 +1,196 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * node that represents element's attribute
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Attr
+{
+ private $value_escaped, $qualified_name, $namespace_uri, $encoding;
+ /**
+ * attribute's value can be overriden with a variable
+ */
+ private $phpVariable;
+ const HIDDEN = -1;
+ const NOT_REPLACED = 0;
+ const VALUE_REPLACED = 1;
+ const FULLY_REPLACED = 2;
+ private $replacedState = 0;
+
+ /**
+ * @param string $qualified_name attribute name with prefix
+ * @param string $namespace_uri full namespace URI or empty string
+ * @param string $value_escaped value with HTML-escaping
+ * @param string $encoding character encoding used by the value
+ */
+ function __construct($qualified_name, $namespace_uri, $value_escaped, $encoding)
+ {
+ $this->value_escaped = $value_escaped;
+ $this->qualified_name = $qualified_name;
+ $this->namespace_uri = $namespace_uri;
+ $this->encoding = $encoding;
+ }
+
+ /**
+ * get character encoding used by this attribute.
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * get full namespace URI. "" for default namespace.
+ */
+ function getNamespaceURI()
+ {
+ return $this->namespace_uri;
+ }
+
+ /**
+ * get attribute name including namespace prefix, if any
+ */
+ function getQualifiedName()
+ {
+ return $this->qualified_name;
+ }
+
+ /**
+ * get "foo" of "ns:foo" attribute name
+ */
+ function getLocalName()
+ {
+ $n = explode(':', $this->qualified_name, 2);
+ return end($n);
+ }
+
+ /**
+ * Returns true if this attribute is ns declaration (xmlns="...")
+ *
+ * @return bool
+ */
+ function isNamespaceDeclaration()
+ {
+ return preg_match('/^xmlns(?:$|:)/', $this->qualified_name);
+ }
+
+
+ /**
+ * get value as plain text
+ *
+ * @return string
+ */
+ function getValue()
+ {
+ return html_entity_decode($this->value_escaped, ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * set plain text as value
+ */
+ function setValue($val)
+ {
+ $this->value_escaped = htmlspecialchars($val, ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * Depends on replaced state.
+ * If value is not replaced, it will return it with HTML escapes.
+ *
+ * @see getReplacedState()
+ * @see overwriteValueWithVariable()
+ */
+ function getValueEscaped()
+ {
+ return $this->value_escaped;
+ }
+
+ /**
+ * Set value of the attribute to this exact string.
+ * String must be HTML-escaped and use attribute's encoding.
+ *
+ * @param string $value_escaped new content
+ */
+ function setValueEscaped($value_escaped)
+ {
+ $this->replacedState = self::NOT_REPLACED;
+ $this->value_escaped = $value_escaped;
+ }
+
+ /**
+ * set PHP code as value of this attribute. Code is expected to echo the value.
+ */
+ private function setPHPCode($code)
+ {
+ $this->value_escaped = '<?php '.$code." ?>\n";
+ }
+
+ /**
+ * hide this attribute. It won't be generated.
+ */
+ function hide()
+ {
+ $this->replacedState = self::HIDDEN;
+ }
+
+ /**
+ * generate value of this attribute from variable
+ */
+ function overwriteValueWithVariable($phpVariable)
+ {
+ $this->replacedState = self::VALUE_REPLACED;
+ $this->phpVariable = $phpVariable;
+ $this->setPHPCode('echo '.$phpVariable);
+ }
+
+ /**
+ * generate complete syntax of this attribute using variable
+ */
+ function overwriteFullWithVariable($phpVariable)
+ {
+ $this->replacedState = self::FULLY_REPLACED;
+ $this->phpVariable = $phpVariable;
+ $this->setPHPCode('echo '.$phpVariable);
+ }
+
+ /**
+ * use any PHP code to generate this attribute's value
+ */
+ function overwriteValueWithCode($code)
+ {
+ $this->replacedState = self::VALUE_REPLACED;
+ $this->phpVariable = null;
+ $this->setPHPCode($code);
+ }
+
+ /**
+ * if value was overwritten with variable, get its name
+ */
+ function getOverwrittenVariableName()
+ {
+ return $this->phpVariable;
+ }
+
+ /**
+ * whether getValueEscaped() returns real value or PHP code
+ */
+ function getReplacedState()
+ {
+ return $this->replacedState;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/CDATASection.php b/lib/phptal/PHPTAL/Dom/CDATASection.php
new file mode 100644
index 0000000..838429b
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/CDATASection.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Outputs <![CDATA[ ]]> blocks, sometimes converts them to text
+ * @todo this might be moved to CDATA processing in Element
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_CDATASection extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $mode = $codewriter->getOutputMode();
+ $value = $this->getValueEscaped();
+ $inCDATAelement = PHPTAL_Dom_Defs::getInstance()->isCDATAElementInHTML($this->parentNode->getNamespaceURI(), $this->parentNode->getLocalName());
+
+ // in HTML5 must limit it to <script> and <style>
+ if ($mode === PHPTAL::HTML5 && $inCDATAelement) {
+ $codewriter->pushHTML($codewriter->interpolateCDATA(str_replace('</', '<\/', $value)));
+ } elseif (($mode === PHPTAL::XHTML && $inCDATAelement) // safe for text/html
+ || ($mode === PHPTAL::XML && preg_match('/[<>&]/', $value)) // non-useless in XML
+ || ($mode !== PHPTAL::HTML5 && preg_match('/<\?|\${structure/', $value))) // hacks with structure (in X[HT]ML) may need it
+ {
+ // in text/html "</" is dangerous and the only sensible way to escape is ECMAScript string escapes.
+ if ($mode === PHPTAL::XHTML) $value = str_replace('</', '<\/', $value);
+
+ $codewriter->pushHTML($codewriter->interpolateCDATA('<![CDATA['.$value.']]>'));
+ } else {
+ $codewriter->pushHTML($codewriter->interpolateHTML(
+ htmlspecialchars($value, ENT_QUOTES, $codewriter->getEncoding())
+ ));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Comment.php b/lib/phptal/PHPTAL/Dom/Comment.php
new file mode 100644
index 0000000..4a3ba3c
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Comment.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Comment extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (!preg_match('/^\s*!/', $this->getValueEscaped())) {
+ $codewriter->pushHTML('<!--'.$this->getValueEscaped().'-->');
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Defs.php b/lib/phptal/PHPTAL/Dom/Defs.php
new file mode 100644
index 0000000..4d12ed6
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Defs.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * PHPTAL constants.
+ *
+ * This is a pseudo singleton class, a user may decide to provide
+ * his own singleton instance which will then be used by PHPTAL.
+ *
+ * This behaviour is mainly useful to remove builtin namespaces
+ * and provide custom ones.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Dom_Defs
+{
+ /**
+ * this is a singleton
+ */
+ public static function getInstance()
+ {
+ if (!self::$_instance) {
+ self::$_instance = new PHPTAL_Dom_Defs();
+ }
+ return self::$_instance;
+ }
+
+ protected function __construct()
+ {
+ $this->registerNamespace(new PHPTAL_Namespace_TAL());
+ $this->registerNamespace(new PHPTAL_Namespace_METAL());
+ $this->registerNamespace(new PHPTAL_Namespace_I18N());
+ $this->registerNamespace(new PHPTAL_Namespace_PHPTAL());
+ }
+
+ /**
+ * true if it's empty in XHTML (e.g. <img/>)
+ * it will assume elements with no namespace may be XHTML too.
+ *
+ * @param string $tagName local name of the tag
+ *
+ * @return bool
+ */
+ public function isEmptyTagNS($namespace_uri, $local_name)
+ {
+ return ($namespace_uri === 'http://www.w3.org/1999/xhtml' || $namespace_uri === '')
+ && in_array(strtolower($local_name), self::$XHTML_EMPTY_TAGS);
+ }
+
+ /**
+ * gives namespace URI for given registered (built-in) prefix
+ */
+ public function prefixToNamespaceURI($prefix)
+ {
+ return isset($this->prefix_to_uri[$prefix]) ? $this->prefix_to_uri[$prefix] : false;
+ }
+
+ /**
+ * gives typical prefix for given (built-in) namespace
+ */
+ public function namespaceURIToPrefix($uri)
+ {
+ return array_search($uri, $this->prefix_to_uri, true);
+ }
+
+ /**
+ * array prefix => uri for prefixes that don't have to be declared in PHPTAL
+ * @return array
+ */
+ public function getPredefinedPrefixes()
+ {
+ return $this->prefix_to_uri;
+ }
+
+ /**
+ * Returns true if the attribute is an xhtml boolean attribute.
+ *
+ * @param string $att local name
+ *
+ * @return bool
+ */
+ public function isBooleanAttribute($att)
+ {
+ return in_array($att, self::$XHTML_BOOLEAN_ATTRIBUTES);
+ }
+
+ /**
+ * true if elements content is parsed as CDATA in text/html
+ * and also accepts /* * / as comments.
+ */
+ public function isCDATAElementInHTML($namespace_uri, $local_name)
+ {
+ return ($local_name === 'script' || $local_name === 'style')
+ && ($namespace_uri === 'http://www.w3.org/1999/xhtml' || $namespace_uri === '');
+ }
+
+ /**
+ * Returns true if the attribute is a valid phptal attribute
+ *
+ * Examples of valid attributes: tal:content, metal:use-slot
+ * Examples of invalid attributes: tal:unknown, metal:content
+ *
+ * @return bool
+ */
+ public function isValidAttributeNS($namespace_uri, $local_name)
+ {
+ if (!$this->isHandledNamespace($namespace_uri)) return false;
+
+ $attrs = $this->namespaces_by_uri[$namespace_uri]->getAttributes();
+ return isset($attrs[$local_name]);
+ }
+
+ /**
+ * is URI registered (built-in) namespace
+ */
+ public function isHandledNamespace($namespace_uri)
+ {
+ return isset($this->namespaces_by_uri[$namespace_uri]);
+ }
+
+ /**
+ * Returns true if the attribute is a phptal handled xml namespace
+ * declaration.
+ *
+ * Examples of handled xmlns: xmlns:tal, xmlns:metal
+ *
+ * @return bool
+ */
+ public function isHandledXmlNs($qname, $value)
+ {
+ return substr(strtolower($qname), 0, 6) == 'xmlns:' && $this->isHandledNamespace($value);
+ }
+
+ /**
+ * return objects that holds information about given TAL attribute
+ */
+ public function getNamespaceAttribute($namespace_uri, $local_name)
+ {
+ $attrs = $this->namespaces_by_uri[$namespace_uri]->getAttributes();
+ return $attrs[$local_name];
+ }
+
+ /**
+ * Register a PHPTAL_Namespace and its attribute into PHPTAL.
+ */
+ public function registerNamespace(PHPTAL_Namespace $ns)
+ {
+ $this->namespaces_by_uri[$ns->getNamespaceURI()] = $ns;
+ $this->prefix_to_uri[$ns->getPrefix()] = $ns->getNamespaceURI();
+ $prefix = strtolower($ns->getPrefix());
+ foreach ($ns->getAttributes() as $name => $attribute) {
+ $key = $prefix.':'.strtolower($name);
+ $this->_dictionary[$key] = $attribute;
+ }
+ }
+
+ private static $_instance = null;
+ private $_dictionary = array();
+ /**
+ * list of PHPTAL_Namespace objects
+ */
+ private $namespaces_by_uri = array();
+ private $prefix_to_uri = array(
+ 'xml'=>'http://www.w3.org/XML/1998/namespace',
+ 'xmlns'=>'http://www.w3.org/2000/xmlns/',
+ );
+
+ /**
+ * This array contains XHTML tags that must be echoed in a &lt;tag/&gt; form
+ * instead of the &lt;tag&gt;&lt;/tag&gt; form.
+ *
+ * In fact, some browsers does not support the later form so PHPTAL
+ * ensure these tags are correctly echoed.
+ */
+ private static $XHTML_EMPTY_TAGS = array(
+ 'area',
+ 'base',
+ 'basefont',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'frame',
+ 'hr',
+ 'img',
+ 'input',
+ 'isindex',
+ 'keygen',
+ 'link',
+ 'meta',
+ 'param',
+ 'wbr',
+ 'source',
+ 'track',
+ );
+
+ /**
+ * This array contains XHTML boolean attributes, their value is self
+ * contained (ie: they are present or not).
+ */
+ private static $XHTML_BOOLEAN_ATTRIBUTES = array(
+ 'autoplay',
+ 'async',
+ 'autofocus',
+ 'checked',
+ 'compact',
+ 'controls',
+ 'declare',
+ 'default',
+ 'defer',
+ 'disabled',
+ 'formnovalidate',
+ 'hidden',
+ 'ismap',
+ 'itemscope',
+ 'loop',
+ 'multiple',
+ 'noresize',
+ 'noshade',
+ 'novalidate',
+ 'nowrap',
+ 'open',
+ 'pubdate',
+ 'readonly',
+ 'required',
+ 'reversed',
+ 'scoped',
+ 'seamless',
+ 'selected',
+ );
+}
diff --git a/lib/phptal/PHPTAL/Dom/DocumentBuilder.php b/lib/phptal/PHPTAL/Dom/DocumentBuilder.php
new file mode 100644
index 0000000..c08587f
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/DocumentBuilder.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * DOM Builder
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+abstract class PHPTAL_Dom_DocumentBuilder
+{
+ protected $_stack; /* array<PHPTAL_Dom_Node> */
+ protected $_current; /* PHPTAL_Dom_Node */
+
+ protected $file, $line;
+
+ public function __construct()
+ {
+ $this->_stack = array();
+ }
+
+ abstract public function getResult();
+
+ abstract public function onDocumentStart();
+
+ abstract public function onDocumentEnd();
+
+ abstract public function onDocType($doctype);
+
+ abstract public function onXmlDecl($decl);
+
+ abstract public function onComment($data);
+
+ abstract public function onCDATASection($data);
+
+ abstract public function onProcessingInstruction($data);
+
+ abstract public function onElementStart($element_qname, array $attributes);
+
+ abstract public function onElementData($data);
+
+ abstract public function onElementClose($qname);
+
+ public function setSource($file, $line)
+ {
+ $this->file = $file; $this->line = $line;
+ }
+
+ abstract public function setEncoding($encoding);
+}
+
diff --git a/lib/phptal/PHPTAL/Dom/DocumentType.php b/lib/phptal/PHPTAL/Dom/DocumentType.php
new file mode 100644
index 0000000..38b49e4
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/DocumentType.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Document doctype representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_DocumentType extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
+ $codewriter->setDocType('<!DOCTYPE html>');
+ } else {
+ $codewriter->setDocType($this->getValueEscaped());
+ }
+ $codewriter->doDoctype();
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Element.php b/lib/phptal/PHPTAL/Dom/Element.php
new file mode 100644
index 0000000..574c830
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Element.php
@@ -0,0 +1,521 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Document Tag representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Element extends PHPTAL_Dom_Node
+{
+ protected $qualifiedName, $namespace_uri;
+ private $attribute_nodes = array();
+ protected $replaceAttributes = array();
+ protected $contentAttributes = array();
+ protected $surroundAttributes = array();
+ public $headFootDisabled = false;
+ public $headPrintCondition = false;
+ public $footPrintCondition = false;
+ public $hidden = false;
+
+ // W3C DOM interface
+ public $childNodes = array();
+ public $parentNode;
+
+ /**
+ * @param string $qname qualified name of the element, e.g. "tal:block"
+ * @param string $namespace_uri namespace of this element
+ * @param array $attribute_nodes array of PHPTAL_Dom_Attr elements
+ * @param object $xmlns object that represents namespaces/prefixes known in element's context
+ */
+ public function __construct($qname, $namespace_uri, array $attribute_nodes, PHPTAL_Dom_XmlnsState $xmlns)
+ {
+ $this->qualifiedName = $qname;
+ $this->attribute_nodes = $attribute_nodes;
+ $this->namespace_uri = $namespace_uri;
+ $this->xmlns = $xmlns;
+
+ // implements inheritance of element's namespace to tal attributes (<metal: use-macro>)
+ foreach ($attribute_nodes as $index => $attr) {
+ // it'll work only when qname == localname, which is good
+ if ($this->xmlns->isValidAttributeNS($namespace_uri, $attr->getQualifiedName())) {
+ $this->attribute_nodes[$index] = new PHPTAL_Dom_Attr($attr->getQualifiedName(), $namespace_uri, $attr->getValueEscaped(), $attr->getEncoding());
+ }
+ }
+
+ if ($this->xmlns->isHandledNamespace($this->namespace_uri)) {
+ $this->headFootDisabled = true;
+ }
+
+ $talAttributes = $this->separateAttributes();
+ $this->orderTalAttributes($talAttributes);
+ }
+
+ /**
+ * returns object that represents namespaces known in element's context
+ */
+ public function getXmlnsState()
+ {
+ return $this->xmlns;
+ }
+
+ /**
+ * Replace <script> foo &gt; bar </script>
+ * with <script>/*<![CDATA[* / foo > bar /*]]>* /</script>
+ * This avoids gotcha in text/html.
+ *
+ * Note that PHPTAL_Dom_CDATASection::generate() does reverse operation, if needed!
+ *
+ * @return void
+ */
+ private function replaceTextWithCDATA()
+ {
+ $isCDATAelement = PHPTAL_Dom_Defs::getInstance()->isCDATAElementInHTML($this->getNamespaceURI(), $this->getLocalName());
+
+ if (!$isCDATAelement) {
+ return;
+ }
+
+ $valueEscaped = ''; // sometimes parser generates split text nodes. "normalisation" is needed.
+ $value = '';
+ foreach ($this->childNodes as $node) {
+ // leave it alone if there is CDATA, comment, or anything else.
+ if (!$node instanceof PHPTAL_Dom_Text) return;
+
+ $value .= $node->getValue();
+ $valueEscaped .= $node->getValueEscaped();
+
+ $encoding = $node->getEncoding(); // encoding of all nodes is the same
+ }
+
+ // only add cdata if there are entities
+ // and there's no ${structure} (because it may rely on cdata syntax)
+ if (false === strpos($valueEscaped, '&') || preg_match('/<\?|\${structure/', $value)) {
+ return;
+ }
+
+ $this->childNodes = array();
+
+ // appendChild sets parent
+ $this->appendChild(new PHPTAL_Dom_Text('/*', $encoding));
+ $this->appendChild(new PHPTAL_Dom_CDATASection('*/'.$value.'/*', $encoding));
+ $this->appendChild(new PHPTAL_Dom_Text('*/', $encoding));
+ }
+
+ public function appendChild(PHPTAL_Dom_Node $child)
+ {
+ if ($child->parentNode) $child->parentNode->removeChild($child);
+ $child->parentNode = $this;
+ $this->childNodes[] = $child;
+ }
+
+ public function removeChild(PHPTAL_Dom_Node $child)
+ {
+ foreach ($this->childNodes as $k => $node) {
+ if ($child === $node) {
+ $child->parentNode = null;
+ array_splice($this->childNodes, $k, 1);
+ return;
+ }
+ }
+ throw new PHPTAL_Exception("Given node is not child of ".$this->getQualifiedName());
+ }
+
+ public function replaceChild(PHPTAL_Dom_Node $newElement, PHPTAL_Dom_Node $oldElement)
+ {
+ foreach ($this->childNodes as $k => $node) {
+ if ($node === $oldElement) {
+ $oldElement->parentNode = NULL;
+
+ if ($newElement->parentNode) $newElement->parentNode->removeChild($child);
+ $newElement->parentNode = $this;
+
+ $this->childNodes[$k] = $newElement;
+ return;
+ }
+ }
+ throw new PHPTAL_Exception("Given node is not child of ".$this->getQualifiedName());
+ }
+
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ try
+ {
+ /// self-modifications
+
+ if ($codewriter->getOutputMode() === PHPTAL::XHTML) {
+ $this->replaceTextWithCDATA();
+ }
+
+ /// code generation
+
+ if ($this->getSourceLine()) {
+ $codewriter->doComment('tag "'.$this->qualifiedName.'" from line '.$this->getSourceLine());
+ }
+
+ $this->generateSurroundHead($codewriter);
+
+ if (count($this->replaceAttributes)) {
+ foreach ($this->replaceAttributes as $att) {
+ $att->before($codewriter);
+ $att->after($codewriter);
+ }
+ } elseif (!$this->hidden) {
+ // a surround tag may decide to hide us (tal:define for example)
+ $this->generateHead($codewriter);
+ $this->generateContent($codewriter);
+ $this->generateFoot($codewriter);
+ }
+
+ $this->generateSurroundFoot($codewriter);
+ }
+ catch(PHPTAL_TemplateException $e) {
+ $e->hintSrcPosition($this->getSourceFile(), $this->getSourceLine());
+ throw $e;
+ }
+ }
+
+ /**
+ * Array with PHPTAL_Dom_Attr objects
+ *
+ * @return array
+ */
+ public function getAttributeNodes()
+ {
+ return $this->attribute_nodes;
+ }
+
+ /**
+ * Replace all attributes
+ *
+ * @param array $nodes array of PHPTAL_Dom_Attr objects
+ */
+ public function setAttributeNodes(array $nodes)
+ {
+ $this->attribute_nodes = $nodes;
+ }
+
+ /** Returns true if the element contains specified PHPTAL attribute. */
+ public function hasAttribute($qname)
+ {
+ foreach($this->attribute_nodes as $attr) if ($attr->getQualifiedName() == $qname) return true;
+ return false;
+ }
+
+ public function hasAttributeNS($ns_uri, $localname)
+ {
+ return null !== $this->getAttributeNodeNS($ns_uri, $localname);
+ }
+
+ public function getAttributeNodeNS($ns_uri, $localname)
+ {
+ foreach ($this->attribute_nodes as $attr) {
+ if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname) return $attr;
+ }
+ return null;
+ }
+
+ public function removeAttributeNS($ns_uri, $localname)
+ {
+ foreach ($this->attribute_nodes as $k => $attr) {
+ if ($attr->getNamespaceURI() === $ns_uri && $attr->getLocalName() === $localname) {
+ unset($this->attribute_nodes[$k]);
+ return;
+ }
+ }
+ }
+
+ public function getAttributeNode($qname)
+ {
+ foreach($this->attribute_nodes as $attr) if ($attr->getQualifiedName() === $qname) return $attr;
+ return null;
+ }
+
+ /**
+ * If possible, use getAttributeNodeNS and setAttributeNS.
+ *
+ * NB: This method doesn't handle namespaces properly.
+ */
+ public function getOrCreateAttributeNode($qname)
+ {
+ if ($attr = $this->getAttributeNode($qname)) return $attr;
+
+ $attr = new PHPTAL_Dom_Attr($qname, "", null, 'UTF-8'); // FIXME: should find namespace and encoding
+ $this->attribute_nodes[] = $attr;
+ return $attr;
+ }
+
+ /** Returns textual (unescaped) value of specified element attribute. */
+ public function getAttributeNS($namespace_uri, $localname)
+ {
+ if ($n = $this->getAttributeNodeNS($namespace_uri, $localname)) {
+ return $n->getValue();
+ }
+ return '';
+ }
+
+ /**
+ * Set attribute value. Creates new attribute if it doesn't exist yet.
+ *
+ * @param string $namespace_uri full namespace URI. "" for default namespace
+ * @param string $qname prefixed qualified name (e.g. "atom:feed") or local name (e.g. "p")
+ * @param string $value unescaped value
+ *
+ * @return void
+ */
+ public function setAttributeNS($namespace_uri, $qname, $value)
+ {
+ $localname = preg_replace('/^[^:]*:/', '', $qname);
+ if (!($n = $this->getAttributeNodeNS($namespace_uri, $localname))) {
+ $this->attribute_nodes[] = $n = new PHPTAL_Dom_Attr($qname, $namespace_uri, null, 'UTF-8'); // FIXME: find encoding
+ }
+ $n->setValue($value);
+ }
+
+ /**
+ * Returns true if this element or one of its PHPTAL attributes has some
+ * content to print (an empty text node child does not count).
+ *
+ * @return bool
+ */
+ public function hasRealContent()
+ {
+ if (count($this->contentAttributes) > 0) return true;
+
+ foreach ($this->childNodes as $node) {
+ if (!$node instanceof PHPTAL_Dom_Text || $node->getValueEscaped() !== '') return true;
+ }
+ return false;
+ }
+
+ public function hasRealAttributes()
+ {
+ if ($this->hasAttributeNS('http://xml.zope.org/namespaces/tal', 'attributes')) return true;
+ foreach ($this->attribute_nodes as $attr) {
+ if ($attr->getReplacedState() !== PHPTAL_Dom_Attr::HIDDEN) return true;
+ }
+ return false;
+ }
+
+ // ~~~~~ Generation methods may be called by some PHPTAL attributes ~~~~~
+
+ public function generateSurroundHead(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ foreach ($this->surroundAttributes as $att) {
+ $att->before($codewriter);
+ }
+ }
+
+ public function generateHead(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->headFootDisabled) return;
+ if ($this->headPrintCondition) {
+ $codewriter->doIf($this->headPrintCondition);
+ }
+
+ $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);
+
+ if ($html5mode) {
+ $codewriter->pushHTML('<'.$this->getLocalName());
+ } else {
+ $codewriter->pushHTML('<'.$this->qualifiedName);
+ }
+
+ $this->generateAttributes($codewriter);
+
+ if (!$html5mode && $this->isEmptyNode($codewriter->getOutputMode())) {
+ $codewriter->pushHTML('/>');
+ } else {
+ $codewriter->pushHTML('>');
+ }
+
+ if ($this->headPrintCondition) {
+ $codewriter->doEnd('if');
+ }
+ }
+
+ public function generateContent(PHPTAL_Php_CodeWriter $codewriter = null, $realContent=false)
+ {
+ if (!$this->isEmptyNode($codewriter->getOutputMode())) {
+ if ($realContent || !count($this->contentAttributes)) {
+ foreach($this->childNodes as $child) {
+ $child->generateCode($codewriter);
+ }
+ }
+ else foreach($this->contentAttributes as $att) {
+ $att->before($codewriter);
+ $att->after($codewriter);
+ }
+ }
+ }
+
+ public function generateFoot(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->headFootDisabled)
+ return;
+ if ($this->isEmptyNode($codewriter->getOutputMode()))
+ return;
+
+ if ($this->footPrintCondition) {
+ $codewriter->doIf($this->footPrintCondition);
+ }
+
+ if ($codewriter->getOutputMode() === PHPTAL::HTML5) {
+ $codewriter->pushHTML('</'.$this->getLocalName().'>');
+ } else {
+ $codewriter->pushHTML('</'.$this->getQualifiedName().'>');
+ }
+
+ if ($this->footPrintCondition) {
+ $codewriter->doEnd('if');
+ }
+ }
+
+ public function generateSurroundFoot(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ for ($i = (count($this->surroundAttributes)-1); $i >= 0; $i--) {
+ $this->surroundAttributes[$i]->after($codewriter);
+ }
+ }
+
+ // ~~~~~ Private members ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ private function generateAttributes(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $html5mode = ($codewriter->getOutputMode() === PHPTAL::HTML5);
+
+ foreach ($this->getAttributeNodes() as $attr) {
+
+ // xmlns:foo is not allowed in text/html
+ if ($html5mode && $attr->isNamespaceDeclaration()) {
+ continue;
+ }
+
+ switch ($attr->getReplacedState()) {
+ case PHPTAL_Dom_Attr::NOT_REPLACED:
+ $codewriter->pushHTML(' '.$attr->getQualifiedName());
+ if ($codewriter->getOutputMode() !== PHPTAL::HTML5
+ || !PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
+ $html = $codewriter->interpolateHTML($attr->getValueEscaped());
+ $codewriter->pushHTML('='.$codewriter->quoteAttributeValue($html));
+ }
+ break;
+
+ case PHPTAL_Dom_Attr::HIDDEN:
+ break;
+
+ case PHPTAL_Dom_Attr::FULLY_REPLACED:
+ $codewriter->pushHTML($attr->getValueEscaped());
+ break;
+
+ case PHPTAL_Dom_Attr::VALUE_REPLACED:
+ $codewriter->pushHTML(' '.$attr->getQualifiedName().'="');
+ $codewriter->pushHTML($attr->getValueEscaped());
+ $codewriter->pushHTML('"');
+ break;
+ }
+ }
+ }
+
+ private function isEmptyNode($mode)
+ {
+ return (($mode === PHPTAL::XHTML || $mode === PHPTAL::HTML5) && PHPTAL_Dom_Defs::getInstance()->isEmptyTagNS($this->getNamespaceURI(), $this->getLocalName())) ||
+ ( $mode === PHPTAL::XML && !$this->hasContent());
+ }
+
+ private function hasContent()
+ {
+ return count($this->childNodes) > 0 || count($this->contentAttributes) > 0;
+ }
+
+ private function separateAttributes()
+ {
+ $talAttributes = array();
+ foreach ($this->attribute_nodes as $index => $attr) {
+ // remove handled xml namespaces
+ if (PHPTAL_Dom_Defs::getInstance()->isHandledXmlNs($attr->getQualifiedName(), $attr->getValueEscaped())) {
+ unset($this->attribute_nodes[$index]);
+ } else if ($this->xmlns->isHandledNamespace($attr->getNamespaceURI())) {
+ $talAttributes[$attr->getQualifiedName()] = $attr;
+ $attr->hide();
+ } else if (PHPTAL_Dom_Defs::getInstance()->isBooleanAttribute($attr->getQualifiedName())) {
+ $attr->setValue($attr->getLocalName());
+ }
+ }
+ return $talAttributes;
+ }
+
+ private function orderTalAttributes(array $talAttributes)
+ {
+ $temp = array();
+ foreach ($talAttributes as $key => $domattr) {
+ $nsattr = PHPTAL_Dom_Defs::getInstance()->getNamespaceAttribute($domattr->getNamespaceURI(), $domattr->getLocalName());
+ if (array_key_exists($nsattr->getPriority(), $temp)) {
+ throw new PHPTAL_TemplateException(sprintf("Attribute conflict in < %s > '%s' cannot appear with '%s'",
+ $this->qualifiedName,
+ $key,
+ $temp[$nsattr->getPriority()][0]->getNamespace()->getPrefix() . ':' . $temp[$nsattr->getPriority()][0]->getLocalName()
+ ), $this->getSourceFile(), $this->getSourceLine());
+ }
+ $temp[$nsattr->getPriority()] = array($nsattr, $domattr);
+ }
+ ksort($temp);
+
+ $this->talHandlers = array();
+ foreach ($temp as $prio => $dat) {
+ list($nsattr, $domattr) = $dat;
+ $handler = $nsattr->createAttributeHandler($this, $domattr->getValue());
+ $this->talHandlers[$prio] = $handler;
+
+ if ($nsattr instanceof PHPTAL_NamespaceAttributeSurround)
+ $this->surroundAttributes[] = $handler;
+ else if ($nsattr instanceof PHPTAL_NamespaceAttributeReplace)
+ $this->replaceAttributes[] = $handler;
+ else if ($nsattr instanceof PHPTAL_NamespaceAttributeContent)
+ $this->contentAttributes[] = $handler;
+ else
+ throw new PHPTAL_ParserException("Unknown namespace attribute class ".get_class($nsattr),
+ $this->getSourceFile(), $this->getSourceLine());
+
+ }
+ }
+
+ function getQualifiedName()
+ {
+ return $this->qualifiedName;
+ }
+
+ function getNamespaceURI()
+ {
+ return $this->namespace_uri;
+ }
+
+ function getLocalName()
+ {
+ $n = explode(':', $this->qualifiedName, 2);
+ return end($n);
+ }
+
+ function __toString()
+ {
+ return '<{'.$this->getNamespaceURI().'}:'.$this->getLocalName().'>';
+ }
+
+ function setValueEscaped($e) {
+ throw new PHPTAL_Exception("Not supported");
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Node.php b/lib/phptal/PHPTAL/Dom/Node.php
new file mode 100644
index 0000000..5858df6
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Node.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Document node abstract class.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+abstract class PHPTAL_Dom_Node
+{
+ public $parentNode;
+
+ private $value_escaped, $source_file, $source_line, $encoding;
+
+ public function __construct($value_escaped, $encoding)
+ {
+ $this->value_escaped = $value_escaped;
+ $this->encoding = $encoding;
+ }
+
+ /**
+ * hint where this node is in source code
+ */
+ public function setSource($file, $line)
+ {
+ $this->source_file = $file;
+ $this->source_line = $line;
+ }
+
+ /**
+ * file from which this node comes from
+ */
+ public function getSourceFile()
+ {
+ return $this->source_file;
+ }
+
+ /**
+ * line on which this node was defined
+ */
+ public function getSourceLine()
+ {
+ return $this->source_line;
+ }
+
+ /**
+ * depends on node type. Value will be escaped according to context that node comes from.
+ */
+ function getValueEscaped()
+ {
+ return $this->value_escaped;
+ }
+
+ /**
+ * Set value of the node (type-dependent) to this exact string.
+ * String must be HTML-escaped and use node's encoding.
+ *
+ * @param string $value_escaped new content
+ */
+ function setValueEscaped($value_escaped)
+ {
+ $this->value_escaped = $value_escaped;
+ }
+
+
+ /**
+ * get value as plain text. Depends on node type.
+ */
+ function getValue()
+ {
+ return html_entity_decode($this->getValueEscaped(), ENT_QUOTES, $this->encoding);
+ }
+
+ /**
+ * encoding used by vaule of this node.
+ */
+ public function getEncoding()
+ {
+ return $this->encoding;
+ }
+
+ /**
+ * use CodeWriter to compile this element to PHP code
+ */
+ public abstract function generateCode(PHPTAL_Php_CodeWriter $gen);
+
+ function __toString()
+ {
+ return " “".$this->getValue()."” ";
+ }
+}
+
diff --git a/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php b/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php
new file mode 100644
index 0000000..a3157be
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/PHPTALDocumentBuilder.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * DOM Builder
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_PHPTALDocumentBuilder extends PHPTAL_Dom_DocumentBuilder
+{
+ private $_xmlns; /* PHPTAL_Dom_XmlnsState */
+ private $encoding;
+
+ public function __construct()
+ {
+ $this->_xmlns = new PHPTAL_Dom_XmlnsState(array(), '');
+ }
+
+ public function getResult()
+ {
+ return $this->documentElement;
+ }
+
+ protected function getXmlnsState()
+ {
+ return $this->_xmlns;
+ }
+
+ // ~~~~~ XmlParser implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ public function onDocumentStart()
+ {
+ $this->documentElement = new PHPTAL_Dom_Element('documentElement', 'http://xml.zope.org/namespaces/tal', array(), $this->getXmlnsState());
+ $this->documentElement->setSource($this->file, $this->line);
+ $this->_current = $this->documentElement;
+ }
+
+ public function onDocumentEnd()
+ {
+ if (count($this->_stack) > 0) {
+ $left='</'.$this->_current->getQualifiedName().'>';
+ for ($i = count($this->_stack)-1; $i>0; $i--) $left .= '</'.$this->_stack[$i]->getQualifiedName().'>';
+ throw new PHPTAL_ParserException("Not all elements were closed before end of the document. Missing: ".$left,
+ $this->file, $this->line);
+ }
+ }
+
+ public function onDocType($doctype)
+ {
+ $this->pushNode(new PHPTAL_Dom_DocumentType($doctype, $this->encoding));
+ }
+
+ public function onXmlDecl($decl)
+ {
+ if (!$this->encoding) {
+ throw new PHPTAL_Exception("Encoding not set");
+ }
+ $this->pushNode(new PHPTAL_Dom_XmlDeclaration($decl, $this->encoding));
+ }
+
+ public function onComment($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_Comment($data, $this->encoding));
+ }
+
+ public function onCDATASection($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_CDATASection($data, $this->encoding));
+ }
+
+ public function onProcessingInstruction($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_ProcessingInstruction($data, $this->encoding));
+ }
+
+ public function onElementStart($element_qname, array $attributes)
+ {
+ $this->_xmlns = $this->_xmlns->newElement($attributes);
+
+ if (preg_match('/^([^:]+):/', $element_qname, $m)) {
+ $prefix = $m[1];
+ $namespace_uri = $this->_xmlns->prefixToNamespaceURI($prefix);
+ if (false === $namespace_uri) {
+ throw new PHPTAL_ParserException("There is no namespace declared for prefix of element < $element_qname >. You must have xmlns:$prefix declaration in the same document.",
+ $this->file, $this->line);
+ }
+ } else {
+ $namespace_uri = $this->_xmlns->getCurrentDefaultNamespaceURI();
+ }
+
+ $attrnodes = array();
+ foreach ($attributes as $qname=>$value) {
+
+ if (preg_match('/^([^:]+):(.+)$/', $qname, $m)) {
+ list(,$prefix, $local_name) = $m;
+ $attr_namespace_uri = $this->_xmlns->prefixToNamespaceURI($prefix);
+
+ if (false === $attr_namespace_uri) {
+ throw new PHPTAL_ParserException("There is no namespace declared for prefix of attribute $qname of element < $element_qname >. You must have xmlns:$prefix declaration in the same document.",
+ $this->file, $this->line);
+ }
+ } else {
+ $local_name = $qname;
+ $attr_namespace_uri = ''; // default NS. Attributes don't inherit namespace per XMLNS spec
+ }
+
+ if ($this->_xmlns->isHandledNamespace($attr_namespace_uri)
+ && !$this->_xmlns->isValidAttributeNS($attr_namespace_uri, $local_name)) {
+ throw new PHPTAL_ParserException("Attribute '$qname' is in '$attr_namespace_uri' namespace, but is not a supported PHPTAL attribute",
+ $this->file, $this->line);
+ }
+
+ $attrnodes[] = new PHPTAL_Dom_Attr($qname, $attr_namespace_uri, $value, $this->encoding);
+ }
+
+ $node = new PHPTAL_Dom_Element($element_qname, $namespace_uri, $attrnodes, $this->getXmlnsState());
+ $this->pushNode($node);
+ $this->_stack[] = $this->_current;
+ $this->_current = $node;
+ }
+
+ public function onElementData($data)
+ {
+ $this->pushNode(new PHPTAL_Dom_Text($data, $this->encoding));
+ }
+
+ public function onElementClose($qname)
+ {
+ if ($this->_current === $this->documentElement) {
+ throw new PHPTAL_ParserException("Found closing tag for < $qname > where there are no open tags",
+ $this->file, $this->line);
+ }
+ if ($this->_current->getQualifiedName() != $qname) {
+ throw new PHPTAL_ParserException("Tag closure mismatch, expected < /".$this->_current->getQualifiedName()." > (opened in line ".$this->_current->getSourceLine().") but found < /".$qname." >",
+ $this->file, $this->line);
+ }
+ $this->_current = array_pop($this->_stack);
+ if ($this->_current instanceof PHPTAL_Dom_Element) {
+ $this->_xmlns = $this->_current->getXmlnsState(); // restore namespace prefixes info to previous state
+ }
+ }
+
+ private function pushNode(PHPTAL_Dom_Node $node)
+ {
+ $node->setSource($this->file, $this->line);
+ $this->_current->appendChild($node);
+ }
+
+ public function setEncoding($encoding)
+ {
+ $this->encoding = $encoding;
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php b/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php
new file mode 100644
index 0000000..552462c
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/ProcessingInstruction.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * processing instructions, including <?php blocks
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_ProcessingInstruction extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if (preg_match('/^<\?(?:php|[=\s])/i', $this->getValueEscaped())) {
+ // block will be executed as PHP
+ $codewriter->pushHTML($this->getValueEscaped());
+ } else {
+ $codewriter->doEchoRaw("'<'");
+ $codewriter->pushHTML(substr($codewriter->interpolateHTML($this->getValueEscaped()), 1));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/SaxXmlParser.php b/lib/phptal/PHPTAL/Dom/SaxXmlParser.php
new file mode 100644
index 0000000..b59a26d
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/SaxXmlParser.php
@@ -0,0 +1,480 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * Simple sax like xml parser for PHPTAL
+ * ("Dom" in the class name comes from name of the directory, not mode of operation)
+ *
+ * At the time this parser was created, standard PHP libraries were not suitable
+ * (could not retrieve doctypes, xml declaration, problems with comments and CDATA).
+ *
+ * There are still some problems: XML parsers don't care about exact format of enties
+ * or CDATA sections (PHPTAL tries to preserve them),
+ * <?php ?> blocks are not allowed in attributes.
+ *
+ * This parser failed to enforce some XML well-formedness constraints,
+ * and there are ill-formed templates "in the wild" because of this.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @see PHPTAL_DOM_DocumentBuilder
+ */
+class PHPTAL_Dom_SaxXmlParser
+{
+ private $_file;
+ private $_line;
+ private $_source;
+
+ // available parser states
+ const ST_ROOT = 0;
+ const ST_TEXT = 1;
+ const ST_LT = 2;
+ const ST_TAG_NAME = 3;
+ const ST_TAG_CLOSE = 4;
+ const ST_TAG_SINGLE = 5;
+ const ST_TAG_ATTRIBUTES = 6;
+ const ST_TAG_BETWEEN_ATTRIBUTE = 7;
+ const ST_CDATA = 8;
+ const ST_COMMENT = 9;
+ const ST_DOCTYPE = 10;
+ const ST_XMLDEC = 11;
+ const ST_PREPROC = 12;
+ const ST_ATTR_KEY = 13;
+ const ST_ATTR_EQ = 14;
+ const ST_ATTR_QUOTE = 15;
+ const ST_ATTR_VALUE = 16;
+
+ const BOM_STR = "\xef\xbb\xbf";
+
+
+ static $state_names = array(
+ self::ST_ROOT => 'root node',
+ self::ST_TEXT => 'text',
+ self::ST_LT => 'start of tag',
+ self::ST_TAG_NAME => 'tag name',
+ self::ST_TAG_CLOSE => 'closing tag',
+ self::ST_TAG_SINGLE => 'self-closing tag',
+ self::ST_TAG_ATTRIBUTES => 'tag',
+ self::ST_TAG_BETWEEN_ATTRIBUTE => 'tag attributes',
+ self::ST_CDATA => 'CDATA',
+ self::ST_COMMENT => 'comment',
+ self::ST_DOCTYPE => 'doctype',
+ self::ST_XMLDEC => 'XML declaration',
+ self::ST_PREPROC => 'preprocessor directive',
+ self::ST_ATTR_KEY => 'attribute name',
+ self::ST_ATTR_EQ => 'attribute value',
+ self::ST_ATTR_QUOTE => 'quoted attribute value',
+ self::ST_ATTR_VALUE => 'unquoted attribute value',
+ );
+
+ private $input_encoding;
+ public function __construct($input_encoding)
+ {
+ $this->input_encoding = $input_encoding;
+ $this->_file = "<string>";
+ }
+
+ public function parseFile(PHPTAL_Dom_DocumentBuilder $builder, $src)
+ {
+ if (!file_exists($src)) {
+ throw new PHPTAL_IOException("file $src not found");
+ }
+ return $this->parseString($builder, file_get_contents($src), $src);
+ }
+
+ public function parseString(PHPTAL_Dom_DocumentBuilder $builder, $src, $filename = '<string>')
+ {
+ try
+ {
+ $builder->setEncoding($this->input_encoding);
+ $this->_file = $filename;
+
+ $this->_line = 1;
+ $state = self::ST_ROOT;
+ $mark = 0;
+ $len = strlen($src);
+
+ $quoteStyle = '"';
+ $tagname = "";
+ $attribute = "";
+ $attributes = array();
+
+ $customDoctype = false;
+
+ $builder->setSource($this->_file, $this->_line);
+ $builder->onDocumentStart();
+
+ $i=0;
+ // remove BOM (UTF-8 byte order mark)...
+ if (substr($src, 0, 3) === self::BOM_STR) {
+ $i=3;
+ }
+ for (; $i<$len; $i++) {
+ $c = $src[$i]; // Change to substr($src, $i, 1); if you want to use mb_string.func_overload
+
+ if ($c === "\n") $builder->setSource($this->_file, ++$this->_line);
+
+ switch ($state) {
+ case self::ST_ROOT:
+ if ($c === '<') {
+ $mark = $i; // mark tag start
+ $state = self::ST_LT;
+ } elseif (!self::isWhiteChar($c)) {
+ $this->raiseError("Characters found before beginning of the document! (wrap document in < tal:block > to avoid this error)");
+ }
+ break;
+
+ case self::ST_TEXT:
+ if ($c === '<') {
+ if ($mark != $i) {
+ $builder->onElementData($this->sanitizeEscapedText($this->checkEncoding(substr($src, $mark, $i-$mark))));
+ }
+ $mark = $i;
+ $state = self::ST_LT;
+ }
+ break;
+
+ case self::ST_LT:
+ if ($c === '/') {
+ $mark = $i+1;
+ $state = self::ST_TAG_CLOSE;
+ } elseif ($c === '?' and strtolower(substr($src, $i, 5)) === '?xml ') {
+ $state = self::ST_XMLDEC;
+ } elseif ($c === '?') {
+ $state = self::ST_PREPROC;
+ } elseif ($c === '!' and substr($src, $i, 3) === '!--') {
+ $state = self::ST_COMMENT;
+ } elseif ($c === '!' and substr($src, $i, 8) === '![CDATA[') {
+ $state = self::ST_CDATA;
+ $mark = $i+8; // past opening tag
+ } elseif ($c === '!' and strtoupper(substr($src, $i, 8)) === '!DOCTYPE') {
+ $state = self::ST_DOCTYPE;
+ } elseif (self::isWhiteChar($c)) {
+ $state = self::ST_TEXT;
+ } else {
+ $mark = $i; // mark node name start
+ $attributes = array();
+ $attribute = "";
+ $state = self::ST_TAG_NAME;
+ }
+ break;
+
+ case self::ST_TAG_NAME:
+ if (self::isWhiteChar($c) || $c === '/' || $c === '>') {
+ $tagname = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($tagname)) $this->raiseError("Invalid tag name '$tagname'");
+
+ if ($c === '/') {
+ $state = self::ST_TAG_SINGLE;
+ } elseif ($c === '>') {
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ } else /* isWhiteChar */ {
+ $state = self::ST_TAG_ATTRIBUTES;
+ }
+ }
+ break;
+
+ case self::ST_TAG_CLOSE:
+ if ($c === '>') {
+ $tagname = rtrim(substr($src, $mark, $i-$mark));
+ $builder->onElementClose($tagname);
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_TAG_SINGLE:
+ if ($c !== '>') {
+ $this->raiseError("Expected '/>', but found '/$c' inside tag < $tagname >");
+ }
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ $builder->onElementClose($tagname);
+ break;
+
+ case self::ST_TAG_BETWEEN_ATTRIBUTE:
+ case self::ST_TAG_ATTRIBUTES:
+ if ($c === '>') {
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ $builder->onElementStart($tagname, $attributes);
+ } elseif ($c === '/') {
+ $state = self::ST_TAG_SINGLE;
+ } elseif (self::isWhiteChar($c)) {
+ $state = self::ST_TAG_ATTRIBUTES;
+ } elseif ($state === self::ST_TAG_ATTRIBUTES && $this->isValidQName($c)) {
+ $mark = $i; // mark attribute key start
+ $state = self::ST_ATTR_KEY;
+ } else $this->raiseError("Unexpected character '$c' between attributes of < $tagname >");
+ break;
+
+ case self::ST_COMMENT:
+ if ($c === '>' && $i > $mark+4 && substr($src, $i-2, 2) === '--') {
+
+ if (preg_match('/^-|--|-$/', substr($src, $mark +4, $i-$mark+1 -7))) {
+ $this->raiseError("Ill-formed comment. XML comments are not allowed to contain '--' or start/end with '-': ".substr($src, $mark+4, $i-$mark+1-7));
+ }
+
+ $builder->onComment($this->checkEncoding(substr($src, $mark+4, $i-$mark+1-7)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_CDATA:
+ if ($c === '>' and substr($src, $i-2, 2) === ']]') {
+ $builder->onCDATASection($this->checkEncoding(substr($src, $mark, $i-$mark-2)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_XMLDEC:
+ if ($c === '?' && substr($src, $i, 2) === '?>') {
+ $builder->onXmlDecl($this->checkEncoding(substr($src, $mark, $i-$mark+2)));
+ $i++; // skip '>'
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_DOCTYPE:
+ if ($c === '[') {
+ $customDoctype = true;
+ } elseif ($customDoctype && $c === '>' && substr($src, $i-1, 2) === ']>') {
+ $customDoctype = false;
+ $builder->onDocType($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ } elseif (!$customDoctype && $c === '>') {
+ $customDoctype = false;
+ $builder->onDocType($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_PREPROC:
+ if ($c === '>' and substr($src, $i-1, 1) === '?') {
+ $builder->onProcessingInstruction($this->checkEncoding(substr($src, $mark, $i-$mark+1)));
+ $mark = $i+1; // mark text start
+ $state = self::ST_TEXT;
+ }
+ break;
+
+ case self::ST_ATTR_KEY:
+ if ($c === '=' || self::isWhiteChar($c)) {
+ $attribute = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($attribute)) {
+ $this->raiseError("Invalid attribute name '$attribute' in < $tagname >");
+ }
+ if (isset($attributes[$attribute])) {
+ $this->raiseError("Attribute $attribute in < $tagname > is defined more than once");
+ }
+
+ if ($c === '=') $state = self::ST_ATTR_VALUE;
+ else /* white char */ $state = self::ST_ATTR_EQ;
+ } elseif ($c === '/' || $c==='>') {
+ $attribute = substr($src, $mark, $i-$mark);
+ if (!$this->isValidQName($attribute)) {
+ $this->raiseError("Invalid attribute name '$attribute'");
+ }
+ $this->raiseError("Attribute $attribute does not have value (found end of tag instead of '=')");
+ }
+ break;
+
+ case self::ST_ATTR_EQ:
+ if ($c === '=') {
+ $state = self::ST_ATTR_VALUE;
+ } elseif (!self::isWhiteChar($c)) {
+ $this->raiseError("Attribute $attribute in < $tagname > does not have value (found character '$c' instead of '=')");
+ }
+ break;
+
+ case self::ST_ATTR_VALUE:
+ if (self::isWhiteChar($c)) {
+ } elseif ($c === '"' or $c === '\'') {
+ $quoteStyle = $c;
+ $state = self::ST_ATTR_QUOTE;
+ $mark = $i+1; // mark attribute real value start
+ } else {
+ $this->raiseError("Value of attribute $attribute in < $tagname > is not in quotes (found character '$c' instead of quote)");
+ }
+ break;
+
+ case self::ST_ATTR_QUOTE:
+ if ($c === $quoteStyle) {
+ $attributes[$attribute] = $this->sanitizeEscapedText($this->checkEncoding(substr($src, $mark, $i-$mark)));
+
+ // PHPTAL's code generator assumes input is escaped for double-quoted strings. Single-quoted attributes need to be converted.
+ // FIXME: it should be escaped at later stage.
+ $attributes[$attribute] = str_replace('"',"&quot;", $attributes[$attribute]);
+ $state = self::ST_TAG_BETWEEN_ATTRIBUTE;
+ }
+ break;
+ }
+ }
+
+ if ($state === self::ST_TEXT) // allows text past root node, which is in violation of XML spec
+ {
+ if ($i > $mark) {
+ $text = substr($src, $mark, $i-$mark);
+ if (!ctype_space($text)) $this->raiseError("Characters found after end of the root element (wrap document in < tal:block > to avoid this error)");
+ }
+ } else {
+ if ($state === self::ST_ROOT) {
+ $msg = "Document does not have any tags";
+ } else {
+ $msg = "Finished document in unexpected state: ".self::$state_names[$state]." is not finished";
+ }
+ $this->raiseError($msg);
+ }
+
+ $builder->onDocumentEnd();
+ }
+ catch(PHPTAL_TemplateException $e)
+ {
+ $e->hintSrcPosition($this->_file, $this->_line);
+ throw $e;
+ }
+ return $builder;
+ }
+
+ private function isValidQName($name)
+ {
+ $name = $this->checkEncoding($name);
+ return preg_match('/^([a-z_\x80-\xff]+[a-z0-9._\x80-\xff-]*:)?[a-z_\x80-\xff]+[a-z0-9._\x80-\xff-]*$/i', $name);
+ }
+
+ private function checkEncoding($str)
+ {
+ if ($str === '') return '';
+
+ if ($this->input_encoding === 'UTF-8') {
+
+ // $match expression below somehow triggers quite deep recurrency and stack overflow in preg
+ // to avoid this, check string bit by bit, omitting ASCII fragments.
+ if (strlen($str) > 200) {
+ $chunks = preg_split('/(?>[\x09\x0A\x0D\x20-\x7F]+)/',$str,null,PREG_SPLIT_NO_EMPTY);
+ foreach ($chunks as $chunk) {
+ if (strlen($chunk) < 200) {
+ $this->checkEncoding($chunk);
+ }
+ }
+ return $str;
+ }
+
+ // http://www.w3.org/International/questions/qa-forms-utf-8
+ $match = '[\x09\x0A\x0D\x20-\x7F]' // ASCII
+ . '|[\xC2-\xDF][\x80-\xBF]' // non-overlong 2-byte
+ . '|\xE0[\xA0-\xBF][\x80-\xBF]' // excluding overlongs
+ . '|[\xE1-\xEC\xEE\xEE][\x80-\xBF]{2}' // straight 3-byte (exclude FFFE and FFFF)
+ . '|\xEF[\x80-\xBE][\x80-\xBF]' // straight 3-byte
+ . '|\xEF\xBF[\x80-\xBD]' // straight 3-byte
+ . '|\xED[\x80-\x9F][\x80-\xBF]' // excluding surrogates
+ . '|\xF0[\x90-\xBF][\x80-\xBF]{2}' // planes 1-3
+ . '|[\xF1-\xF3][\x80-\xBF]{3}' // planes 4-15
+ . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'; // plane 16
+
+ if (!preg_match('/^(?:(?>'.$match.'))+$/s',$str)) {
+ $res = preg_split('/((?>'.$match.')+)/s',$str,null,PREG_SPLIT_DELIM_CAPTURE);
+ for($i=0; $i < count($res); $i+=2)
+ {
+ $res[$i] = self::convertBytesToEntities(array(1=>$res[$i]));
+ }
+ $this->raiseError("Invalid UTF-8 bytes: ".implode('', $res));
+ }
+ }
+ if ($this->input_encoding === 'ISO-8859-1') {
+
+ // http://www.w3.org/TR/2006/REC-xml11-20060816/#NT-RestrictedChar
+ $forbid = '/((?>[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F]+))/s';
+
+ if (preg_match($forbid, $str)) {
+ $str = preg_replace_callback($forbid, array('self', 'convertBytesToEntities'), $str);
+ $this->raiseError("Invalid ISO-8859-1 characters: ".$str);
+ }
+ }
+
+ return $str;
+ }
+
+ /**
+ * preg callback
+ * Changes all bytes to hexadecimal XML entities
+ *
+ * @param array $m first array element is used for input
+ *
+ * @return string
+ */
+ private static function convertBytesToEntities(array $m)
+ {
+ $m = $m[1]; $out = '';
+ for($i=0; $i < strlen($m); $i++)
+ {
+ $out .= '&#X'.strtoupper(dechex(ord($m[$i]))).';';
+ }
+ return $out;
+ }
+
+ /**
+ * This is where this parser violates XML and refuses to be an annoying bastard.
+ */
+ private function sanitizeEscapedText($str)
+ {
+ $str = str_replace('&apos;', '&#39;', $str); // PHP's html_entity_decode doesn't seem to support that!
+
+ /* <?php ?> blocks can't reliably work in attributes (due to escaping impossible in XML)
+ so they have to be converted into special TALES expression
+ */
+ $types = ini_get('short_open_tag')?'php|=|':'php';
+ $str = preg_replace_callback("/<\?($types)(.*?)\?>/", array('self', 'convertPHPBlockToTALES'), $str);
+
+ // corrects all non-entities and neutralizes potentially problematic CDATA end marker
+ $str = strtr(preg_replace('/&(?!(?:#x?[a-f0-9]+|[a-z][a-z0-9]*);)/i', '&amp;', $str), array('<'=>'&lt;', ']]>'=>']]&gt;'));
+
+ return $str;
+ }
+
+ private static function convertPHPBlockToTALES($m)
+ {
+ list(, $type, $code) = $m;
+ if ($type === '=') $code = 'echo '.$code;
+ return '${structure phptal-internal-php-block:'.rawurlencode($code).'}';
+ }
+
+ public function getSourceFile()
+ {
+ return $this->_file;
+ }
+
+ public function getLineNumber()
+ {
+ return $this->_line;
+ }
+
+ public static function isWhiteChar($c)
+ {
+ return strpos(" \t\n\r\0", $c) !== false;
+ }
+
+ protected function raiseError($errStr)
+ {
+ throw new PHPTAL_ParserException($errStr, $this->_file, $this->_line);
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/Text.php b/lib/phptal/PHPTAL/Dom/Text.php
new file mode 100644
index 0000000..f8ef2ab
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/Text.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+
+/**
+ * Document text data representation.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_Text extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ if ($this->getValueEscaped() !== '') {
+ $codewriter->pushHTML($codewriter->interpolateHTML($this->getValueEscaped()));
+ }
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/XmlDeclaration.php b/lib/phptal/PHPTAL/Dom/XmlDeclaration.php
new file mode 100644
index 0000000..e28dfb9
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/XmlDeclaration.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+
+/**
+ * XML declaration node.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ */
+class PHPTAL_Dom_XmlDeclaration extends PHPTAL_Dom_Node
+{
+ public function generateCode(PHPTAL_Php_CodeWriter $codewriter)
+ {
+ $codewriter->setXmlDeclaration($this->getValueEscaped());
+ $codewriter->doXmlDeclaration();
+ }
+}
diff --git a/lib/phptal/PHPTAL/Dom/XmlnsState.php b/lib/phptal/PHPTAL/Dom/XmlnsState.php
new file mode 100644
index 0000000..4e9288f
--- /dev/null
+++ b/lib/phptal/PHPTAL/Dom/XmlnsState.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * PHPTAL templating engine
+ *
+ * PHP Version 5
+ *
+ * @category HTML
+ * @package PHPTAL
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk>
+ * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
+ * @version SVN: $Id$
+ * @link http://phptal.org/
+ */
+/**
+ * Stores XMLNS aliases fluctuation in the xml flow.
+ *
+ * This class is used to bind a PHPTAL namespace to an alias, for example using
+ * xmlns:t="http://xml.zope.org/namespaces/tal" and later use t:repeat instead
+ * of tal:repeat.
+ *
+ * @package PHPTAL
+ * @subpackage Dom
+ * @author Laurent Bedubourg <lbedubourg@motion-twin.com>
+ */
+class PHPTAL_Dom_XmlnsState
+{
+ /** Create a new XMLNS state inheriting provided aliases. */
+ public function __construct(array $prefix_to_uri, $current_default)
+ {
+ $this->prefix_to_uri = $prefix_to_uri;
+ $this->current_default = $current_default;
+ }
+
+ public function prefixToNamespaceURI($prefix)
+ {
+ if ($prefix === 'xmlns') return 'http://www.w3.org/2000/xmlns/';
+ if ($prefix === 'xml') return 'http://www.w3.org/XML/1998/namespace';
+
+ // domdefs provides fallback for all known phptal ns
+ if (isset($this->prefix_to_uri[$prefix])) {
+ return $this->prefix_to_uri[$prefix];
+ } else {
+ return PHPTAL_Dom_Defs::getInstance()->prefixToNamespaceURI($prefix);
+ }
+ }
+
+ /** Returns true if $attName is a valid attribute name, false otherwise. */
+ public function isValidAttributeNS($namespace_uri, $local_name)
+ {
+ return PHPTAL_Dom_Defs::getInstance()->isValidAttributeNS($namespace_uri, $local_name);
+ }
+
+ public function isHandledNamespace($namespace_uri)
+ {
+ return PHPTAL_Dom_Defs::getInstance()->isHandledNamespace($namespace_uri);
+ }
+
+ /**
+ * Returns a new XmlnsState inheriting of $this if $nodeAttributes contains
+ * xmlns attributes, returns $this otherwise.
+ *
+ * This method is used by the PHPTAL parser to keep track of xmlns fluctuation for
+ * each encountered node.
+ */
+ public function newElement(array $nodeAttributes)
+ {
+ $prefix_to_uri = $this->prefix_to_uri;
+ $current_default = $this->current_default;
+
+ $changed = false;
+ foreach ($nodeAttributes as $qname => $value) {
+ if (preg_match('/^xmlns:(.+)$/', $qname, $m)) {
+ $changed = true;
+ list(, $prefix) = $m;
+ $prefix_to_uri[$prefix] = $value;
+ }
+
+ if ($qname == 'xmlns') {$changed=true;$current_default = $value;}
+ }
+
+ if ($changed) {
+ return new PHPTAL_Dom_XmlnsState($prefix_to_uri, $current_default);
+ } else {
+ return $this;
+ }
+ }
+
+ function getCurrentDefaultNamespaceURI()
+ {
+ return $this->current_default;
+ }
+
+ private $prefix_to_uri, $current_default;
+}