Your IP : 216.73.217.80


Current Path : /home/jlgagfroyt/www/plugins/system/tabs/src/
Upload File :
Current File : /home/jlgagfroyt/www/plugins/system/tabs/src/Replace.php

<?php
/**
 * @package         Tabs
 * @version         8.3.0
 * 
 * @author          Peter van Westen <info@regularlabs.com>
 * @link            http://regularlabs.com
 * @copyright       Copyright © 2022 Regular Labs All Rights Reserved
 * @license         http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 */

namespace RegularLabs\Plugin\System\Tabs;

defined('_JEXEC') or die;

use Joomla\CMS\Factory as JFactory;
use Joomla\CMS\Router\Route as JRoute;
use Joomla\CMS\Uri\Uri as JUri;
use RegularLabs\Library\Alias as RL_Alias;
use RegularLabs\Library\Html as RL_Html;
use RegularLabs\Library\HtmlTag as RL_HtmlTag;
use RegularLabs\Library\PluginTag as RL_PluginTag;
use RegularLabs\Library\Protect as RL_Protect;
use RegularLabs\Library\RegEx as RL_RegEx;
use RegularLabs\Library\StringHelper as RL_String;
use RegularLabs\Library\Title as RL_Title;
use RegularLabs\Library\Uri as RL_Uri;

class Replace
{

    static $allitems = [];
    static $context  = '';
    static $ids      = [];
    static $matches  = [];
    static $setcount = 0;
    static $sets     = [];

    public static function getSets(&$string, $only_basic_details = false)
    {
        $regex = Params::getRegex();

        RL_RegEx::matchAll($regex, $string, $matches);

        if (empty($matches))
        {
            return [];
        }

        self::$sets = [];
        $set_ids    = [];


        foreach ($matches as $match)
        {
            if (substr($match['tag'], 0, 1) == '/')
            {
                if (empty($set_ids))
                {
                    continue;
                }

                $set_id = key($set_ids);

                array_pop($set_ids);

                if (empty($set_id))
                {
                    continue;
                }

                self::$sets[$set_id][0]->ending = $match[0];

                continue;
            }

            end($set_ids);

            $item = self::getSetItem($match, $set_ids, $only_basic_details);

            if ($only_basic_details)
            {
                if ( ! isset(self::$sets['basic']))
                {
                    self::$sets['basic'] = [];
                }

                self::$sets['basic'][] = $item;
                continue;
            }


            if ( ! isset(self::$sets[$item->set]))
            {
                self::$sets[$item->set] = [];
            }

            self::$sets[$item->set][] = $item;
        }


        return self::$sets;
    }

    public static function replaceTags(&$string, $area = 'article', $context = '')
    {
        if ( ! is_string($string) || $string == '')
        {
            return false;
        }

        self::$context = $context;

        // Check if tags are in the text snippet used for the search component
        if (strpos($context, 'com_search.') === 0)
        {
            $limit = explode('.', $context, 2);
            $limit = (int) array_pop($limit);

            $string_check = substr($string, 0, $limit);

            if ( ! RL_String::contains($string_check, Params::getTags(true)))
            {
                return false;
            }
        }

        $params = Params::get();

        // allow in component?
        if (RL_Protect::isRestrictedComponent($params->disabled_components ?? [], $area))
        {

            Protect::_($string);

            self::handlePrintPage($string);

            RL_Protect::unprotect($string);

            return true;
        }

        if ( ! RL_String::contains($string, Params::getTags(true)))
        {
            // Links with #tab-name or &tab=tab-name
            self::replaceLinks($string);

            return true;
        }

        Protect::_($string);

        [$start_tags, $end_tags] = Params::getTags();

        [$pre_string, $string, $post_string] = RL_Html::getContentContainingSearches(
            $string,
            $start_tags,
            $end_tags
        );

        if (JFactory::getApplication()->input->getInt('print', 0))
        {
            // Replace syntax with general html on print pages
            self::handlePrintPage($string);

            $string = $pre_string . $string . $post_string;

            RL_Protect::unprotect($string);

            return true;
        }

        $sets = self::getSets($string);
        self::initSets($sets);

        // Tag syntax: {tab ...}
        self::replaceSyntax($string, $sets);

        // Closing tag: {/tab}
        self::replaceClosingTag($string);

        // Links with #tab-name or &tab=tab-name
        self::replaceLinks($string);

        // Link tag {tablink ...}
        self::replaceLinkTag($string);

        $string = $pre_string . $string . $post_string;

        RL_Protect::unprotect($string);

        return true;
    }

