Issue with BBCode tags inside the spoiler
Published: 2024-04-02 11:48:54

I have BBCode parsing issue inside spoiler tag. So, for example, when I add [hr] or any other `BBCode` tag, it breaks the spoiler:

**BBCode:**

    [spoiler=Title]#ff7f50 hex color red value is 255, green value is 127 and the blue value of its RGB is 80. 
    
    [hr]
    
    Cylindrical-coordinate representations (also known as HSL) of color #ff7f50 hue: 0.04 , saturation: 1.00 and the lightness value of ff7f50 is 0.66.[/spoiler]

**Screenshot:**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1apt9knzjbhc7r76bgxe.gif)

As you can see on the image, the text after `[hr]` tag displays outside the spoiler.

**Code:**

*InfoSpoiler.tsx:*

    `import React, {useState} from "react";
    import CustomTooltip from "./CustomTooltip";
    
    const InfoSpoiler = ({isDefaultSpoilerTitle, spoilerContentVisibility, spoilerTitle, spoilerContent}) => {
        const [isSpoilerVisible, setSpoilerVisible] = useState(spoilerContentVisibility);
    
        const handleSpoilerState = (event) => {
            setSpoilerVisible(!isSpoilerVisible);
        };
    
        return (
            <div className="spoiler">
                <CustomTooltip msg="Click to open/close">
                    <p className={`${isDefaultSpoilerTitle ? "spoiler-title" : "spoiler-title-centered"}`} onClick={handleSpoilerState}><i className={`notification-icon fa-solid ${isSpoilerVisible ? "fa-minus" : "fa-plus"}`}></i>{spoilerTitle}</p>
                </CustomTooltip>
                {isSpoilerVisible && (
                    typeof spoilerContent === "string" ? (
                        <div className="spoiler-content" dangerouslySetInnerHTML={{__html: spoilerContent}} />
                    ) : (
                        <div className="spoiler-content">{spoilerContent}</div>
                    )
                )} 
            </div>
        );
    };
    
    export default InfoSpoiler;`

*CustomTooltip.tsx:*

    `import React from "react";
    import "react-tippy/dist/tippy.css";
    import {Tooltip} from "react-tippy";
    
    export type Position =
      | "top"
      | "top-start"
      | "top-end"
      | "bottom"
      | "bottom-start"
      | "bottom-end"
      | "left"
      | "left-start"
      | "left-end"
      | "right"
      | "right-start"
      | "right-end";
    export type Size = "small" | "regular" | "big";
    export type Theme = "dark" | "light" | "transparent";
    
    interface IToolTip {
        children: React.ReactNode;
        msg: string;
        pos?: Position, 
        tooltipSize?: Size, 
        tooltipTheme?: Theme
    }
    
    const CustomTooltip = ({children, msg, pos = "top", tooltipSize = "big", tooltipTheme = "light"} : IToolTip) => {
        return (
            <Tooltip title={msg} position={pos} size={tooltipSize} theme={tooltipTheme} followCursor={true}>{children}</Tooltip>
        );
    };
    
    export default CustomTooltip;`

*utilsGeneral.ts:*

    `const validateHtml = (data) => {
      return data
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    };`


