DEV Community

Ruslan Sydorovych
Ruslan Sydorovych

Posted on

Issue with BBCode tags inside the spoiler

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]
Enter fullscreen mode Exit fullscreen mode

Screenshot:

Image description

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;`
Enter fullscreen mode Exit fullscreen mode

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;`
Enter fullscreen mode Exit fullscreen mode

utilsGeneral.ts:

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

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, "<s>$1</s>")
        .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;`
Enter fullscreen mode Exit fullscreen mode

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>`
Enter fullscreen mode Exit fullscreen mode

Any ideas how to fix this issue? Thanks.

Top comments (0)