    private static function addChildToParent($item)
    {
        if (empty($item->parent))
        {
            return;
        }

        [$parent_set, $parent_item] = $item->parent;

        if (empty(self::$sets[$parent_set]) || empty(self::$sets[$parent_set][$parent_item]))
        {
            return;
        }

        self::$sets[$parent_set][$parent_item]->children[] = $item->set;
    }

    private static function addClassBySetting($item, &$classes, $setting = '')
    {
        if (
            (empty($item->{$setting}) && empty(Params::get()->{$setting}))
            || (isset($item->{$setting}) && ! $item->{$setting})
        )
        {
            return;
        }

        $classes[] = $setting;
    }

    private static function addClassesBySettings($item, &$classes, $settings = [])
    {
        foreach ($settings as $setting)
        {
            self::addClassBySetting($item, $classes, $setting);
        }
    }

    private static function createId($alias)
    {
        $id = $alias;

        $i = 1;
        while (in_array($id, self::$ids))
        {
            $id = $alias . '-' . ++$i;
        }

        self::$ids[] = $id;

        return $id;
    }

    private static function findItemByMatch($id)
    {
        foreach (self::$allitems as $item)
        {
            if ( ! in_array($id, $item->matches))
            {
                continue;
            }

            return $item->id;
        }

        return false;
    }

    private static function getAccessLevels()
    {
        if ( ! is_null(self::$accesslevels))
        {
            return self::$accesslevels;
        }

        $user   = JFactory::getApplication()->getIdentity() ?: JFactory::getUser();
        $levels = $user->getAuthorisedViewLevels();

        $db = JFactory::getDbo();

        $query = $db->getQuery(true)
            ->select('LOWER(REPLACE(a.title, " ", ""))')
            ->from('#__viewlevels as a')
            ->where('a.id IN (\'' . implode('\',\'', $levels) . '\')');
        $db->setQuery($query);

        self::$accesslevels = $db->loadColumn();

        return self::$accesslevels;
    }

    private static function getItemClass($item, $mainclass = 'rl_tabs-tab nn_tabs-tab nav-item')
    {
        // nav-item used for Boootstrap 4
        $class = [$mainclass];

        if ($item->open)
        {
            $class[] = 'active';
        }

        if ( ! empty($item->mode))
        {
            $class[] = $item->mode == 'hover' ? 'hover' : 'click';
        }

        $class[] = trim($item->class);

        return trim(implode(' ', $class));
    }

    private static function getLinkAttributes($id)
    {
        return 'href="' . RL_Uri::get($id) . '"'
            . ' class="rl_tabs-link rl_tabs-link-' . $id . ' nn_tabs-link nn_tabs-link-' . $id . '"'
            . ' data-id="' . $id . '"';
    }

    private static function getMainClasses($item)
    {
        $params = Params::get();

        $classes = [
            'rl_tabs nn_tabs',
            $params->mainclass,
        ];

        if ( ! empty($item->mainclass))
        {
            $classes[] = $item->mainclass;
        }

        if ( ! empty($item->nooutline))
        {
            $item->outline_handles = false;
            $item->outline_content = false;
        }

        if ( ! empty($item->outline_handles) || ! empty($item->outline_content))
        {
            $item->nooutline = false;
        }

        $settings = [
            'nooutline',
            'outline_handles',
            'outline_content',
            'color_inactive_handles',
        ];
        self::addClassesBySettings($item, $classes, $settings);

        $align = isset($item->align) ? 'align_' . $item->align : Params::getAlignment();
        $position = 'top';

        $classes[] = $position;
        $classes[] = $align;

        $classes = array_diff($classes, ['']);

        return trim(implode(' ', $classes));
    }

