parseTags($input); } /** * Parse out the template tags. * @param string $buffer The text to parse. * @return string The parsed text. * @internal */ private function parseTags($buffer = '') { // split up the buffer into tags and text $parts = preg_split("~<(/?)([^:]*):([a-z0-9_]+)((?: [^>]*)?)>~i", $buffer, -1, PREG_SPLIT_DELIM_CAPTURE); // create a node stack, this will keep track of // non-text nodes, and push our root node onto // it as the first node. $stack = array(); $stack[] = new RootNode(''); // loop through the split up parts of the buffer and // also increment a tracker $i = -1; while(isset($parts[++$i])) { // get the last node added to the stack $parent = end($stack); // 0 for text, the 1-4 for tag info $key = $i % 5; // stuff before if($key == 0) { $parent->buffer($parts[$i]); } // do we have a tag? if(isset($parts[$i+4])) { // get the tag info $closing = trim($parts[$i+1] == '/'); $prefix = trim($parts[$i+2]); $tag_name = trim($parts[$i+3]); $attribs = trim($parts[$i+4]); $non_closing = FALSE; // this tag is non-closing if($attribs != '' && $attribs{strlen($attribs)-1} == '/') { $non_closing = TRUE; $attribs = trim(substr($attribs, 0, -1)); } // closing tag if($closing) { $tag = array_pop($stack); $parent = end($stack); // right closing tag if($tag_name == $tag->getName()) { $parent->buffer($tag->parseTag()); // wrong closing tag } else { $parent->buffer(''); } // opening tag } else { // get the tag handler $tag = $this->getTagHandler($prefix, $tag_name); // parse out the attributes if($attribs != '') { preg_match_all('~(?P[a-z]+)="(?P[^"]*)"~i', $attribs, $attributes); $attributes = $this->parseTagAttributes($attributes); if(!empty($attributes)) { $tag->addAttributes($attributes); } } // non-closing node, parse it right away and add it to the parent // nodes buffer if($non_closing) { $parent->buffer($tag->parseTag()); // opening tag of a closing node, add it to the stack } else { $stack[] = $tag; } } } $i+= 4; } // parse out any tags that somehow weren't properly parsed. If there were // any tags that weren't properly closed, this will catch them. $parsed_buffer = ''; $temp_buffer = ''; while($node = array_pop($stack)) { // is this the root node? if($node instanceof RootNode) { // add any text back into it from malformed // tags then parse it. $node->buffer($temp_buffer); // parse the root node $parsed_buffer = $node->parseTag(); } // extract the buffers from the malformed tags // to put them back into the root node later. else { $temp_buffer .= $node->buffer(); } } return $parsed_buffer; } /** * Parse the tag attributes. * @param array $parts The tag attributes. * @return array An associative array of attributes => value * @internal */ private function parseTagAttributes(array $parts = array()) { $attributes = array(); foreach($parts['attrib'] as $i => $attrib) { $attributes[strtolower($attrib)] = $parts['value'][$i]; } return $attributes; } } /** * Exception for template nodes, gets called for missing attributes. */ class TemplateNodeException extends Exception { } /** * A template node, this is any xml tag that uses a namespace, * for example: */ abstract class TemplateNode implements Node, ArrayAccess { /** * The text in-between the opening and closing nodes od this tag. * @internal */ private $buffer = ''; /** * This tags name. * @internal */ private $name = ''; /** * An associative array of this tags attributes. * @internal */ private $attributes = array(); /** * Constructor, pass in this tags name. * @param string $name The tag name. */ public function __construct($name = '') { $this->name = $name; } /** * Add the tags attributes to the attributes array. * @param array $array An associative array of attribute=>value. */ public function addAttributes(array $array = array()) { $this->attributes = array_merge($this->attributes, $array); } /** * Add text to this tags buffer. * @param string $buffer The text to append to the buffer. * @see ClosingNode::$buffer * @internal */ public function buffer($buffer = FALSE) { if($buffer !== FALSE) { $this->buffer .= $buffer; } else { return $this->buffer; } } /** * Get the name of this tag. * @return string The tag name. */ public function getName() { return $this->name; } /** * Require that this tag be passed certain arguments. * * $this->requireAttributes('href', 'title', ...); * */ public function requireAttributes() { foreach(func_get_args() as $attrib) { if(!array_key_exists($attrib, $this->attributes)) { throw new TemplateNodeException("Missing attribute [$attrib] for tag [{$this->name}]."); } } } /** * Require at least one out of a number of attributes to exist. * * $this->requireOne('href', 'src'); * * @return boolean The first present attribute from the set found. */ public function requireOne() { $ret = FALSE; $attribs = func_get_args(); foreach($attribs as $attrib) { if(array_key_exists($attrib, $this->attributes)) { $ret = $attrib; break; } } if($ret === FALSE) { throw new TemplateNodeException("Missing at least one required attribute of set [". implode(',', $attribs) ."] for tag [{$this->name}]."); } return $ret; } /** * Get an attribute. * @param string $key The attribute name. * @return mixed The value of the attribute. * @see ArrayAccess */ public function offsetGet($key) { return $this->offsetExists($key) ? $this->attributes[$key] : NULL; } /** * Set an attribute. * @param string $key The attribute name. * @param mixed $val The value of the attribute to set. * @see ArrayAccess */ public function offsetSet($key, $val) { $this->attributes[$key] = $val; } /** * Check if an attribute exists. * @param string $key The attribute name. * @return boolean Whether or not the attribute exists. * @see ArrayAccess */ public function offsetExists($key) { return isset($this->attributes[$key]); } /** * Unset an attribute. * @param string $key The attribute name. * @see ArrayAccess */ public function offsetUnset($key) { unset($this->attributes[$key]); } } /** * Class to handle the root node in the template parser stack. */ class RootNode extends TemplateNode { public function parseTag() { return $this->buffer(); } } /** * Class to handle nodes that the parser doesn't recognize. */ class UnknownNode extends TemplateNode { public function parseTag() { return $this->buffer(); } }