text:
<?php

namespace pQuery;
/**
 * Holds (x)html/xml tag information like tag name, attributes,
 * parent, children, self close, etc.
 *
 */
class DomNode implements IQuery
{

    /**
     * Element Node, used for regular elements
     */
    const NODE_ELEMENT = 0;
    /**
     * Text Node
     */
    const NODE_TEXT = 1;
    /**
     * Comment Node
     */
    const NODE_COMMENT = 2;
    /**
     * Conditional Node (<![if]> <![endif])
     */
    const NODE_CONDITIONAL = 3;
    /**
     * CDATA Node (<![CDATA[]]>
     */
    const NODE_CDATA = 4;
    /**
     * Doctype Node
     */
    const NODE_DOCTYPE = 5;
    /**
     * XML Node, used for tags that start with ?, like <?xml and <?php
     */
    const NODE_XML = 6;
    /**
     * ASP Node
     */
    const NODE_ASP = 7;

    #php4 Compatibility with PHP4, this gets changed to a regular var in release tool
    #static $NODE_TYPE = self::NODE_ELEMENT;
    #php4e
    #php5
    /**
     * Node type of class
     */
    const NODE_TYPE = self::NODE_ELEMENT;
    #php5e

    /**
     * Name of the selector class
     * @var string
     * @see select()
     */
    var $selectClass = 'pQuery\\HtmlSelector';
    /**
     * Name of the parser class
     * @var string
     * @see setOuterText()
     * @see setInnerText()
     */
    var $parserClass = 'pQuery\\Html5Parser';

    /**
     * Name of the class used for {@link addChild()}
     * @var string
     */
    var $childClass = __CLASS__;
    /**
     * Name of the class used for {@link addText()}
     * @var string
     */
    var $childClass_Text = 'pQuery\\TextNode';
    /**
     * Name of the class used for {@link addComment()}
     * @var string
     */
    var $childClass_Comment = 'pQuery\\CommentNode';
    /**
     * Name of the class used for {@link addContional()}
     * @var string
     */
    var $childClass_Conditional = 'pQuery\\ConditionalTagNode';
    /**
     * Name of the class used for {@link addCDATA()}
     * @var string
     */
    var $childClass_CDATA = 'pQuery\\CdataNode';
    /**
     * Name of the class used for {@link addDoctype()}
     * @var string
     */
    var $childClass_Doctype = 'pQuery\\DoctypeNode';
    /**
     * Name of the class used for {@link addXML()}
     * @var string
     */
    var $childClass_XML = 'pQuery\\XmlNode';
    /**
     * Name of the class used for {@link addASP()}
     * @var string
     */
    var $childClass_ASP = 'pQuery\\AspEmbeddedNode';

    /**
     * Parent node, null if none
     * @var DomNode
     * @see changeParent()
     */
    var $parent = null;

    /**
     * Attributes of node
     * @var array
     * @internal array('attribute' => 'value')
     * @internal Public for faster access!
     * @see getAttribute()
     * @see setAttribute()
     * @access private
     */
    var $attributes = array();

    /**
     * Namespace info for attributes
     * @var array
     * @internal array('tag' => array(array('ns', 'tag', 'ns:tag', index)))
     * @internal Public for easy outside modifications!
     * @see findAttribute()
     * @access private
     */
    var $attributes_ns = null;

    /**
     * Array of child nodes
     * @var array
     * @internal Public for faster access!
     * @see childCount()
     * @see getChild()
     * @see addChild()
     * @see deleteChild()
     * @access private
     */
    var $children = array();

    /**
     * Full tag name (including namespace)
     * @var string
     * @see getTagName()
     * @see getNamespace()
     */
    var $tag = '';

    /**
     * Namespace info for tag
     * @var array
     * @internal array('namespace', 'tag')
     * @internal Public for easy outside modifications!
     * @access private
     */
    var $tag_ns = null;

    /**
     * Is node a self closing node? No closing tag if true.
     * @var bool
     */
    var $self_close = false;

    /**
     * If self close, then this will be used to close the tag
     * @var string
     * @see $self_close
     */
    var $self_close_str = ' /';

    /**
     * Use short tags for attributes? If true, then attributes
     * with values equal to the attribute name will not output
     * the value, e.g. selected="selected" will be selected.
     * @var bool
     */
    var $attribute_shorttag = true;

    /**
     * Function map used for the selector filter
     * @var array
     * @internal array('root' => 'filter_root') will cause the
     * selector to call $this->filter_root at :root
     * @access private
     */
    var $filter_map = array(
        'root' => 'filter_root',
        'nth-child' => 'filter_nchild',
        'eq' => 'filter_nchild', //jquery (naming) compatibility
        'gt' => 'filter_gt',
        'lt' => 'filter_lt',
        'nth-last-child' => 'filter_nlastchild',
        'nth-of-type' => 'filter_ntype',
        'nth-last-of-type' => 'filter_nlastype',
        'odd' => 'filter_odd',
        'even' => 'filter_even',
        'every' => 'filter_every',
        'first-child' => 'filter_first',
        'last-child' => 'filter_last',
        'first-of-type' => 'filter_firsttype',
        'last-of-type' => 'filter_lasttype',
        'only-child' => 'filter_onlychild',
        'only-of-type' => 'filter_onlytype',
        'empty' => 'filter_empty',
        'not-empty' => 'filter_notempty',
        'has-text' => 'filter_hastext',
        'no-text' => 'filter_notext',
        'lang' => 'filter_lang',
        'contains' => 'filter_contains',
        'has' => 'filter_has',
        'not' => 'filter_not',
        'element' => 'filter_element',
        'text' => 'filter_text',
        'comment' => 'filter_comment',
        'checked' => 'filter_checked',
        'selected' => 'filter_selected',
    );

    /**
     * Class constructor
     * @param string|array $tag Name of the tag, or array with taginfo (array(
     *    'tag_name' => 'tag',
     *    'self_close' => false,
     *    'attributes' => array('attribute' => 'value')))
     * @param DomNode $parent Parent of node, null if none
     */
    function __construct($tag, $parent)
    {
        $this->parent = $parent;

        if (is_string($tag)) {
            $this->tag = $tag;
        } else {
            $this->tag = $tag['tag_name'];
            $this->self_close = $tag['self_close'];
            $this->attributes = $tag['attributes'];
        }
    }

    #php4 PHP4 class constructor compatibility
    #function DomNode($tag, $parent) {return $this->__construct($tag, $parent);}
    #php4e

    /**
     * Class destructor
     * @access private
     */
    function __destruct()
    {
        $this->delete();
    }

    /**
     * Class toString, outputs {@link $tag}
     * @return string
     * @access private
     */
    function __toString()
    {
        return (($this->tag === '~root~') ? $this->toString(true, true, 1) : $this->tag);
    }

    /**
     * Class magic get method, outputs {@link getAttribute()}
     * @return string
     * @access private
     */
    function __get($attribute)
    {
        return $this->getAttribute($attribute);
    }

    /**
     * Class magic set method, performs {@link setAttribute()}
     * @access private
     */
    function __set($attribute, $value)
    {
        $this->setAttribute($attribute, $value);
    }

    /**
     * Class magic isset method, returns {@link hasAttribute()}
     * @return bool
     * @access private
     */
    function __isset($attribute)
    {
        return $this->hasAttribute($attribute);
    }

    /**
     * Class magic unset method, performs {@link deleteAttribute()}
     * @access private
     */
    function __unset($attribute)
    {
        return $this->deleteAttribute($attribute);
    }

    /**
     * Class magic invoke method, performs {@link query()}.
     * @param string $query The css query to run on the nodes.
     * @return \pQuery
     */
    function __invoke($query = '*')
    {
        return $this->query($query);
    }

    /**
     * Returns place in document
     * @return string
     */
    function dumpLocation()
    {
        return (($this->parent) ? (($p = $this->parent->dumpLocation()) ? $p . ' > ' : '') . $this->tag . '(' . $this->typeIndex() . ')' : '');
    }

    /**
     * Returns all the attributes and their values
     * @return string
     * @access private
     */
    protected function toString_attributes()
    {
        $s = '';
        foreach ($this->attributes as $a => $v) {
            $s .= ' ' . $a;
            if ((!$this->attribute_shorttag) || ($v !== $a)) {
                $quote = (strpos($v, '"') === false) ? '"' : "'";
                $s .= '=' . $quote . $v . $quote;
            }
        }
        return $s;
    }

    /**
     * Returns the content of the node (child tags and text)
     * @param bool $attributes Print attributes of child tags
     * @param bool|int $recursive How many sublevels of childtags to print. True for all.
     * @param bool $content_only Only print text, false will print tags too.
     * @return string
     * @access private
     */
    protected function toString_content($attributes = true, $recursive = true, $content_only = false)
    {
        $s = '';
        foreach ($this->children as $c) {
            $s .= $c->toString($attributes, $recursive, $content_only);
        }
        return $s;
    }