    private static function getNav(&$items)
    {
        $html = [];

        $ul_extra = '';

        // Nav for non-mobile view
        $html[] = '<!--googleoff: index-->';
        $html[] = '<a id="rl_tabs-scrollto_' . $items[0]->set . '" class="anchor rl_tabs-scroll nn_tabs-scroll"></a>';
        $html[] = '<ul class="nav nav-tabs" id="set-rl_tabs-' . $items[0]->set . '" role="tablist"' . $ul_extra . '>';
        foreach ($items as $item)
        {
            $href            = '#' . $item->id;
            $title           = $item->title_full;
            $link_attributes = ' id="tab-' . $item->id . '"'
                . ' data-toggle="tab" data-id="' . $item->id . '"'
                . ' role="tab" aria-controls="' . $item->id . '"'
                . ' aria-selected="' . ($item->open ? 'true' : 'false') . '"';

            $class = 'rl_tabs-toggle nn_tabs-toggle';

            // nav-link used for Boootstrap 4
            $class .= ' nav-link';


            $onclick = '';

            $heading_attributes = '';

            if ( ! empty($item->heading_attributes))
            {
                $heading_attributes .= ' ' . $item->heading_attributes;
            }

            if ($item->haslink)
            {
                if (RL_RegEx::match('<a [^>]*href="(.*?)"', $title, $match))
                {
                    $href = $match[1];
                }

                // nav-link used for Boootstrap 4
                $class = 'rl_tabs-link nav-link';

                if (RL_RegEx::match('<a [^>]*class="(.*?)"', $title, $match))
                {
                    $class = trim($class . ' ' . $match[1]);
                }

                $link_attributes = '';

                if (RL_RegEx::match('<a ([^>]*)', $title, $match))
                {
                    $link_attributes = $match[1];
                    $link_attributes = trim(RL_RegEx::replace('(href|class)=".*?"', '', $link_attributes));
                }

                if ( ! empty($item->link_attributes))
                {
                    $link_attributes .= ' ' . $item->link_attributes;
                }

                $title = RL_RegEx::replace('<a .*?>(.*?)</a>', '\1', $title);
            }

            $link_title = $title !== $item->title
                ? ' title="' . htmlspecialchars($item->title) . '"'
                : '';

            $html[] = '<li class="' . self::getItemClass($item) . '" ' . $heading_attributes . '>'
                . '<a href="' . $href . '" class="' . $class . '"' . $link_title . $onclick . $link_attributes . '>'
                . '<span class="rl_tabs-toggle-inner nn_tabs-toggle-inner">'
                . $title
                . '</span>'
                . '</a>'
                . '</li>';
        }

        $html[] = '</ul>';
        $html[] = '<!--googleon: index-->';

        return implode("\n", $html);
    }

    /* <<< [PRO] <<< */

    private static function getParent($set_id, $level)
    {
        if (empty(self::$sets))
        {
            return false;
        }

        if (isset(self::$sets[$set_id]))
        {
            return self::$sets[$set_id][0]->parent;
        }

        reset(self::$sets);

        $previous_set   = current(self::$sets);
        $previous_level = $previous_set[0]->level;

        while ($previous_level >= $level)
        {
            $previous_set = prev(self::$sets);

            if (empty($previous_set))
            {
                end(self::$sets);

                return false;
            }

            $previous_level = $previous_set[0]->level;
        }

        end(self::$sets);
        end($previous_set);

        $parent_item = key($previous_set);

        return [$previous_set[$parent_item]->set, $parent_item];
    }

    private static function getPreHtml($item, $items, $first = 0)
    {
        if ( ! $first)
        {
            return '</div>';
        }

        $params = Params::get();
        $class  = self::getMainClasses($item);


        $html[] = '<div class="' . trim($class) . '" role="presentation">';
        $html[] = self::getNav($items);
        $html[] = '<div class="tab-content">';

        return implode("\n", $html);
    }

    private static function getResponsiveNav(&$items)
    {
    }