*BBCodeComponent.tsx:*

    `import React, {useEffect} from "react";
    import ReactDOM from "react-dom";
    import CustomTooltip from "./CustomTooltip";
    import {validateHtml} from "../../../utils/utilsGeneral";
    import InfoSpoiler from "./InfoSpoiler";
    
    const BBCodeComponent = ({content}) => {
         useEffect(() => {
            const handleTooltip = () => {
                const tooltipWrappers = document.querySelectorAll(".tooltip-wrapper");
                tooltipWrappers.forEach((wrapper) => {
                    const title = wrapper.getAttribute("data-title");
                    const src = wrapper.getAttribute("data-src");
    
                    if (title && src) {
                        ReactDOM.render(<CustomTooltip msg={title}><img src={src} alt={title} /></CustomTooltip>, wrapper);
                    }
                });
            };
    
            const handleSpoiler = () => {
                const spoilerWrappers = document.querySelectorAll(".spoiler-wrapper");
                spoilerWrappers.forEach((wrapper) => {
                    const title = wrapper.getAttribute("data-title");
                    const content = wrapper.getAttribute("data-content");
    
                    if (title && content) {
                        ReactDOM.render(<InfoSpoiler isDefaultSpoilerTitle={true} spoilerContentVisibility={false} spoilerTitle={title} spoilerContent={parseBBCode(content)} />, wrapper);
                    }
                });
            };
    
            handleTooltip();
            handleSpoiler();
        }, [content]);
    
        const parseBBCode = (text) => {
            return text
            .replace(/\[b\](.*?)\[\/b\]/g, "<strong>$1</strong>")
            .replace(/\[i\](.*?)\[\/i\]/g, "<em>$1</em>")
            .replace(/\[u\](.*?)\[\/u\]/g, "<u>$1</u>")
            .replace(/\[s\](.*?)\[\/s\]/g, "
$1
")
            .replace(/\[inline\](.*?)\[\/inline\]/g, "<div class=\"inline-tag\">$1</div>")
            .replace(/\[left\](.*?)\[\/left\]/g, "<div class=\"left-tag\">$1</div>")
            .replace(/\[center\](.*?)\[\/center\]/g, "<div class=\"center-tag\">$1</div>")
            .replace(/\[right\](.*?)\[\/right\]/g, "<div class=\"right-tag\">$1</div>")
            .replace(/\[img=(.*?)\](.*?)\[\/img\]/g, (match, title, src) => {
                return `<div class=\"tooltip-wrapper\" data-title=\"${title}\" data-src=\"${src}\"><img src=\"${src}\" alt=\"${title}\"></div>`;
            })
            .replace(/\[quote\](.*?)\[\/quote\]/g, "<blockquote class=\"quote-tag\">$1</blockquote>")
            .replace(/\[url=(.*?)\](.*?)\[\/url\]/g, "<a class=\"url-tag\" href=\"$1\">$2</a>")
            .replace(/\[code\]([\s\S]*?)\[\/code\]/g, (match, content) => {
                return `<pre class=\"code-tag\">${validateHtml(content)}</pre>`;
            })
            .replace(/\[spoiler=(.*?)\]([\s\S]*?)\[\/spoiler\]/g, (match, title, content) => {
                return `<div class=\"spoiler-wrapper\" data-title=\"${title}\" data-content=\"${validateHtml(content.trim())}\"></div>`;
            })
            .replace(/\[hr\]/g, "<div class=\"hr-wrapper\"><hr class=\"horizontal-line-tag\"></div>")
            .replace(/\[li\](.*?)\[\/li\]/g, "<li class=\"list-tag\">$1</li>")
            .replace(/\[color=(\#[0-9A-F]{6}|[a-z]+|[rgb(\d{1,3},\d{1,3},\d{1,3}(\s?))]+)\](.*?)\[\/color\]/g, "<span style=\"color: $1\">$2</span>")
            .replace(/\[youtube\](.*?)\[\/youtube\]/g, (match, url) => {
                const videoID = url.split("v=")[1];
                return `<div class="youtube-container"><iframe width=\"640\" height=\"510\" src=\"https://www.youtube.com/embed/${videoID}\" loading=\"lazy\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`;
            });
        };
        
        return <div dangerouslySetInnerHTML={{__html: parseBBCode(content)}} />;
    };
    
    export default BBCodeComponent;`

These BBCode tags work well when outside the spoiler but fails to render inside the spoiler. 

**HTML output from a browser:**

    `<div><div class="spoiler-wrapper" data-title="Title" data-content="#ff7f50 hex color red value is 255, green value is 127 and the blue value of its RGB is 80. 
    
    <div class=" hr-wrapper"=""><div class="spoiler"><div class="" data-tooltipped="" aria-describedby="tippy-tooltip-59" data-original-title="Click to open/close" style="display: inline;"><p class="spoiler-title"><i class="notification-icon fa-solid fa-minus"></i>Title</p></div><div class="spoiler-content">#ff7f50 hex color red value is 255, green value is 127 and the blue value of its RGB is 80. 
    
    </div></div></div>
    
    Cylindrical-coordinate representations (also known as HSL) of color #ff7f50 hue: 0.04 , saturation: 1.00 and the lightness value of ff7f50 is 0.66."&gt;</div>`

Any ideas how to fix this issue? Thanks.