    /**
     * Returns the node as string
     * @param bool $attributes Print attributes (of child tags)
     * @param bool|int $recursive How many sub-levels of child tags to print. True for all.
     * @param bool|int $content_only Only print text, false will print tags too.
     * @return string
     */
    function toString($attributes = true, $recursive = true, $content_only = false)
    {
        if ($content_only) {
            if (is_int($content_only)) {
                --$content_only;
            }
            return $this->toString_content($attributes, $recursive, $content_only);
        }

        $s = '<' . $this->tag;
        if ($attributes) {
            $s .= $this->toString_attributes();
        }
        if ($this->self_close) {
            $s .= $this->self_close_str . '>';
        } else {
            $s .= '>';
            if ($recursive) {
                $s .= $this->toString_content($attributes);
            }
            $s .= '</' . $this->tag . '>';
        }
        return $s;
    }

    /**
     * Similar to JavaScript outerText, will return full (html formatted) node
     * @return string
     */
    function getOuterText()
    {
        return html_entity_decode($this->toString(), ENT_QUOTES);
    }

    /**
     * Similar to JavaScript outerText, will replace node (and child nodes) with new text
     * @param string $text
     * @param HtmlParserBase $parser Null to auto create instance
     * @return bool|array True on succeed, array with errors on failure
     */
    function setOuterText($text, $parser = null)
    {
        if (trim($text)) {
            $index = $this->index();
            if ($parser === null) {
                $parser = new $this->parserClass();
            }
            $parser->setDoc($text);
            $parser->parse_all();
            $parser->root->moveChildren($this->parent, $index);
        }
        $this->delete();
        return (($parser && $parser->errors) ? $parser->errors : true);
    }

    /**
     * Return html code of node
     * @internal jquery (naming) compatibility
     * @param string|null $value The value to set or null to get the value.
     * @see toString()
     * @return string
     */
    function html($value = null)
    {
        if ($value !== null) {
            $this->setInnerText($value);
        }
        return $this->getInnerText();
    }

    /**
     * Similar to JavaScript innerText, will return (html formatted) content
     * @return string
     */
    function getInnerText()
    {
        return html_entity_decode($this->toString(true, true, 1), ENT_QUOTES);
    }

    /**
     * Similar to JavaScript innerText, will replace child nodes with new text
     * @param string $text
     * @param HtmlParserBase $parser Null to auto create instance
     * @return bool|array True on succeed, array with errors on failure
     */
    function setInnerText($text, $parser = null)
    {
        $this->clear();
        if (trim($text)) {
            if ($parser === null) {
                $parser = new $this->parserClass();
            }
            $parser->root =& $this;
            $parser->setDoc($text);
            $parser->parse_all();
        }
        return (($parser && $parser->errors) ? $parser->errors : true);
    }

    /**
     * Similar to JavaScript plainText, will return text in node (and subnodes)
     * @return string
     */
    function getPlainText()
    {
        return preg_replace('`\s+`', ' ', html_entity_decode($this->toString(true, true, true), ENT_QUOTES));
    }

    /**
     * Return plaintext taking document encoding into account
     * @return string
     */
    function getPlainTextUTF8()
    {
        $txt = $this->toString(true, true, true);
        $enc = $this->getEncoding();
        if ($enc !== false) {
            $txt = mb_convert_encoding($txt, 'UTF-8', $enc);
        }
        return preg_replace('`\s+`', ' ', html_entity_decode($txt, ENT_QUOTES, 'UTF-8'));
    }

    /**
     * Similar to JavaScript plainText, will replace child nodes with new text (literal)
     * @param string $text
     */
    function setPlainText($text)
    {
        $this->clear();
        if (trim($text)) {
            $this->addText(htmlentities($text, ENT_QUOTES));
        }
    }

    /**
     * Delete node from parent and clear node
     */
    function delete()
    {
        if (($p = $this->parent) !== null) {
            $this->parent = null;
            $p->deleteChild($this);
        } else {
            $this->clear();
        }
    }

    /**
     * Detach node from parent
     * @param bool $move_children_up Only detach current node and replace it with child nodes
     * @internal jquery (naming) compatibility
     * @see delete()
     */
    function detach($move_children_up = false)
    {
        if (($p = $this->parent) !== null) {
            $index = $this->index();
            $this->parent = null;

            if ($move_children_up) {
                $this->moveChildren($p, $index);
            }
            $p->deleteChild($this, true);
        }
    }

    /**
     * Deletes all child nodes from node
     */
    function clear()
    {
        foreach ($this->children as $c) {
            $c->parent = null;
            $c->delete();
        }
        $this->children = array();
    }

    /**
     * Get top parent
     * @return DomNode Root, null if node has no parent
     */
    function getRoot()
    {
        $r = $this->parent;
        $n = ($r === null) ? null : $r->parent;
        while ($n !== null) {
            $r = $n;
            $n = $r->parent;
        }

        return $r;
    }

    /**
     * Change parent
     * @param null|DomNode $to New parent, null if none
     * @param false|int $index Add child to parent if not present at index, false to not add, negative to count from end, null to append
     */
    #php4
    #function changeParent($to, &$index) {
    #php4e
    #php5
    function changeParent($to, &$index = null)
    {
        #php5e
        if ($this->parent !== null) {
            $this->parent->deleteChild($this, true);
        }
        $this->parent = $to;
        if ($index !== false) {
            $new_index = $this->index();
            if (!(is_int($new_index) && ($new_index >= 0))) {
                $this->parent->addChild($this, $index);
            }
        }
    }

    /**
     * Find out if node has (a certain) parent
     * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node, null to return if node has parent
     * @param bool $recursive
     * @return bool
     */
    function hasParent($tag = null, $recursive = false)
    {
        if ($this->parent !== null) {
            if ($tag === null) {
                return true;
            } elseif (is_string($tag)) {
                return (($this->parent->tag === $tag) || ($recursive && $this->parent->hasParent($tag)));
            } elseif (is_object($tag)) {
                return (($this->parent === $tag) || ($recursive && $this->parent->hasParent($tag)));
            }
        }

        return false;
    }

    /**
     * Find out if node is parent of a certain tag
     * @param DomNode|string $tag Match against parent, string to match tag, object to fully match node
     * @param bool $recursive
     * @return bool
     * @see hasParent()
     */
    function isParent($tag, $recursive = false)
    {
        return ($this->hasParent($tag, $recursive) === ($tag !== null));
    }

    /**
     * Find out if node is text
     * @return bool
     */
    function isText()
    {
        return false;
    }

    /**
     * Find out if node is comment
     * @return bool
     */
    function isComment()
    {
        return false;
    }

    /**
     * Find out if node is text or comment node
     * @return bool
     */
    function isTextOrComment()
    {
        return false;
    }

    /**
     * Move node to other node
     * @param DomNode $to New parent, null if none
     * @param int $new_index Add child to parent at index if not present, null to not add, negative to count from end
     * @internal Performs {@link changeParent()}
     */
    #php4
    #function move($to, &$new_index) {
    #php4e
    #php5
    function move($to, &$new_index = -1)
    {
        #php5e
        $this->changeParent($to, $new_index);
    }

    /**
     * Move child nodes to other node
     * @param DomNode $to New parent, null if none
     * @param int $new_index Add child to new node at index if not present, null to not add, negative to count from end
     * @param int $start Index from child node where to start wrapping, 0 for first element
     * @param int $end Index from child node where to end wrapping, -1 for last element
     */
    #php4
    #function moveChildren($to, &$new_index, $start = 0, $end = -1) {
    #php4e
    #php5
    function moveChildren($to, &$new_index = -1, $start = 0, $end = -1)
    {
        #php5e
        if ($end < 0) {
            $end += count($this->children);
        }
        for ($i = $start; $i <= $end; $i++) {
            $this->children[$start]->changeParent($to, $new_index);
        }
    }

    /**
     * Index of node in parent
     * @param bool $count_all True to count all tags, false to ignore text and comments
     * @return int -1 if not found
     */
    function index($count_all = true)
    {
        if (!$this->parent) {
            return -1;
        } elseif ($count_all) {
            return $this->parent->findChild($this);
        } else {
            $index = -1;
            //foreach($this->parent->children as &$c) {
            //	if (!$c->isTextOrComment()) {
            //		++$index;
            //	}
            //	if ($c === $this) {
            //		return $index;
            //	}
            //}

            foreach (array_keys($this->parent->children) as $k) {
                if (!$this->parent->children[$k]->isTextOrComment()) {
                    ++$index;
                }
                if ($this->parent->children[$k] === $this) {
                    return $index;
                }
            }
            return -1;
        }
    }

    /**
     * Change index of node in parent
     * @param int $index New index
     */
    function setIndex($index)
    {
        if ($this->parent) {
            if ($index > $this->index()) {
                --$index;
            }
            $this->delete();
            $this->parent->addChild($this, $index);
        }
    }