    private static function getSetItem($match, &$set_ids, $only_basic_details = false)
    {
        $item = (object) [];

        // Set the values from the tag
        $tag = RL_Title::clean($match['data'], false, false);
        self::setTagAttributes($item, $tag);

        if ($only_basic_details)
        {
            return $item;
        }

        $item->orig   = $match[0];
        $item->set_id = trim(str_replace('-', '_', $match['set_id']));

        // New set
        if (empty($set_ids) || current($set_ids) != $item->set_id)
        {
            self::$setcount++;
            $set_ids[self::$setcount . '.' . $item->set_id] = $item->set_id;
        }

        $item->set = array_search($item->set_id, array_reverse($set_ids));

        $item->level = self::getSetLevel($item->set, $set_ids);


        [$item->pre, $item->post] = RL_Html::cleanSurroundingTags(
            [$match['pre'], $match['post']],
            ['div', 'p', 'span', 'h[0-6]']
        );

        return $item;
    }

    private static function getSetLevel($set_id, $set_ids)
    {
        // Sets are still empty, so this is the first set
        if (empty(self::$sets))
        {
            return 1;
        }

        // Grab the level from the previous entry of this set
        if (isset(self::$sets[$set_id]))
        {
            return self::$sets[$set_id][0]->level;
        }

        // Look up the level of the previous set
        $previous_set_id = array_search(prev($set_ids), array_reverse($set_ids));

        // Grab the level from the previous entry of this set
        if (isset(self::$sets[$previous_set_id]))
        {
            return self::$sets[$previous_set_id][0]->level + 1;
        }

        return 1;
    }

    private static function getTagAttributes($string)
    {
        RL_PluginTag::protectSpecialChars($string);

        $is_old_syntax = (strpos($string, '|') !== false);

        if ($is_old_syntax)
        {
            // Fix some different old syntaxes
            $string = str_replace(
                [
                    '|alias:',
                    '|align_',
                ],
                [
                    '|alias=',
                    '|align=',
                ],
                $string
            );
        }

        RL_PluginTag::unprotectSpecialChars($string, true);

        $known_boolean_keys = [
            'open', 'active', 'opened', 'default',
            'scroll', 'noscroll',
            'nooutline', 'outline_handles', 'outline_content', 'color_inactive_handles',
        ];

        // Get the values from the tag
        $attributes = RL_PluginTag::getAttributesFromString($string, 'title', $known_boolean_keys);

        $key_aliases = [
            'title'              => ['name'],
            'title-opened'       => ['title-open', 'title-active'],
            'title-closed'       => ['title-close', 'title-inactive'],
            'open'               => ['active', 'opened', 'default'],
            'access'             => ['accesslevels', 'accesslevel'],
            'usergroup'          => ['usergroups', 'group', 'groups'],
            'position'           => ['positioning'],
            'align'              => ['alignment'],
            'heading_attributes' => ['li_attributes'],
            'link_attributes'    => ['a_attributes'],
            'body_attributes'    => ['content_attributes'],
        ];

        RL_PluginTag::replaceKeyAliases($attributes, $key_aliases);

        if ($is_old_syntax)
        {
            self::setPositionFromOldClasses($attributes);
        }

        return $attributes;
    }

    private static function getUserGroups()
    {
    }

    private static function handlePrintPage(&$string)
    {
        $sets = self::getSets($string);
        self::initSets($sets);

        $prefix = '';
        foreach ($sets as $items)
        {
            foreach ($items as $item)
            {
                $class = 'rl_tabs-print';

                if ($item->open)
                {
                    $class .= ' active';
                }

                $replace = $prefix . '<div id="' . $item->id . '" class="' . $class . '">'
                    . '<' . $item->title_tag . ' class="rl_tabs-title nn_tabs-title">'
                    . '<a id="anchor-' . $item->id . '" class="anchor"></a>'
                    . $item->title_full
                    . '</' . $item->title_tag . '>';

                $string = RL_String::replaceOnce($item->orig, $replace, $string);
                $prefix = '</div>';
            }
        }

        $regex = Params::getRegex('end');

        RL_RegEx::matchAll($regex, $string, $matches);

        $replace = '</div>';
        foreach ($matches as $match)
        {
            $string  = RL_String::replaceOnce($match[0], $replace, $string);
            $replace = '';
        }

        $regex = Params::getRegex('link');

        RL_RegEx::matchAll($regex, $string, $matches);

        foreach ($matches as $match)
        {
            $href   = RL_Uri::get($match['id']);
            $link   = '<a href="' . $href . '">' . $match['text'] . '</a>';
            $string = RL_String::replaceOnce($match[0], $link, $string);
        }
    }

