public function filter(string $string): string
    {
        $result = '';

        $string = $this->filterEntities($string);
        $string = $this->filterMetaTags($string);

        // Current/last tag name
        $tagName = null;
        // Current/last attribute name
        $attrName = null;
        // Current context
        $context = self::CTX_TAG_CONTENT;
        // Expected context
        $expecting = null;

        // By default tag content treated as text.
        $ignoreTagContent = false;
        // By default attribute values NOT treated as text.
        $ignoreAttrValue = true;

        $length = mb_strlen($string);
        for ($i = 0; $i < $length; $i++) {
            $char = mb_substr($string, $i, 1);
            switch (true) {
                case '<' === $char:
                    $context = self::CTX_TAG_NAME;
                    $tagName = null;
                    $char = ' ';
                    break;

                case '>' === $char:
                    if ($this->isIgnoredTag($tagName)) {
                        $ignoreTagContent = true;
                    } elseif ('/' === $tagName[0]) {
                        $ignoreTagContent = false; // Restore to default state.
                    }
                    $context = self::CTX_TAG_CONTENT;
                    $expecting = null;
                    $char = ' ';
                    break;

                case ' ' === $char:
                case "\n" === $char:
                case "\t" === $char:
                    switch ($context) {
                        case self::CTX_TAG_NAME:
                            $context = self::CTX_TAG_ATTRS;
                            break;

                        case self::CTX_ATTR_NAME:
                            $context = self::CTX_TAG_ATTRS;
                            break;
                    }
                    break;

                case '=' === $char
                    && (self::CTX_ATTR_NAME === $context || self::CTX_TAG_ATTRS === $context):
                    $expecting = self::CTX_ATTR_VALUE;
                    $char = ' ';
                    break;

                case '"' === $char:
                case "'" === $char:
                    switch (true) {
                        case self::CTX_ATTR_VALUE === $expecting:
                            $context = self::CTX_ATTR_VALUE;
                            $ignoreAttrValue
                                = !in_array(strtolower($attrName), self::$textAttrs, true);
                            $expecting = null;
                            $char = ' ';
                            break;

                        case self::CTX_ATTR_VALUE === $context:
                            $context = self::CTX_TAG_ATTRS;
                            $char = ' ';
                            break;
                    }
                    break;

                default:
                    switch ($context) {
                        case self::CTX_TAG_NAME:
                            $tagName .= $char;
                            $char = ' ';
                            break;

                        /** @noinspection PhpMissingBreakStatementInspection */
                        case self::CTX_TAG_ATTRS:
                            $context = self::CTX_ATTR_NAME;
                            $attrName = null;
                        // no break needed
                        case self::CTX_ATTR_NAME:
                            $attrName .= $char;
                            $char = ' ';
                            break;

                        case self::CTX_ATTR_VALUE:
                            if ($ignoreAttrValue) {
                                $char = ' ';
                            }
                            break;

                        case self::CTX_TAG_CONTENT:
                            if ($ignoreTagContent) {
                                $char = ' ';
                            }
                            break;
                    }
            }
            $result .= $char;
        }

        return $result;
    }