    /**
     * Index of all similar nodes in parent
     * @return int -1 if not found
     */
    function typeIndex()
    {
        if (!$this->parent) {
            return -1;
        } else {
            $index = -1;
            //foreach($this->parent->children as &$c) {
            //	if (strcasecmp($this->tag, $c->tag) === 0) {
            //		++$index;
            //	}
            //	if ($c === $this) {
            //		return $index;
            //	}
            //}

            foreach (array_keys($this->parent->children) as $k) {
                if (strcasecmp($this->tag, $this->parent->children[$k]->tag) === 0) {
                    ++$index;
                }
                if ($this->parent->children[$k] === $this) {
                    return $index;
                }
            }
            return -1;
        }
    }

    /**
     * Calculate indent of node (number of parent tags - 1)
     * @return int
     */
    function indent()
    {
        return (($this->parent) ? $this->parent->indent() + 1 : -1);
    }

    /**
     * Get sibling node
     * @param int $offset Offset from current node
     * @return DomNode Null if not found
     */
    function getSibling($offset = 1)
    {
        $index = $this->index() + $offset;
        if (($index >= 0) && ($index < $this->parent->childCount())) {
            return $this->parent->getChild($index);
        } else {
            return null;
        }
    }

    /**
     * Get node next to current
     * @param bool $skip_text_comments
     * @return DomNode Null if not found
     * @see getSibling()
     * @see getPreviousSibling()
     */
    function getNextSibling($skip_text_comments = true)
    {
        $offset = 1;
        while (($n = $this->getSibling($offset)) !== null) {
            if ($skip_text_comments && ($n->tag[0] === '~')) {
                ++$offset;
            } else {
                break;
            }
        }

        return $n;
    }

    /**
     * Get node previous to current
     * @param bool $skip_text_comments
     * @return DomNode Null if not found
     * @see getSibling()
     * @see getNextSibling()
     */
    function getPreviousSibling($skip_text_comments = true)
    {
        $offset = -1;
        while (($n = $this->getSibling($offset)) !== null) {
            if ($skip_text_comments && ($n->tag[0] === '~')) {
                --$offset;
            } else {
                break;
            }
        }

        return $n;
    }

    /**
     * Get namespace of node
     * @return string
     * @see setNamespace()
     */
    function getNamespace()
    {
        if ($this->tag_ns === null) {
            $a = explode(':', $this->tag, 2);
            if (empty($a[1])) {
                $this->tag_ns = array('', $a[0]);
            } else {
                $this->tag_ns = array($a[0], $a[1]);
            }
        }

        return $this->tag_ns[0];
    }

    /**
     * Set namespace of node
     * @param string $ns
     * @see getNamespace()
     */
    function setNamespace($ns)
    {
        if ($this->getNamespace() !== $ns) {
            $this->tag_ns[0] = $ns;
            $this->tag = $ns . ':' . $this->tag_ns[1];
        }
    }

    /**
     * Get tagname of node (without namespace)
     * @return string
     * @see setTag()
     */
    function getTag()
    {
        if ($this->tag_ns === null) {
            $this->getNamespace();
        }

        return $this->tag_ns[1];
    }

    /**
     * Set tag (with or without namespace)
     * @param string $tag
     * @param bool $with_ns Does $tag include namespace?
     * @see getTag()
     */
    function setTag($tag, $with_ns = false)
    {
        $with_ns = $with_ns || (strpos($tag, ':') !== false);
        if ($with_ns) {
            $this->tag = $tag;
            $this->tag_ns = null;
        } elseif ($this->getTag() !== $tag) {
            $this->tag_ns[1] = $tag;
            $this->tag = (($this->tag_ns[0]) ? $this->tag_ns[0] . ':' : '') . $tag;
        }
    }

    /**
     * Try to determine the encoding of the current tag
     * @return string|bool False if encoding could not be found
     */
    function getEncoding()
    {
        $root = $this->getRoot();
        if ($root !== null) {
            if ($enc = $root->select('meta[charset]', 0, true, true)) {
                return $enc->getAttribute("charset");
            } elseif ($enc = $root->select('"?xml"[encoding]', 0, true, true)) {
                return $enc->getAttribute("encoding");
            } elseif ($enc = $root->select('meta[content*="charset="]', 0, true, true)) {
                $enc = $enc->getAttribute("content");
                return substr($enc, strpos($enc, "charset=") + 8);
            }
        }

        return false;
    }

    /**
     * Number of children in node
     * @param bool $ignore_text_comments Ignore text/comments with calculation
     * @return int
     */
    function childCount($ignore_text_comments = false)
    {
        if (!$ignore_text_comments) {
            return count($this->children);
        } else {
            $count = 0;
            //foreach($this->children as &$c) {
            //	if (!$c->isTextOrComment()) {
            //		++$count;
            //	}
            //}

            foreach (array_keys($this->children) as $k) {
                if (!$this->children[$k]->isTextOrComment()) {
                    ++$count;
                }
            }
            return $count;
        }
    }

    /**
     * Find node in children
     * @param DomNode $child
     * @return int False if not found
     */
    function findChild($child)
    {
        return array_search($child, $this->children, true);
    }

    /**
     * Checks if node has another node as child
     * @param DomNode $child
     * @return bool
     */
    function hasChild($child)
    {
        return ((bool)findChild($child));
    }

    /**
     * Get childnode
     * @param int|DomNode $child Index, negative to count from end
     * @param bool $ignore_text_comments Ignore text/comments with index calculation
     * @return DomNode
     */
    function &getChild($child, $ignore_text_comments = false)
    {
        if (!is_int($child)) {
            $child = $this->findChild($child);
        } elseif ($child < 0) {
            $child += $this->childCount($ignore_text_comments);
        }

        if ($ignore_text_comments) {
            $count = 0;
            $last = null;
            //foreach($this->children as &$c) {
            //	if (!$c->isTextOrComment()) {
            //		if ($count++ === $child) {
            //			return $c;
            //		}
            //		$last = $c;
            //	}
            //}

            foreach (array_keys($this->children) as $k) {
                if (!$this->children[$k]->isTextOrComment()) {
                    if ($count++ === $child) {
                        return $this->children[$k];
                    }
                    $last = $this->children[$k];
                }
            }
            return (($child > $count) ? $last : null);
        } else {
            return $this->children[$child];
        }
    }

    /**
     * Add child node
     * @param string|DomNode $tag Tag name or object
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     */
    #php4
    #function &addChild($tag, &$offset) {
    #php4e
    #php5
    function &addChild($tag, &$offset = null)
    {
        #php5e
        if (is_array($tag)) {
            $tag = new $this->childClass($tag, $this);
        } elseif (is_string($tag)) {
            $nodes = $this->createNodes($tag);
            $tag = array_shift($nodes);

            if ($tag && $tag->parent !== $this) {
                $index = false;
                $tag->changeParent($this, $index);
            }
        } elseif (is_object($tag) && $tag->parent !== $this) {
            $index = false; //Needs to be passed by ref
            $tag->changeParent($this, $index);
        }

        if (is_int($offset) && ($offset < count($this->children)) && ($offset !== -1)) {
            if ($offset < 0) {
                $offset += count($this->children);
            }
            array_splice($this->children, $offset++, 0, array(&$tag));
        } else {
            $this->children[] =& $tag;
        }

        return $tag;
    }

    /**
     * First child node
     * @param bool $ignore_text_comments Ignore text/comments with index calculation
     * @return DomNode
     */
    function &firstChild($ignore_text_comments = false)
    {
        return $this->getChild(0, $ignore_text_comments);
    }

    /**
     * Last child node
     * @param bool $ignore_text_comments Ignore text/comments with index calculation
     * @return DomNode
     */
    function &lastChild($ignore_text_comments = false)
    {
        return $this->getChild(-1, $ignore_text_comments);
    }

    /**
     * Insert childnode
     * @param string|DomNode $tag Tagname or object
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    function &insertChild($tag, $index)
    {
        return $this->addChild($tag, $index);
    }

    /**
     * Add text node
     * @param string $text
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addText($text, &$offset) {
    #php4e
    #php5
    function &addText($text, &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_Text($this, $text), $offset);
    }

    /**
     * Add comment node
     * @param string $text
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addComment($text, &$offset) {
    #php4e
    #php5
    function &addComment($text, &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_Comment($this, $text), $offset);
    }

    /**
     * Add conditional node
     * @param string $condition
     * @param bool True for <!--[if, false for <![if
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addConditional($condition, $hidden = true, &$offset) {
    #php4e
    #php5
    function &addConditional($condition, $hidden = true, &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_Conditional($this, $condition, $hidden), $offset);
    }

    /**
     * Add CDATA node
     * @param string $text
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addCDATA($text, &$offset) {
    #php4e
    #php5
    function &addCDATA($text, &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_CDATA($this, $text), $offset);
    }

    /**
     * Add doctype node
     * @param string $dtd
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addDoctype($dtd, &$offset) {
    #php4e
    #php5
    function &addDoctype($dtd, &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_Doctype($this, $dtd), $offset);
    }

    /**
     * Add xml node
     * @param string $tag Tag name after "?", e.g. "php" or "xml"
     * @param string $text
     * @param array $attributes Array of attributes (array('attribute' => 'value'))
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addXML($tag = 'xml', $text = '', $attributes = array(), &$offset) {
    #php4e
    #php5
    function &addXML($tag = 'xml', $text = '', $attributes = array(), &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_XML($this, $tag, $text, $attributes), $offset);
    }

    /**
     * Add ASP node
     * @param string $tag Tag name after "%"
     * @param string $text
     * @param array $attributes Array of attributes (array('attribute' => 'value'))
     * @param int $offset Position to insert node, negative to count from end, null to append
     * @return DomNode Added node
     * @see addChild();
     */
    #php4
    #function &addASP($tag = '', $text = '', $attributes = array(), &$offset) {
    #php4e
    #php5
    function &addASP($tag = '', $text = '', $attributes = array(), &$offset = null)
    {
        #php5e
        return $this->addChild(new $this->childClass_ASP($this, $tag, $text, $attributes), $offset);
    }