    private static function hasAccess($item)
    {
    }

    private static function hasAccessByList($levels, $list)
    {
    }

    private static function initSets(&$sets)
    {
        $params = Params::get();

        $urlitem   = JFactory::getApplication()->input->get('tab');
        $itemcount = 0;

        foreach ($sets as $set_id => $items)
        {
            $opened_by_default = 0;


            foreach ($items as $i => $item)
            {
                $item->title      = trim($item->title ?? 'Tab');
                $item->title_full = $item->title;

                if (isset($item->{'title-opened'}) || isset($item->{'title-closed'}))
                {
                    $title_closed = $item->{'title-closed'} ?? $item->title;
                    $title_opened = $item->{'title-opened'} ?? $item->title;

                    // Set main title to the title-opened, otherwise to title-closed
                    $item->title = $title_opened ?: ($title_closed ?: $item->title);

                    // place the title-opened and title-closed in css controlled spans
                    $item->title_full = '<span class="rl_tabs-title-inactive nn_tabs-title-inactive">' . $title_closed . '</span>'
                        . '<span class="rl_tabs-title-active nn_tabs-title-active">' . $title_opened . '</span>';
                }

                $item->haslink = RL_RegEx::match('<a [^>]*>.*?</a>', $item->title);

                $item->title = RL_Title::clean($item->title, true);
                $item->title = $item->title ?: RL_HtmlTag::getAttributeValue('title', $item->title_full);
                $item->title = $item->title ?: RL_HtmlTag::getAttributeValue('alt', $item->title_full);

                $item->alias = RL_Alias::get($item->alias ?? $item->title);
                $item->alias = $item->alias ?: 'tab';

                $item->id    = self::createId($item->alias);
                $item->set   = (int) $set_id;
                $item->count = $i + 1;


                $set_keys = [
                    'class', 'open', 'output_title_tag', 'title_tag', 'onclick',
                ];
                foreach ($set_keys as $key)
                {
                    $item->{$key} = isset($item->{$key})
                        ? $item->{$key}
                        : ($params->{$key} ?? '');
                }

                $item->matches   = RL_Title::getUrlMatches([$item->id, $item->title]);
                $item->matches[] = ++$itemcount . '';
                $item->matches[] = $item->set . '.' . ($i + 1);
                $item->matches[] = $item->set . '-' . ($i + 1);

                $item->matches = array_unique($item->matches);
                $item->matches = array_diff($item->matches, self::$matches);
                self::$matches = array_merge(self::$matches, $item->matches);

                if (self::itemIsOpen($item, $urlitem, $i == 0))
                {
                    $opened_by_default = $i;
                }

                // Will be set after all items are checked based on the $opened_by_default id
                $item->open = false;

                $sets[$set_id][$i] = $item;
                self::$allitems[]  = $item;
            }

            self::setOpenItem($sets[$set_id], $opened_by_default);
        }
    }

    private static function itemIsOpen($item, $urlitem, $is_first = false)
    {

        if ($item->haslink)
        {
            return false;
        }

        if ( ! empty($item->close))
        {
            return false;
        }

        if (isset($item->open))
        {
            return $item->open;
        }

        if ($urlitem && in_array($urlitem, $item->matches))
        {
            return true;
        }

        if ($is_first)
        {
            return true;
        }

        return false;
    }

    private static function removeByAccess(&$string)
    {
    }

