* @author Kornel LesiƄski * @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 () 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 * with * 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('getLocalName().'>'); } else { $codewriter->pushHTML('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"); } }