    /**
     * Delete a child node
     * @param int|DomNode $child Child(index) to delete, negative to count from end
     * @param bool $soft_delete False to call {@link delete()} from child
     */
    function deleteChild($child, $soft_delete = false)
    {
        if (is_object($child)) {
            $child = $this->findChild($child);
        } elseif ($child < 0) {
            $child += count($this->children);
        }

        if (!is_bool($child)) {
            if (!$soft_delete) {
                $this->children[$child]->delete();
            }
            unset($this->children[$child]);

            //Rebuild indices
            $tmp = array();

            //foreach($this->children as &$c) {
            //	$tmp[] =& $c;
            //}
            foreach (array_keys($this->children) as $k) {
                $tmp[] =& $this->children[$k];
            }
            $this->children = $tmp;
        }
    }

    /**
     * Wrap node
     * @param string|DomNode $node Wrapping node, string to create new element node
     * @param int $wrap_index Index to insert current node in wrapping node, -1 to append
     * @param int $node_index Index to insert wrapping node, null to keep at same position
     * @return DomNode Wrapping node
     */
    function wrap($node, $wrap_index = -1, $node_index = null)
    {
        if ($node_index === null) {
            $node_index = $this->index();
        }

        if (!is_object($node)) {
            $node = $this->parent->addChild($node, $node_index);
        } elseif ($node->parent !== $this->parent) {
            $node->changeParent($this->parent, $node_index);
        }

        $this->changeParent($node, $wrap_index);
        return $node;
    }

    /**
     * Wrap child nodes
     * @param string|DomNode $node Wrapping node, string to create new element node
     * @param int $start Index from child node where to start wrapping, 0 for first element
     * @param int $end Index from child node where to end wrapping, -1 for last element
     * @param int $wrap_index Index to insert in wrapping node, -1 to append
     * @param int $node_index Index to insert current node, null to keep at same position
     * @return DomNode Wrapping node
     */
    function wrapInner($node, $start = 0, $end = -1, $wrap_index = -1, $node_index = null)
    {
        if ($end < 0) {
            $end += count($this->children);
        }
        if ($node_index === null) {
            $node_index = $end + 1;
        }

        if (!is_object($node)) {
            $node = $this->addChild($node, $node_index);
        } elseif ($node->parent !== $this) {
            $node->changeParent($this->parent, $node_index);
        }

        $this->moveChildren($node, $wrap_index, $start, $end);
        return $node;
    }

    /**
     * Number of attributes
     * @return int
     */
    function attributeCount()
    {
        return count($this->attributes);
    }

    /**
     * Find attribute using namespace, name or both
     * @param string|int $attr Negative int to count from end
     * @param string $compare "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     * @return array array('ns', 'attr', 'ns:attr', index)
     * @access private
     */
    protected function findAttribute($attr, $compare = 'total', $case_sensitive = false)
    {
        if (is_int($attr)) {
            if ($attr < 0) {
                $attr += count($this->attributes);
            }
            $keys = array_keys($this->attributes);
            return $this->findAttribute($keys[$attr], 'total', true);
        } else if ($compare === 'total') {
            $b = explode(':', $attr, 2);
            if ($case_sensitive) {
                $t =& $this->attributes;
            } else {
                $t = array_change_key_case($this->attributes);
                $attr = strtolower($attr);
            }

            if (isset($t[$attr])) {
                $index = 0;
                foreach ($this->attributes as $a => $v) {
                    if (($v === $t[$attr]) && (strcasecmp($a, $attr) === 0)) {
                        $attr = $a;
                        $b = explode(':', $attr, 2);
                        break;
                    }
                    ++$index;
                }

                if (empty($b[1])) {
                    return array(array('', $b[0], $attr, $index));
                } else {
                    return array(array($b[0], $b[1], $attr, $index));
                }
            } else {
                return false;
            }
        } else {
            if ($this->attributes_ns === null) {
                $index = 0;
                foreach ($this->attributes as $a => $v) {
                    $b = explode(':', $a, 2);
                    if (empty($b[1])) {
                        $this->attributes_ns[$b[0]][] = array('', $b[0], $a, $index);
                    } else {
                        $this->attributes_ns[$b[1]][] = array($b[0], $b[1], $a, $index);
                    }
                    ++$index;
                }
            }

            if ($case_sensitive) {
                $t =& $this->attributes_ns;
            } else {
                $t = array_change_key_case($this->attributes_ns);
                $attr = strtolower($attr);
            }

            if ($compare === 'namespace') {
                $res = array();
                foreach ($t as $ar) {
                    foreach ($ar as $a) {
                        if ($a[0] === $attr) {
                            $res[] = $a;
                        }
                    }
                }
                return $res;
            } elseif ($compare === 'name') {
                return ((isset($t[$attr])) ? $t[$attr] : false);
            } else {
                trigger_error('Unknown comparison mode');
            }
        }
    }

    /**
     * Checks if node has attribute
     * @param string|int $attr Negative int to count from end
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     * @return bool
     */
    function hasAttribute($attr, $compare = 'total', $case_sensitive = false)
    {
        return ((bool)$this->findAttribute($attr, $compare, $case_sensitive));
    }

    /**
     * Gets namespace of attribute(s)
     * @param string|int $attr Negative int to count from end
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     * @return string|array False if not found
     */
    function getAttributeNS($attr, $compare = 'name', $case_sensitive = false)
    {
        $f = $this->findAttribute($attr, $compare, $case_sensitive);
        if (is_array($f) && $f) {
            if (count($f) === 1) {
                return $this->attributes[$f[0][0]];
            } else {
                $res = array();
                foreach ($f as $a) {
                    $res[] = $a[0];
                }
                return $res;
            }
        } else {
            return false;
        }
    }