    private static function replaceAnchorLinks(&$string)
    {
        RL_RegEx::matchAll(
            '(?<link><a\s[^>]*href="(?<url>([^"]*)?)\#(?<id>[^"]*)"[^>]*>)(?<text>.*?)</a>',
            $string,
            $matches
        );

        if (empty($matches))
        {
            return;
        }

        self::replaceLinksMatches($string, $matches);
    }

    private static function replaceClosingTag(&$string)
    {
        $params = Params::get();
        $regex  = Params::getRegex('end');

        RL_RegEx::matchAll($regex, $string, $matches);

        if (empty($matches))
        {
            return;
        }

        foreach ($matches as $match)
        {
            $html = '</div></div></div>';


            if ($params->place_comments)
            {
                $html .= Protect::getCommentEndTag();
            }

            [$pre, $post] = RL_Html::cleanSurroundingTags([$match['pre'], $match['post']]);

            $html = $pre . $html . $post;

            $string = RL_String::replaceOnce($match[0], $html, $string);
        }
    }

    private static function replaceLinkTag(&$string)
    {
        $regex = Params::getRegex('link');

        RL_RegEx::matchAll($regex, $string, $matches);

        if (empty($matches))
        {
            return;
        }

        foreach ($matches as $match)
        {
            self::replaceLinkTagMatch($string, $match);
        }
    }

    private static function replaceLinkTagMatch(&$string, $match)
    {
        $params = Params::get();

        $id = RL_Alias::get($match['id']);

        if ( ! self::stringHasItem($string, $id))
        {
            $id_by_name = self::findItemByMatch($match['id']);
            $id_by_id   = self::findItemByMatch($id);
            $id         = $id_by_name ?: ($id_by_id ?: $id);
        }

        if ( ! self::stringHasItem($string, $id))
        {
            $html = '<a href="' . RL_Uri::get($id) . '">' . $match['text'] . '</a>';

            if ($params->place_comments)
            {
                $html = Protect::wrapInCommentTags($html);
            }

            $string = RL_String::replaceOnce($match[0], $html, $string);

            return;
        }

        $html = '<a ' . self::getLinkAttributes($id) . '>'
            . '<span class="rl_tabs-link-inner nn_tabs-link-inner">' . $match['text'] . '</span>'
            . '</a>';

        if ($params->place_comments)
        {
            $html = Protect::wrapInCommentTags($html);
        }

        $string = RL_String::replaceOnce($match[0], $html, $string);
    }

    private static function replaceLinks(&$string)
    {
        // Links with #tab-name
        self::replaceAnchorLinks($string);
        // Links with &tab=tab-name
        self::replaceUrlLinks($string);
    }

    private static function replaceLinksMatches(&$string, $matches)
    {
        $uri            = JUri::getInstance();
        $current_urls   = [];
        $current_urls[] = $uri->toString(['path']);
        $current_urls[] = $uri->toString(['scheme', 'host', 'path']);
        $current_urls[] = $uri->toString(['scheme', 'host', 'port', 'path']);

        foreach ($matches as $match)
        {
            $link = $match['link'];

            if (
                strpos($link, 'data-toggle=') !== false
                || strpos($link, 'onclick=') !== false
                || strpos($link, 'rl_tabs-toggle-sm') !== false
                || strpos($link, 'rl_tabs-link') !== false
                || strpos($link, 'rl_sliders-link') !== false
            )
            {
                continue;
            }

            $url = $match['url'];
            if (strpos($url, 'index.php/') === 0)
            {
                $url = '/' . $url;
            }

            if (strpos($url, 'index.php') === 0)
            {
                $url = JRoute::_($url);
            }

            if ($url != '' && ! in_array($url, $current_urls))
            {
                continue;
            }

            $id = $match['id'];

            if ( ! self::stringHasItem($string, $id))
            {
                // This is a link to a normal anchor or other element on the page
                // Remove the prepending obsolete url and leave the hash
                // $string = str_replace('href="' . $match['url'] . '#' . $id . '"', 'href="#' . $id . '"', $string);

                continue;
            }

            $attributes = self::getLinkAttributes($id);

            // Combine attributes with original
            $attributes = RL_HtmlTag::combineAttributes($link, $attributes);

            $html = '<a ' . $attributes . '><span class="rl_tabs-link-inner nn_tabs-link-inner">' . $match['text'] . '</span></a>';

            $string = str_replace($match[0], $html, $string);
        }
    }