    /**
     * Sets namespace of attribute(s)
     * @param string|int $attr Negative int to count from end
     * @param string $namespace
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     * @return bool
     */
    function setAttributeNS($attr, $namespace, $compare = 'name', $case_sensitive = false)
    {
        $f = $this->findAttribute($attr, $compare, $case_sensitive);
        if (is_array($f) && $f) {
            if ($namespace) {
                $namespace .= ':';
            }
            foreach ($f as $a) {
                $val = $this->attributes[$a[2]];
                unset($this->attributes[$a[2]]);
                $this->attributes[$namespace . $a[1]] = $val;
            }
            $this->attributes_ns = null;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Gets value(s) of attribute(s)
     * @param string|int $attr Negative int to count from end
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     * @return string|array
     */
    function getAttribute($attr, $compare = 'total', $case_sensitive = false)
    {
        $f = $this->findAttribute($attr, $compare, $case_sensitive);
        if (is_array($f) && $f) {
            if (count($f) === 1) {
                return $this->attributes[$f[0][2]];
            } else {
                $res = array();
                foreach ($f as $a) {
                    $res[] = $this->attributes[$a[2]];
                }
                return $res;
            }
        } else {
            return null;
        }
    }

    /**
     * Sets value(s) of attribute(s)
     * @param string|int $attr Negative int to count from end
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     */
    function setAttribute($attr, $val, $compare = 'total', $case_sensitive = false)
    {
        if ($val === null) {
            return $this->deleteAttribute($attr, $compare, $case_sensitive);
        }

        $f = $this->findAttribute($attr, $compare, $case_sensitive);
        if (is_array($f) && $f) {
            foreach ($f as $a) {
                if(is_array($val)){
                    $val = '';
                }
                $this->attributes[$a[2]] = (string)$val;
            }
        } else {
            if(is_array($val)){
                $val = '';
            }
            $this->attributes[$attr] = (string)$val;
        }
    }

    /**
     * Add new attribute
     * @param string $attr
     * @param string $val
     */
    function addAttribute($attr, $val)
    {
        $this->setAttribute($attr, $val, 'total', true);
    }

    /**
     * Delete attribute(s)
     * @param string|int $attr Negative int to count from end
     * @param string $compare Find node using "namespace", "name" or "total"
     * @param bool $case_sensitive Compare with case sensitivity
     */
    function deleteAttribute($attr, $compare = 'total', $case_sensitive = false)
    {
        $f = $this->findAttribute($attr, $compare, $case_sensitive);
        if (is_array($f) && $f) {
            foreach ($f as $a) {
                unset($this->attributes[$a[2]]);
                if ($this->attributes_ns !== null) {
                    unset($this->attributes_ns[$a[1]]);
                }
            }
        }
    }

    /**
     * Determine if node has a certain class
     * @param string $className
     * @return bool
     */
    function hasClass($className)
    {
        return ($className && preg_match('`\b' . preg_quote($className) . '\b`si', $this->class));
    }

    /**
     * Add new class(es)
     * @param string|array $className
     */
    function addClass($className)
    {
        if (!is_array($className)) {
            $className = array($className);
        }
        $class = $this->class;
        foreach ($className as $c) {
            if (!(preg_match('`\b' . preg_quote($c) . '\b`si', $class) > 0)) {
                $class .= ' ' . $c;
            }
        }
        $this->class = trim($class);
    }

    /**
     * Remove clas(ses)
     * @param string|array $className
     */
    function removeClass($className)
    {
        if (!is_array($className)) {
            $className = array($className);
        }
        $class = $this->class;
        foreach ($className as $c) {
            $class = preg_replace('`\b' . preg_quote($c) . '\b`si', '', $class);
        }
        if ($class) {
            $this->class = $class;
        } else {
            unset($this->class);
        }
    }

    /**
     * Finds children using a callback function
     * @param callable $callback Function($node) that returns a bool
     * @param bool|int $recursive Check recursively
     * @param bool $check_self Include this node in search?
     * @return array
     */
    function getChildrenByCallback($callback, $recursive = true, $check_self = false)
    {
        $count = $this->childCount();
        if ($check_self && $callback($this)) {
            $res = array($this);
        } else {
            $res = array();
        }

        if ($count > 0) {
            if (is_int($recursive)) {
                $recursive = (($recursive > 1) ? $recursive - 1 : false);
            }

            for ($i = 0; $i < $count; $i++) {
                if ($callback($this->children[$i])) {
                    $res[] = $this->children[$i];
                }
                if ($recursive) {
                    $res = array_merge($res, $this->children[$i]->getChildrenByCallback($callback, $recursive));
                }
            }
        }

        return $res;
    }

    /**
     * Finds children using the {$link match()} function
     * @param $conditions See {$link match()}
     * @param $custom_filters See {$link match()}
     * @param bool|int $recursive Check recursively
     * @param bool $check_self Include this node in search?
     * @return array
     */
    function getChildrenByMatch($conditions, $recursive = true, $check_self = false, $custom_filters = array())
    {
        $count = $this->childCount();
        if ($check_self && $this->match($conditions, true, $custom_filters)) {
            $res = array($this);
        } else {
            $res = array();
        }

        if ($count > 0) {
            if (is_int($recursive)) {
                $recursive = (($recursive > 1) ? $recursive - 1 : false);
            }

            for ($i = 0; $i < $count; $i++) {
                if ($this->children[$i]->match($conditions, true, $custom_filters)) {
                    $res[] = $this->children[$i];
                }
                if ($recursive) {
                    $res = array_merge($res, $this->children[$i]->getChildrenByMatch($conditions, $recursive, false, $custom_filters));
                }
            }
        }

        return $res;
    }

    /**
     * Checks if tag matches certain conditions
     * @param array $tags array('tag1', 'tag2') or array(array(
     *    'tag' => 'tag1',
     *    'operator' => 'or'/'and',
     *    'compare' => 'total'/'namespace'/'name',
     *    'case_sensitive' => true))
     * @return bool
     * @internal Used by selector class
     * @see match()
     * @access private
     */
    protected function match_tags($tags)
    {
        $res = false;

        foreach ($tags as $tag => $match) {
            if (!is_array($match)) {
                $match = array(
                    'match' => $match,
                    'operator' => 'or',
                    'compare' => 'total',
                    'case_sensitive' => false
                );
            } else {
                if (is_int($tag)) {
                    $tag = $match['tag'];
                }
                if (!isset($match['match'])) {
                    $match['match'] = true;
                }
                if (!isset($match['operator'])) {
                    $match['operator'] = 'or';
                }
                if (!isset($match['compare'])) {
                    $match['compare'] = 'total';
                }
                if (!isset($match['case_sensitive'])) {
                    $match['case_sensitive'] = false;
                }
            }

            if (($match['operator'] === 'and') && (!$res)) {
                return false;
            } elseif (!($res && ($match['operator'] === 'or'))) {
                if ($match['compare'] === 'total') {
                    $a = $this->tag;
                } elseif ($match['compare'] === 'namespace') {
                    $a = $this->getNamespace();
                } elseif ($match['compare'] === 'name') {
                    $a = $this->getTag();
                }

                if ($match['case_sensitive']) {
                    $res = (($a === $tag) === $match['match']);
                } else {
                    $res = ((strcasecmp($a, $tag) === 0) === $match['match']);
                }
            }
        }

        return $res;
    }

    /**
     * Checks if attributes match certain conditions
     * @param array $attributes array('attr' => 'val') or array(array(
     *    'operator_value' => 'equals'/'='/'contains_regex'/etc
     *    'attribute' => 'attr',
     *    'value' => 'val',
     *    'match' => true,
     *    'operator_result' => 'or'/'and',
     *    'compare' => 'total'/'namespace'/'name',
     *    'case_sensitive' => true))
     * @return bool
     * @internal Used by selector class
     * @see match()
     * @access private
     */
    protected function match_attributes($attributes)
    {
        $res = false;

        foreach ($attributes as $attribute => $match) {
            if (!is_array($match)) {
                $match = array(
                    'operator_value' => 'equals',
                    'value' => $match,
                    'match' => true,
                    'operator_result' => 'or',
                    'compare' => 'total',
                    'case_sensitive' => false
                );
            } else {
                if (is_int($attribute)) {
                    $attribute = $match['attribute'];
                }
                if (!isset($match['match'])) {
                    $match['match'] = true;
                }
                if (!isset($match['operator_result'])) {
                    $match['operator_result'] = 'or';
                }
                if (!isset($match['compare'])) {
                    $match['compare'] = 'total';
                }
                if (!isset($match['case_sensitive'])) {
                    $match['case_sensitive'] = false;
                }
            }

            if (is_string($match['value']) && (!$match['case_sensitive'])) {
                $match['value'] = strtolower($match['value']);
            }

            if (($match['operator_result'] === 'and') && (!$res)) {
                return false;
            } elseif (!($res && ($match['operator_result'] === 'or'))) {
                $possibles = $this->findAttribute($attribute, $match['compare'], $match['case_sensitive']);

                $has = (is_array($possibles) && $possibles);
                $res = (($match['value'] === $has) || (($match['match'] === false) && ($has === $match['match'])));

                if ((!$res) && $has && is_string($match['value'])) {
                    foreach ($possibles as $a) {
                        $val = $this->attributes[$a[2]];
                        if (is_string($val) && (!$match['case_sensitive'])) {
                            $val = strtolower($val);
                        }

                        switch ($match['operator_value']) {
                            case '%=':
                            case 'contains_regex':
                                $res = ((preg_match('`' . $match['value'] . '`s', $val) > 0) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '|=':
                            case 'contains_prefix':
                                $res = ((preg_match('`\b' . preg_quote($match['value']) . '[\-\s]`s', $val) > 0) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '~=':
                            case 'contains_word':
                                $res = ((preg_match('`\s' . preg_quote($match['value']) . '\s`s', " $val ") > 0) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '*=':
                            case 'contains':
                                $res = ((strpos($val, $match['value']) !== false) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '$=':
                            case 'ends_with':
                                $res = ((substr($val, -strlen($match['value'])) === $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '^=':
                            case 'starts_with':
                                $res = ((substr($val, 0, strlen($match['value'])) === $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '!=':
                            case 'not_equal':
                                $res = (($val !== $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '=':
                            case 'equals':
                                $res = (($val === $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '>=':
                            case 'bigger_than':
                                $res = (($val >= $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            case '<=':
                            case 'smaller_than':
                                $res = (($val >= $match['value']) === $match['match']);
                                if ($res) break 1; else break 2;

                            default:
                                trigger_error('Unknown operator "' . $match['operator_value'] . '" to match attributes!');
                                return false;
                        }
                    }
                }
            }
        }

        return $res;
    }

    /**
     * Checks if node matches certain filters
     * @param array $tags array(array(
     *    'filter' => 'last-child',
     *    'params' => '123'))
     * @param array $custom_filters Custom map next to {@link $filter_map}
     * @return bool
     * @internal Used by selector class
     * @see match()
     * @access private
     */
    protected function match_filters($conditions, $custom_filters = array())
    {
        foreach ($conditions as $c) {
            $c['filter'] = strtolower($c['filter']);
            if (isset($this->filter_map[$c['filter']])) {
                if (!$this->{$this->filter_map[$c['filter']]}($c['params'])) {
                    return false;
                }
            } elseif (isset($custom_filters[$c['filter']])) {
                if (!call_user_func($custom_filters[$c['filter']], $this, $c['params'])) {
                    return false;
                }
            } else {
                trigger_error('Unknown filter "' . $c['filter'] . '"!');
                return false;
            }
        }

        return true;
    }

    /**
     * Checks if node matches certain conditions
     * @param array $tags array('tags' => array(tag_conditions), 'attributes' => array(attr_conditions), 'filters' => array(filter_conditions))
     * @param array $match Should conditions evaluate to true?
     * @param array $custom_filters Custom map next to {@link $filter_map}
     * @return bool
     * @internal Used by selector class
     * @see match_tags();
     * @see match_attributes();
     * @see match_filters();
     * @access private
     */
    function match($conditions, $match = true, $custom_filters = array())
    {
        $t = isset($conditions['tags']);
        $a = isset($conditions['attributes']);
        $f = isset($conditions['filters']);

        if (!($t || $a || $f)) {
            if (is_array($conditions) && $conditions) {
                foreach ($conditions as $c) {
                    if ($this->match($c, $match)) {
                        return true;
                    }
                }
            }

            return false;
        } else {
            if (($t && (!$this->match_tags($conditions['tags']))) === $match) {
                return false;
            }

            if (($a && (!$this->match_attributes($conditions['attributes']))) === $match) {
                return false;
            }

            if (($f && (!$this->match_filters($conditions['filters'], $custom_filters))) === $match) {
                return false;
            }

            return true;
        }
    }

    /**
     * Finds children that match a certain attribute
     * @param string $attribute
     * @param string $value
     * @param string $mode Compare mode, "equals", "|=", "contains_regex", etc.
     * @param string $compare "total"/"namespace"/"name"
     * @param bool|int $recursive
     * @return array
     */
    function getChildrenByAttribute($attribute, $value, $mode = 'equals', $compare = 'total', $recursive = true)
    {
        if ($this->childCount() < 1) {
            return array();
        }

        $mode = explode(' ', strtolower($mode));
        $match = ((isset($mode[1]) && ($mode[1] === 'not')) ? 'false' : 'true');

        return $this->getChildrenByMatch(
            array(
                'attributes' => array(
                    $attribute => array(
                        'operator_value' => $mode[0],
                        'value' => $value,
                        'match' => $match,
                        'compare' => $compare
                    )
                )
            ),
            $recursive
        );
    }

    /**
     * Finds children that match a certain tag
     * @param string $tag
     * @param string $compare "total"/"namespace"/"name"
     * @param bool|int $recursive
     * @return array
     */
    function getChildrenByTag($tag, $compare = 'total', $recursive = true)
    {
        if ($this->childCount() < 1) {
            return array();
        }

        $tag = explode(' ', strtolower($tag));
        $match = ((isset($tag[1]) && ($tag[1] === 'not')) ? 'false' : 'true');

        return $this->getChildrenByMatch(
            array(
                'tags' => array(
                    $tag[0] => array(
                        'match' => $match,
                        'compare' => $compare
                    )
                )
            ),
            $recursive
        );
    }

    /**
     * Finds all children using ID attribute
     * @param string $id
     * @param bool|int $recursive
     * @return array
     */
    function getChildrenByID($id, $recursive = true)
    {
        return $this->getChildrenByAttribute('id', $id, 'equals', 'total', $recursive);
    }

    /**
     * Finds all children using class attribute
     * @param string $class
     * @param bool|int $recursive
     * @return array
     */
    function getChildrenByClass($class, $recursive = true)
    {
        return $this->getChildrenByAttribute('class', $class, 'equals', 'total', $recursive);
    }

    /**
     * Finds all children using name attribute
     * @param string $name
     * @param bool|int $recursive
     * @return array
     */
    function getChildrenByName($name, $recursive = true)
    {
        return $this->getChildrenByAttribute('name', $name, 'equals', 'total', $recursive);
    }

    /**
     * Performs a css query on the node.
     * @param string $query
     * @return \pQuery | \pQuery[]
     */
    public function query($query = '*')
    {
        $select = $this->select($query);
        $result = new \pQuery((array)$select);
        return $result;
    }

    /**
     * Performs css query on node
     * @param string $query
     * @param int|bool $index True to return node instead of array if only 1 match,
     * false to return array, int to return match at index, negative int to count from end
     * @param bool|int $recursive
     * @param bool $check_self Include this node in search or only search child nodes
     * @return DomNode[]|DomNode Returns an array of matching {@link DomNode} objects
     *  or a single {@link DomNode} if `$index` is not false.
     */
    function select($query = '*', $index = false, $recursive = true, $check_self = false)
    {
        $s = new $this->selectClass($this, $query, $check_self, $recursive);
        $res = $s->result;
        unset($s);
        if (is_array($res) && ($index === true) && (count($res) === 1)) {
            return $res[0];
        } elseif (is_int($index) && is_array($res)) {
            if ($index < 0) {
                $index += count($res);
            }
            return ($index < count($res)) ? $res[$index] : null;
        } else {
            return $res;
        }
    }

    /**
     * Checks if node matches css query filter ":root"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_root()
    {
        return (strtolower($this->tag) === 'html');
    }

    /**
     * Checks if node matches css query filter ":nth-child(n)"
     * @param string $n 1-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_nchild($n)
    {
        return ($this->index(false) + 1 === (int)$n);
    }

    /**
     * Checks if node matches css query filter ":gt(n)"
     * @param string $n 0-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_gt($n)
    {
        return ($this->index(false) > (int)$n);
    }

    /**
     * Checks if node matches css query filter ":lt(n)"
     * @param string $n 0-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_lt($n)
    {
        return ($this->index(false) < (int)$n);
    }

    /**
     * Checks if node matches css query filter ":nth-last-child(n)"
     * @param string $n 1-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_nlastchild($n)
    {
        if ($this->parent === null) {
            return false;
        } else {
            return ($this->parent->childCount(true) - $this->index(false) === (int)$n);
        }
    }

    /**
     * Checks if node matches css query filter ":nth-of-type(n)"
     * @param string $n 1-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_ntype($n)
    {
        return ($this->typeIndex() + 1 === (int)$n);
    }

    /**
     * Checks if node matches css query filter ":nth-last-of-type(n)"
     * @param string $n 1-based index
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_nlastype($n)
    {
        if ($this->parent === null) {
            return false;
        } else {
            return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) - $this->typeIndex() === (int)$n);
        }
    }

    /**
     * Checks if node matches css query filter ":odd"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_odd()
    {
        return (($this->index(false) & 1) === 1);
    }

    /**
     * Checks if node matches css query filter ":even"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_even()
    {
        return (($this->index(false) & 1) === 0);
    }

    /**
     * Checks if node matches css query filter ":every(n)"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_every($n)
    {
        return (($this->index(false) % (int)$n) === 0);
    }

    /**
     * Checks if node matches css query filter ":first"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_first()
    {
        return ($this->index(false) === 0);
    }

    /**
     * Checks if node matches css query filter ":last"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_last()
    {
        if ($this->parent === null) {
            return false;
        } else {
            return ($this->parent->childCount(true) - 1 === $this->index(false));
        }
    }

    /**
     * Checks if node matches css query filter ":first-of-type"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_firsttype()
    {
        return ($this->typeIndex() === 0);
    }

    /**
     * Checks if node matches css query filter ":last-of-type"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_lasttype()
    {
        if ($this->parent === null) {
            return false;
        } else {
            return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) - 1 === $this->typeIndex());
        }
    }

    /**
     * Checks if node matches css query filter ":only-child"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_onlychild()
    {
        if ($this->parent === null) {
            return false;
        } else {
            return ($this->parent->childCount(true) === 1);
        }
    }

    /**
     * Checks if node matches css query filter ":only-of-type"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_onlytype()
    {
        if ($this->parent === null) {
            return false;
        } else {
            return (count($this->parent->getChildrenByTag($this->tag, 'total', false)) === 1);
        }
    }

    /**
     * Checks if node matches css query filter ":empty"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_empty()
    {
        return ($this->childCount() === 0);
    }

    /**
     * Checks if node matches css query filter ":not-empty"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_notempty()
    {
        return ($this->childCount() !== 0);
    }

    /**
     * Checks if node matches css query filter ":has-text"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_hastext()
    {
        return ($this->getPlainText() !== '');
    }

    /**
     * Checks if node matches css query filter ":no-text"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_notext()
    {
        return ($this->getPlainText() === '');
    }

    /**
     * Checks if node matches css query filter ":lang(s)"
     * @param string $lang
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_lang($lang)
    {
        return ($this->lang === $lang);
    }

    /**
     * Checks if node matches css query filter ":contains(s)"
     * @param string $text
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_contains($text)
    {
        return (strpos($this->getPlainTextUTF8(), $text) !== false);
    }

    /**
     * Checks if node matches css query filter ":has(s)"
     * @param string $selector
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_has($selector)
    {
        $s = $this->select((string)$selector, false);
        return (is_array($s) && (count($s) > 0));
    }

    /**
     * Checks if node matches css query filter ":not(s)"
     * @param string $selector
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_not($selector)
    {
        $s = $this->select((string)$selector, false, true, true);
        return ((!is_array($s)) || (array_search($this, $s, true) === false));
    }

    /**
     * Checks if node matches css query filter ":element"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_element()
    {
        return true;
    }

    /**
     * Checks if node matches css query filter ":text"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_text()
    {
        return false;
    }

    /**
     * Checks if a node matches css query filter ":checked"
     * @return bool
     * @see match()
     */
    protected function filter_checked()
    {
        $attr = $this->getAttribute('checked');
        if (is_array($attr))
            $attr = reset($attr);
        return strcasecmp($attr, 'checked') === 0;
    }

    /**
     * Checks if node matches css query filter ":comment"
     * @return bool
     * @see match()
     * @access private
     */
    protected function filter_comment()
    {
        return false;
    }

    /**
     * Checks if a node matches css query filter ":selected"
     * @return bool
     * @see match()
     */
    protected function filter_selected()
    {
        $attr = $this->getAttribute('selected');
        if (is_array($attr))
            $attr = reset($attr);

        return strcasecmp($attr, 'selected') === 0;
    }

    public function after($content)
    {
        $offset = $this->index() + 1;
        $parent = $this->parent;
        $nodes = $this->createNodes($content);

        foreach ($nodes as $node) {
            $node->changeParent($parent, $offset);
        }
        return $this;
    }

    /**
     * Create a {@link DomNode} from its string representation.
     * @param string|DomNode $content
     * @return DomNode
     */
    protected function createNode($content)
    {
        $nodes = $this->createNodes($content);
        return reset($nodes);
    }

    /**
     * Create an array of {@link DomNode} objects from their string representation.
     * @param string|DomNode $content
     * @return DomNode[]
     */
    protected function createNodes($content)
    {
        if (is_string($content)) {
            if (strpos($content, ' ') === false) {
                $nodes = array(new $this->childClass($content, $this));
            } else {
                $node = new $this->parserClass($content);
                $nodes = $node->root->children;
            }
        } else {
            $nodes = (array)$content;
        }
        return $nodes;
    }

    public function append($content)
    {
        $nodes = $this->createNodes($content);
        foreach ($nodes as $node) {
            $node->changeParent($this);
        }
        return $this;
    }

    public function attr($name, $value = null)
    {
        if ($value === null)
            return $this->getAttribute($name);

        $this->setAttribute($name, $value);
        return $this;
    }

    public function before($content)
    {
        $offset = $this->index();
        $parent = $this->parent;
        $nodes = $this->createNodes($content);

        foreach ($nodes as $node) {
            $node->changeParent($parent, $offset);
        }

        return $this;
    }

    public function count()
    {
        return 1;
    }

//   public function css($name, $value = null) {
//
//   }

    public function prepend($content = null)
    {
        $offset = 0;
        $parent = $this;
        $nodes = $this->createNodes($content);

        foreach ($nodes as $node) {
            $node->changeParent($parent, $offset);
        }

        return $this;
    }

    public function prop($name, $value = null)
    {
        switch (strtolower($name)) {
            case 'checked':
            case 'disabled':
            case 'selected':
                if ($value !== null) {
                    if ($value) {
                        $this->attr($name, $name);
                    } else {
                        $this->removeAttr($name);
                    }
                    return $this;
                }
                return $this->attr($name) == $name;
            case 'tagname':
                return $this->tagName($value);
        }
        // The property is not supported, degrade gracefully
        if ($value === null)
            return $this;
        else
            return null;
    }

    public function remove($selector = null)
    {
        if ($selector == null) {
            $this->delete();
        } else {
            $nodes = (array)$this->select($selector);
            foreach ($nodes as $node) {
                $node->delete();
            }
        }
    }

    public function removeAttr($name)
    {
        $this->deleteAttribute($name);

        return $this;
    }

    function replaceWith($content)
    {
        $node_index = $this->index();

        // Add the new node.
        $node = $this->createNode($content);
        $node->changeParent($this->parent, $node_index);

        // Remove this node.
        $this->remove();

        return $node;
    }

    /**
     * @param type $value
     * @return string|DomNode
     */
    public function tagName($value = null)
    {
        if ($value !== null) {
            $this->setTag($value);
            return $this;
        }
        return $this->getTag();
    }

    public function text($value = null)
    {
        if ($value === null)
            return $this->getPlainText();

        $this->setPlainText($value);
        return $this;
    }

    public function toggleClass($classname, $switch = null)
    {
        if ($switch === true) {
            $this->addClass($classname);
        } elseif ($switch === false) {
            $this->removeClass($classname);
        } else {
            if ($this->hasClass($classname))
                $this->removeClass($classname);
            else
                $this->addClass($classname);
        }
        return $this;
    }

    public function unwrap()
    {
        $this->parent->detach(true);
        return $this;
    }

    public function val($value = null)
    {
        switch (strtolower($this->tag)) {
            case 'select':
                if ($value === null) {
                    // Return the value of a selected child.
                    return $this->query('option:selected')->attr('value');
                } else {
                    // Select the option with the right value and deselect the others.
                    foreach ($this->query('option') as $option) {
                        if ($option->attr('value') == $value) {
                            $option->attr('selected', 'selected');
                        } else {
                            $option->removeAttr('selected');
                        }
                    }
                    return $this;
                }
            case 'textarea':
                if ($value === null) {
                    // Return the contents of the textarea.
                    return $this->getInnerText();
                } else {
                    // Set the contents of the textarea.
                    $this->setInnerText($value);
                    return $this;
                }
            case 'input':
                switch (strtolower($this->getAttribute('type'))) {
                    case 'checkbox':
                        if ($value === null)
                            return $this->prop('checked') ? $this->getAttribute('value') : null;
                        else {
                            if (!$value) {
                                $this->deleteAttribute('checked');
                            } else {
                                $this->setAttribute('value', $value);
                                $this->setAttribute('checked', 'checked');
                            }
                            return $this;
                        }
                }
        }

        // Other node types can just get/set the value attribute.
        if ($value !== null) {
            $this->setAttribute('value', $value);
            return $this;
        }
        return $this->getAttribute('value');
    }

}


text:
Q: Sitecore and WFFM, cant specify rules for fields I have no option to set rules when selecting my fields within my form using WFFM and the Forms Designer.
Is there any requirements of things to be insatlled to get the rules option enabled?..(DBM, analytics, anything?), or is there any setting within Sitecore that needs to be enabled or something?

A: Can you check to see if you can add rules in Sitecore Web Forms for Marketers for that particular field?

A: Please navigate on /sitecore/system/Settings/Rules/Web Form for Marketers/Tags/Default path and here you will get a multilist field with name Taxonomy please select tags what rules you want to apply and after that all Rule will start showing in your Rule Set Editor.

A: Please check if the Sitecore.Analytics.config file is disabled in your AppConfig\Include folder.

A: In the Sitecore.Xdb.config configuration file from the /App_Config/Include/ folder, there is an option for setting Tracking
<!--  XDB TRACKING ENABLED
      Determines whether tracking is enabled or not.
      Default value: true
-->
<setting name="Xdb.Tracking.Enabled" value="true" />

This needs to be true in order for the rule section to show up in the forms section.
Be aware that this option has license consequences according to https://doc.sitecore.net/sitecore_experience_platform/setting_up__maintaining/experience_management/configuring/configure_experience_management

A: Please refer below link if you are using these specific rules.
https://kb.sitecore.net/articles/932002
Below is the solution in link:

When creating forms using the Web Forms for Marketers module, it is
  possible to configure different rules for the form fields. Such rules
  can be used for hiding specific fields, setting field default values,
  etc. However, the rules may not work when using the conditions from
  the Item Information group, specifically the following ones:

*

*where the item ID compares to value

*where the item name compares to value

*where the item template is specific template

  For example, the following rule will not hide the field when rendering
  the form on the Home item: where the item name is equal to Home hide
  element
Solution
Apply the following patch to resolve the issue: Copy the
  Sitecore.Support.401148.dll assembly into the /bin folder of your
  solution. Change the Type field of the following items in the master
  database as specified below:
  /sitecore/system/Settings/Rules/Definitions/Elements/Item
  Information/Item ID:
  Sitecore.Support.Rules.Conditions.ItemConditions.ItemIdCondition,Sitecore.Support.401148
  /sitecore/system/Settings/Rules/Definitions/Elements/Item
  Information/Item Name:
  Sitecore.Support.Rules.Conditions.ItemConditions.ItemNameCondition,Sitecore.Support.401148
  /sitecore/system/Settings/Rules/Definitions/Elements/Item
  Information/Item Template:
  Sitecore.Support.Rules.Conditions.ItemConditions.WhenTemplateIs,Sitecore.Support.401148
  Publish the changes.

redpajama_set_name: RedPajamaStackExchange


Turmeric with BioPerine Review-Does It Really Work Or Scam?
GlucoNeuro+ Review-Aquil Labs Supplement Does It Really Work?
BP Optimizer Review-Supplement Does It Work? User Results!!
Fungus Hack Supplement Review-Scam Or Really Work?
Kachin Diabetes Solution Review-Does It Work? Tested User Results!!
Myco Nuker Review-Organic Fungus Nuker Really Work Or Scam?
Hearing X3 Review-Supplement Is it Risky?
Sugar Balance Review-David Pearson's Supplement Is it Risky?
Pure Greens Review-Read Before Buy Benefits & Side Effects!!
Gluco Type 2 Review-Phytage labs Blood Sugar Support Formula!!
BioLeptin Review-Supplement Does It Work? User Experience & Results!
Purefit Keto Review-Read Before Buying This Scam Diet Supplement!!
Keto 900 (Keto Plus) Review-Dietary Supplement Scam Or Legit?
Nutra Keto 24 Review-Read Before Buy This Scam Diet Supplement!!
Nutrition Hacks Total Trim 11 Keto Review-Tested Benefits & Side Effects!!
Neural Fusion Review-Does It Work?Tested User Results!!
Garcinia Cambogia Plus Review-Natural Weight Loss Pills!!
Joint N-11 Review-Supplement Does It Really Work?
Ultra Garcinia 360 Slim Review-Does It Work? User Results!!
Pure Natural Keto Review-Does It Work? Tested User Results!!


Could you make the same fatal mistake as many other women?
Yes, Netflix, I said it — Ted Bundy was hot. He was the picture-perfect image of a stereotypically attractive male: 5'10" with dark brown hair and bright blue eyes. And I cannot help but wonder: if I were alive during the 1970s, would I have fallen under the trap of Ted Bundy?
Would I have faced the same fate as Lynda Ann Healy, Donna Gail Mason, Susan Elaine Rancourt, Brenda Carol Bar, and many women?
Ted Bundy, from the outside, looked like the type of man that a father would want his daughter to date. He was an honors student, studying to be a lawyer, and worked at the local Suicide Hotline Crisis Center. From the outside, he looked absolutely perfect — And that's the problem.
It is important to admit that Ted Bundy was hot.
Think about the last time you were walking to class in the rain with an umbrella that cannot cover you and your backpack at the same time. Now, imagine if a boy drove by and rolled down his window, asking if you wanted a ride because he is also heading to campus for his Spanish class. He looks nice, like your typical college guy.
Would you get in the car? Would that be a fatal mistake?
It is important to admit that Ted Bundy was hot because, honestly, there are a lot of hot people in the world. Attractiveness does not necessarily mean trustworthy, even though we seem to associate the two together.
A kind face does not mean a kind heart or good intentions.
In the present day and age, we get into random people's cars through Uber and Lyft. We go on dates with random people through Tinder and Bumble. We blindly trust people, people who have not earned our trust.
I think that Ted Bundy was hot. If you also think that Ted Bundy was hot, admit it, and remember that the next time you are about to hop into the car with someone to escape the rain on the way to class.


text:
layout: page
title: HN Kotlin Bot
permalink: /hn-kotlin-bot/
---
![HN Vimmy Bot](/assets/HN_Kotlin_Bot_Image.png)

Hi, I'm HN Kotlin Bot. I live a simple life. Every hour I check [Hacker News](https://news.ycombinator.com/) for stories about [Kotlin Programming Language](https://kotlinlang.org/). When I find a story, I tweet it to all my fans.

#### Read More In Our [PRESS RELEASE](http://mikepland.com/kotlin/bots/hn-bots/hacker-news/2017/05/21/PRESS-RELEASE-Introducing-HN-Kotlin-Bot/)

## You should follow me if...

* Like Me, you're on Twitter
* Like Me, you love Hacker News
* Like Me, you love Kotlin

Follow @hn_kotlin_bot

## More about me

* I'm a [NodeJS](https://nodejs.org/) app
* I live in a [Raspberry Pi](https://www.raspberrypi.org/)
* Checkout my [Github Repo](https://github.com/lando2319/hn_vimmy_bot) to see what I am made of.

## In my free time

* I mostly just sit idle

## See where I live

![Vimmy Home](/assets/kotlinHome.png)

## Checkout my tweets

Tweets by HN_Kotlin_Bot

redpajama_set_name: RedPajamaGithub


I have written before about the natural shingle wetlands on Dungeness, a series of water bodies created by the sea on the east coast of Dungeness. As shingle is washed around the coast it sometimes forms natural hollows which, in time, find themselves separated from the sea by a broadening shingle beach and the early saline lagoons eventually become peat-filled fresh-water habitats like the Open Pits, until finally, on the eroding south-coast saline conditions return.
These are the dying days of Abnor Pit and its fringing saltmarsh of sea purslane Halimione portaculoides, near Galloways. It is progressively being buried by in-rolled shingle, with some of the material washed eastwards towards Dungeness Point, and this was all that remained at the weekend.
Another storm could bury the last square metre or two this winter, or it might persist for a few more years, the end of a structure that was probably formed in Roman / Anglo-Saxon times, and was significant enough to be given a local name.
As with a tumbled-down ruin it is sad to see the loss of such features. It forces you to consider the life that once lived there. Thirty years ago you could walk around the whole intact pit. It is tempting to propose better coast protection on the south-coast of Dungeness to preserve these ancient features, but that would be a mistake. A range of coastal species depend on such dynamic processes, and near the lifeboat station, a new pit has been formed courtesy of shingle washed from the retreating south coast. Welcome changes in coast management have allowed shingle to start accreting on the east coast again and the beach has extended seawards significantly in the last few years.
And within this new stretch of shingle beach a new pit has been formed, the first for centuries.
Our knowledge of the development of these wetlands has been informed by studying cores of the silt and peat accumulated in their beds, but the exact timeline is uncertain, limited by our inability to date key events precisely using radiocarbon or other methods of dating peat, silt and sandy cores. Here we have a rare opportunity to study and document the key processes of early pit development.
The early days of such a pit sees daily dry periods, as water drains through the porous shingle on the tidal cycle. However silt is starting to accumulate and eventually it will form a seal, perhaps allowing the creation of a more permanent saline lagoon.
And plant-life has found its way there with rapid colonisation of three saltmarsh plant species, glasswort Salicornia spp, annual sea blite Suaeda maritima and sea purslane.
So, the destruction of an ancient pit provides material for its replacement. Its survival depends on the vaguaries of winter storms and our desire to allow natural coastal processes. If the storms of the next few years fail to infill this basin, and sediment supply is maintained I look forward to witnessing a process that has not been seen at Dungeness for a very long time.
Thank you, Brian, for a truly fascinating and original article.


Our team of commercial HVAC specialists provide design and planning, installation and consultation to the commercial and industrial sector.
Flow Rate Ltd provide specialist assistance to facilities management companies, schools, care homes, hospitals as well as many other commercial and industrial business properties.
We understand that every company requires a unique solution. With a comprehensive understanding to all different commercial business properties, our team of HVAC specialists maintain and provide a safe, comfortable working environment.
Whether your company requires servicing and breakdown assistance, new installation requirements or a bespoke maintenance package, we are here to support and solve your problems when it comes to Heating, Ventilation and Air Conditioning.
Flow Rate Ltd are a team of Specialist engineers that provide these services to the commercial and industrial sector.
Gas Appliance Installation, Repairs and Servicing.
Plant Room Inspection and Maintenance.
Catering Gas Appliance Installation, Repairs and Servicing.
Air conditioning Installation, Repairs and Servicing.
Pipe fitting and welding services.
Pipe Insulation and Lagging service.
Water treatment, Disinfection / Chlorination services.
Specialising in commercial, industrial and domestic heating and hot water systems, Flow Rate Ltd Heating Specialists provide a service of the highest quality regardless to the size of the job. From commercial boiler installations to domestic boiler repairs we will use our experience and knowledge to meet your needs.
Why not check out our Video channel on YouTube. Here we demonstrate some of our work as well as providing easy to follow 'How to' videos. Our step by step guides will ensure your plumbing flows right! Don't forget to 'subscribe' for our regular updates.
Our team consists of qualified plumbers whose main focus is to solve the problems you encounter at work or at home. As well as heating services, we also offer general plumbing solutions to commercial and domestic properties alike.
No matter what the problem is, you can guarantee Flow Rate Ltd will provide a solution. For a free estimate, call Flow Rate Ltd Heating Specialists today!
"Finding a reliable plumber can be difficult but Flow Rate is the exception. My heating system was in need of a serious overhaul and Tony and Luke did a first rate job on it. Although it was a bigger job than originally expected they worked on into the evening to complete it - and for the quote provided.