    private static function replaceSyntax(&$string, $sets)
    {
        $regex = Params::getRegex('end');

        if ( ! RL_RegEx::match($regex, $string))
        {
            return;
        }

        foreach ($sets as $items)
        {
            self::replaceSyntaxItemList($string, $items);
        }
    }

    private static function replaceSyntaxItem(&$string, $item, $items, $first = 0)
    {
        if (strpos($string, $item->orig) === false)
        {
            return;
        }

        $params = Params::get();

        $html   = [];
        $html[] = $item->post;
        $html[] = $item->pre;

        if ($first && $params->place_comments)
        {
            $html[] = Protect::getCommentStartTag();
        }

        if ( ! in_array(self::$context, ['com_search.search', 'com_search.search.article', 'com_finder.indexer']))
        {
            $html[] = self::getPreHtml($item, $items, $first);
        }

        $class = self::getItemClass($item, 'tab-pane rl_tabs-pane nn_tabs-pane');

        $body_attributes = 'role="tabpanel"'
            . ' aria-labelledby="tab-' . $item->id . '"';
        if ( ! $item->open)
        {
            $body_attributes .= ' hidden="hidden"';
        }
        if ( ! empty($item->body_attributes))
        {
            $body_attributes .= ' ' . $item->body_attributes;
        }

        $html[] = '<div class="' . trim($class) . '" id="' . $item->id . '" ' . $body_attributes . '>';

        if ( ! $item->haslink)
        {
            if ($item->output_title_tag)
            {
                $html[] = '<' . $item->title_tag . ' class="rl_tabs-title nn_tabs-title">';
            }

            $class = 'anchor';

            $html[] = '<a id="anchor-' . $item->id . '" class="' . $class . '"></a>';

            if ($item->output_title_tag)
            {
                $html[] = $item->title . '</' . $item->title_tag . '>';
            }
        }

        $html = implode("\n", $html);

        $string = RL_String::replaceOnce($item->orig, $html, $string);
    }

    private static function replaceSyntaxItemList(&$string, $items)
    {
        $first = key($items);
        end($items);

        foreach ($items as $i => &$item)
        {
            self::replaceSyntaxItem($string, $item, $items, ($i == $first));
        }
    }

    private static function replaceUrlLinks(&$string)
    {
        RL_RegEx::matchAll(
            '(?<link><a\s[^>]*href="(?<url>[^"]*)(?:\?|&(?:amp;)?)tab=(?<id>[^"\#&]*)(?:\#[^"]*)?"[^>]*>)(?<text>.*?)</a>',
            $string,
            $matches
        );

        if (empty($matches))
        {
            return;
        }

        self::replaceLinksMatches($string, $matches);
    }

    private static function setOpenItem(&$items, $opened_by_default = 0)
    {
        $opened_by_default = (int) $opened_by_default;

        while (isset($items[$opened_by_default]) && $items[$opened_by_default]->haslink)
        {
            $opened_by_default++;
        }

        if ( ! isset($items[$opened_by_default]))
        {
            return;
        }

        $items[$opened_by_default]->open = true;
    }

    private static function setPositionFromOldClasses(&$values)
    {
        if (empty($values->class) || ! empty($values->position))
        {
            return;
        }

        $classes   = explode(' ', $values->class);
        $positions = ['top', 'bottom', 'left', 'right'];
        $found     = array_intersect($classes, $positions);

        if (empty($found))
        {
            return;
        }

        $position = array_shift($found);

        $classes = array_diff($classes, [$position]);

        $values->class    = implode(' ', $classes);
        $values->position = $position;
    }

    private static function setTagAttributes(&$item, $string)
    {
        $values = self::getTagAttributes($string);

        $item = (object) array_merge((array) $item, (array) $values);
    }

    private static function stringHasItem(&$string, $id)
    {
        return (strpos($string, 'data-toggle="tab" data-id="' . $id . '"') !== false);
    }
}