Skip to main content
Developer Tools · Text Converter

HTML to BBCode Converter

Convert HTML to BBCode online — free, instant, and 100% in your browser. Paste HTML markup or upload an .html file and get forum-ready BBCode for phpBB, vBulletin, XenForo, SMF, and MyBB, with a live rendered preview. No signup, no uploads, no server.

How the HTML to BBCode conversion works

Paste your HTML on the left — the converter parses it with a real tokenizer (not fragile regex find-and-replace), so nested markup like lists inside blockquotes converts with correct structure. Every run happens locally in your browser: nothing is uploaded, which also makes it safe to convert private drafts or client content.

For example, this HTML:

<h1>Big Sale</h1><p>Visit <a href="https://example.com">our store</a> for <b>50% off</b> today.</p>

becomes this forum-ready BBCode:

[size=7]Big Sale[/size]

Visit [url=https://example.com]our store[/url] for [b]50% off[/b] today.

Sanitized by design

<script>, <style>, <iframe>, <object>, <embed>, and form elements are removed along with HTML comments — they never reach the output. Link and image URLs are allow-listed to http(s):// (plus relative URLs): javascript:, data:, and vbscript: payloads are neutralized while keeping the link text. Dangerous attributes (onclick and friends) are ignored entirely, and colors accept only CSS named colors and hex codes.

Built for forum migration and cross-posting

Author once in rich HTML — in Word, Google Docs, or any WYSIWYG editor — then cross-post to phpBB, vBulletin, XenForo, SMF, or MyBB without re-typing tags. The Rendered preview tab shows how your BBCode will look on the forum before you paste it. Need the reverse direction? Our BBCode to HTML converter is the one-way companion tool.

HTML to BBCode tag mapping

Every supported HTML tag and the exact BBCode it produces. Tags without a BBCode equivalent are dropped while their text content is always kept.

HTMLBBCode outputNotes
<b>, <strong>[b]…[/b]Bold
<i>, <em>[i]…[/i]Italic
<u>, <ins>[u]…[/u]Underline
<s>, <strike>, <del>[s]…[/s]Strikethrough
<sub>, <sup>[sub], [sup]Sub/superscript
<a href="…">[url]…[/url] / [url=…]…[/url]javascript:/data: stripped; text kept
<img src="…">[img]…[/img]Unsafe URLs dropped; toggleable
<blockquote> (+<cite>)[quote] / [quote="author"]Quotes with optional attribution
<pre>, <code>[code]…[/code]Whitespace preserved verbatim
<ul>, <ol>, <li>[list], [list=1], [*]Nested lists supported
<h1>–<h6>[size=7]–[size=2]Classic 1–7 scale; toggleable
<p>blank lineParagraph separation
<br>newlineLine break
<hr>[hr]Horizontal rule
<center>[center]…[/center]Centered text
<font color>, style="color:…"[color=…]…[/color]Named colors + #hex validated
<font size="1"–"7">[size=…]…[/size]Legacy font sizes
<table>cell | cell rowsTables flattened to text rows
<q>"…"Inline quotation marks
<script>, <style>, <iframe>, commentsremovedSanitized — never in output

Frequently asked questions

What is BBCode and why would I convert HTML to BBCode?

BBCode (Bulletin Board Code) is a lightweight markup language used by forum software such as phpBB, vBulletin, XenForo, SMF, and MyBB. Forums strip raw HTML from posts for security, so members format with square-bracket tags like [b], [quote], and [url] instead. This converter lets you author content as rich HTML — pasted from Word, Google Docs, or a WYSIWYG editor — and get forum-ready BBCode back, instead of re-typing every tag by hand.

Which HTML tags does this converter turn into BBCode?

Bold and italic (<b>, <strong>, <i>, <em>), underline and strikethrough (<u>, <s>, <strike>, <del>), subscript and superscript, links (<a href> becomes [url] or [url=…]), images (<img> becomes [img]), quotes (<blockquote> becomes [quote], with [quote="author"] when a <cite> is present), code (<pre> and <code> become [code] with whitespace preserved), lists (<ul>, <ol>, <li> become [list], [list=1], [*] including nested lists), headings (<h1>–<h6>), paragraphs, line breaks, horizontal rules, centered text, colors, and tables. See the mapping table above for the full list.

How are HTML headings converted to BBCode?

Headings map to the classic BBCode 1–7 size scale: <h1> becomes [size=7], <h2> becomes [size=6], down to <h6> becoming [size=2]. This matches how most forums render heading-like text. If you prefer plain headings without size tags, uncheck the ‘Headings to [size]’ option and headings convert to plain text separated by blank lines.

Is my HTML uploaded anywhere? Is this private?

No. The conversion runs entirely in your browser with JavaScript — your HTML is never sent to a server, logged, or stored. The file-upload button reads your .html file locally with the FileReader API. That makes it safe to convert private drafts, client content, or unreleased announcements.

Is the BBCode output safe to post on forums?

Yes. Sanitizing is built in: <script>, <style>, <iframe>, <object>, <embed>, and form elements are removed along with HTML comments, and dangerous attributes are ignored. Link and image URLs with unsafe schemes (javascript:, data:, vbscript:) are neutralized — the link text is kept but no executable URL survives. Colors accept only CSS named colors and hex codes.

What happens to HTML tags that have no BBCode equivalent?

The tag is dropped but its text content is always kept — nothing you wrote is silently lost. Structural elements get sensible treatment: <div> and <section> become paragraph breaks, <table> rows become ‘cell | cell’ lines, <font size=‘1’–‘7’> maps to [size], and definition lists become bold terms with plain definitions.

Which forum software does the BBCode output work with?

Any forum using standard BBCode: phpBB, vBulletin, XenForo, SMF (Simple Machines Forum), MyBB, and most bulletin boards derived from them. The tags produced — [b], [i], [url], [img], [quote], [code], [list] with [*] items, [size], [color], [center], [hr] — are the common core supported virtually everywhere. Use the Rendered preview tab to check how your post will look.

Can I convert a whole HTML file to BBCode?

Yes. Click ‘Upload .html’ and pick a .html, .htm, or .txt file — it is read locally in your browser and converted immediately, with no server-side size limits. Then copy the BBCode or download it as html-to-bbcode.txt. Tip: pasting a full saved web page works too; navigation, scripts, and styles are stripped automatically.

How do I convert BBCode back to HTML?

This tool is intentionally one-way: HTML to BBCode only, with no reverse mode. To go the other direction — BBCode to HTML — use our companion BBCode to HTML converter, which parses forum markup into clean, sanitized HTML with the same 100% client-side approach.

Related tools

<script> /* * HTML -> BBCode converter engine (canonical implementation). * * Used by template-standalone-html-to-bbcode.php (inlined at build time) * and by test.js / oracle.py (differential testing). 100% client-side, * no dependencies, no DOMParser (pure-JS tokenizer so Node and browsers * behave identically). * * ===================================================================== * SPEC v1 (implemented identically in engine.js and oracle.py) * --------------------------------------------------------------------- * 1. Tokenize: text | open | close | comment | doctype/decl | pi | * rawtext (script, style, textarea, title: raw until matching close). * 2. Tree: lenient. Auto-close p/li/dt/dd/tr/td/th on same-tag open. * Close tag pops to nearest matching ancestor; unmatched close ignored. * `
` is treated as `
`. * 3. Drop whole subtree: script, style, iframe, object, embed, applet, * noembed, noframes, noscript, form, head, title, meta, link, base, * template, textarea. Comments/doctype/pi dropped. * 4. Void elements: area, base, br, col, embed, hr, img, input, link, * meta, param, source, track, wbr (plus explicit `/>` self-close). * 5. URL safety: a[href] / img[src] with a scheme other than http/https * (javascript:, data:, vbscript:, ...) are unsafe. Unsafe link -> * plain link text. Unsafe image -> alt text (or dropped). * 6. Color: #rgb/#rrggbb/#rrggbbaa or CSS named color (validated); * from or style="color:...". Invalid -> tag dropped. * 7. Tag mapping: * b,strong->[b] i,em->[i] u,ins->[u] s,strike,del->[s] * sub->[sub] sup->[sup] * a[href]->[url=href]text[/url] ([url]href[/url] when text==href) * img->[img]src[/img] * blockquote->[quote] (cite child -> [quote="author"]) * pre->[code] (verbatim) code->[code] * ul->[list] ol->[list=1] li->[*] * h1..h6->[size=7..2] (toggleable via options.headings) * p->paragraph (blank line after) br->newline hr->[hr] * center->[center] font[color]->[color] font[size=1-7]->[size] * span[style*=color]->[color] * table->rows of "cell | cell"; dt->[b]term[/b]; q->"quoted" * unknown tags: tag dropped, children kept. * 8. Whitespace: CR/CRLF -> LF. Inside
: verbatim. Elsewhere
 *    horizontal runs collapse to one space; whitespace-only text nodes
 *    become ' ' between inline elements and are dropped at block level.
 *    Block wrappers trim; final pass collapses 3+ newlines to 2.
 * 9. Options: {headings:true, images:true, links:true, plain:false}.
 *    plain:true strips all formatting -> plain text with paragraphs.
 * =====================================================================
 */
(function (root, factory) {
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = factory();
    } else {
        root.HTMLToBBCode = factory();
    }
}(typeof self !== 'undefined' ? self : this, function () {
    'use strict';

    /* ---------------- HTML entities ---------------- */
    var ENTITY_MAP = {
        amp: '&', lt: '<', gt: '>', quot: '"', apos: "'",
        nbsp: ' ', iexcl: '¡', cent: '¢', pound: '£', curren: '¤',
        yen: '¥', brvbar: '¦', sect: '§', uml: '¨', copy: '©',
        ordf: 'ª', laquo: '«', not: '¬', shy: '­', reg: '®',
        macr: '¯', deg: '°', plusmn: '±', sup2: '²', sup3: '³',
        acute: '´', micro: 'µ', para: '¶', middot: '·', cedil: '¸',
        sup1: '¹', ordm: 'º', raquo: '»', frac14: '¼', frac12: '½',
        frac34: '¾', iquest: '¿', Agrave: 'À', Aacute: 'Á', Acirc: 'Â',
        Atilde: 'Ã', Auml: 'Ä', Aring: 'Å', AElig: 'Æ', Ccedil: 'Ç',
        Egrave: 'È', Eacute: 'É', Ecirc: 'Ê', Euml: 'Ë', Igrave: 'Ì',
        Iacute: 'Í', Icirc: 'Î', Iuml: 'Ï', ETH: 'Ð', Ntilde: 'Ñ',
        Ograve: 'Ò', Oacute: 'Ó', Ocirc: 'Ô', Otilde: 'Õ', Ouml: 'Ö',
        times: '×', Oslash: 'Ø', Ugrave: 'Ù', Uacute: 'Ú', Ucirc: 'Û',
        Uuml: 'Ü', Yacute: 'Ý', THORN: 'Þ', szlig: 'ß', agrave: 'à',
        aacute: 'á', acirc: 'â', atilde: 'ã', auml: 'ä', aring: 'å',
        aelig: 'æ', ccedil: 'ç', egrave: 'è', eacute: 'é', ecirc: 'ê',
        euml: 'ë', igrave: 'ì', iacute: 'í', icirc: 'î', iuml: 'ï',
        eth: 'ð', ntilde: 'ñ', ograve: 'ò', oacute: 'ó', ocirc: 'ô',
        otilde: 'õ', ouml: 'ö', divide: '÷', oslash: 'ø', ugrave: 'ù',
        uacute: 'ú', ucirc: 'û', uuml: 'ü', yacute: 'ý', thorn: 'þ',
        yuml: 'ÿ', OElig: 'Œ', oelig: 'œ', Scaron: 'Š', scaron: 'š',
        Yuml: 'Ÿ', fnof: 'ƒ', circ: 'ˆ', tilde: '˜', Alpha: 'Α',
        Beta: 'Β', Gamma: 'Γ', Delta: 'Δ', Epsilon: 'Ε', Zeta: 'Ζ',
        Eta: 'Η', Theta: 'Θ', Iota: 'Ι', Kappa: 'Κ', Lambda: 'Λ',
        Mu: 'Μ', Nu: 'Ν', Xi: 'Ξ', Omicron: 'Ο', Pi: 'Π', Rho: 'Ρ',
        Sigma: 'Σ', Tau: 'Τ', Upsilon: 'Υ', Phi: 'Φ', Chi: 'Χ',
        Psi: 'Ψ', Omega: 'Ω', alpha: 'α', beta: 'β', gamma: 'γ',
        delta: 'δ', epsilon: 'ε', zeta: 'ζ', eta: 'η', theta: 'θ',
        iota: 'ι', kappa: 'κ', lambda: 'λ', mu: 'μ', nu: 'ν',
        xi: 'ξ', omicron: 'ο', pi: 'π', rho: 'ρ', sigmaf: 'ς',
        sigma: 'σ', tau: 'τ', upsilon: 'υ', phi: 'φ', chi: 'χ',
        psi: 'ψ', omega: 'ω', hellip: '…', mdash: '—', ndash: '–',
        lsquo: '‘', rsquo: '’', ldquo: '“', rdquo: '”', dagger: '†',
        Dagger: '‡', bull: '•', permil: '‰', prime: '′', Prime: '″',
        lsaquo: '‹', rsaquo: '›', oline: '‾', euro: '€', trade: '™',
        larr: '←', uarr: '↑', rarr: '→', darr: '↓', harr: '↔'
    };

    function decodeEntities(s) {
        return String(s).replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]*);/g, function (m, ent) {
            if (ent.charAt(0) === '#') {
                var cp = ent.charAt(1).toLowerCase() === 'x'
                    ? parseInt(ent.slice(2), 16)
                    : parseInt(ent.slice(1), 10);
                if (isNaN(cp) || cp < 0 || cp > 0x10FFFF) return m;
                try { return String.fromCodePoint(cp); } catch (e) { return m; }
            }
            return ENTITY_MAP.hasOwnProperty(ent) ? ENTITY_MAP[ent] : m;
        });
    }

    /* ---------------- tokenizer ---------------- */
    var VOID_TAGS = {
        area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1,
        input: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1
    };
    var RAW_TEXT_TAGS = { script: 1, style: 1, textarea: 1, title: 1 };
    var DROP_TAGS = {
        script: 1, style: 1, iframe: 1, object: 1, embed: 1, applet: 1,
        noembed: 1, noframes: 1, noscript: 1, form: 1, head: 1, title: 1,
        meta: 1, link: 1, base: 1, template: 1, textarea: 1
    };

    // Parse one tag starting at html[i] === '<'. Returns {token, next}.
    function readTag(html, i) {
        var n = html.length;
        // comment
        if (html.substr(i, 4) === '', i + 4);
            return { token: { type: 'comment' }, next: end === -1 ? n : end + 3 };
        }
        // doctype / declaration 
        if (html.charAt(i + 1) === '!') {
            var gt = html.indexOf('>', i + 2);
            return { token: { type: 'decl' }, next: gt === -1 ? n : gt + 1 };
        }
        // processing instruction <\u003f...?>
        if (html.charAt(i + 1) === '?') {
            var pe = html.indexOf('?>', i + 2);
            return { token: { type: 'pi' }, next: pe === -1 ? n : pe + 2 };
        }
        var close = html.charAt(i + 1) === '/';
        var j = i + (close ? 2 : 1);
        var m = /^[a-zA-Z][a-zA-Z0-9-]*/.exec(html.slice(j));
        if (!m) return { token: { type: 'text', value: '<' }, next: i + 1 };
        var name = m[0].toLowerCase();
        j += m[0].length;
        if (close) {
            var ge = html.indexOf('>', j);
            return { token: { type: 'close', name: name }, next: ge === -1 ? n : ge + 1 };
        }
        // attributes
        var attrs = {};
        var selfClose = false;
        while (j < n) {
            var c = html.charAt(j);
            if (c === '>') { j++; break; }
            if (c === '/' && html.charAt(j + 1) === '>') { selfClose = true; j += 2; break; }
            if (/\s/.test(c)) { j++; continue; }
            var am = /^[^\s=/>]+/.exec(html.slice(j));
            if (!am) { j++; continue; }
            var aname = am[0].toLowerCase();
            j += am[0].length;
            var aval = '';
            while (j < n && /\s/.test(html.charAt(j))) j++;
            if (html.charAt(j) === '=') {
                j++;
                while (j < n && /\s/.test(html.charAt(j))) j++;
                var q = html.charAt(j);
                if (q === '"' || q === "'") {
                    var qe = html.indexOf(q, j + 1);
                    if (qe === -1) { aval = html.slice(j + 1); j = n; }
                    else { aval = html.slice(j + 1, qe); j = qe + 1; }
                } else {
                    var vm = /^[^\s>]*/.exec(html.slice(j));
                    aval = vm[0]; j += vm[0].length;
                }
                aval = decodeEntities(aval);
            }
            if (!attrs.hasOwnProperty(aname)) attrs[aname] = aval;
        }
        if (VOID_TAGS[name]) selfClose = true;
        var tok = { type: 'open', name: name, attrs: attrs, selfClose: selfClose };
        // raw text elements: consume verbatim until matching close tag
        if (RAW_TEXT_TAGS[name] && !selfClose) {
            var re = new RegExp('', 'i');
            var rest = html.slice(j);
            var rm = re.exec(rest);
            var raw, next;
            if (rm) { raw = rest.slice(0, rm.index); next = j + rm.index + rm[0].length; }
            else { raw = rest; next = n; }
            return {
                token: { type: 'rawtext', name: name, attrs: attrs, value: raw },
                next: next
            };
        }
        return { token: tok, next: j };
    }

    function tokenize(html) {
        var toks = [];
        var i = 0, n = html.length, textStart = 0;
        while (i < n) {
            if (html.charAt(i) === '<') {
                if (i > textStart) toks.push({ type: 'text', value: decodeEntities(html.slice(textStart, i)) });
                var r = readTag(html, i);
                if (r.token.type !== 'comment' && r.token.type !== 'decl' && r.token.type !== 'pi') {
                    toks.push(r.token);
                }
                i = r.next;
                textStart = i;
            } else {
                i++;
            }
        }
        if (textStart < n) toks.push({ type: 'text', value: decodeEntities(html.slice(textStart)) });
        return toks;
    }

    /* ---------------- tree builder ---------------- */
    var AUTO_CLOSE = {
        p: ['p'], li: ['li'], dt: ['dt', 'dd'], dd: ['dt', 'dd'],
        tr: ['tr'], td: ['td', 'th'], th: ['td', 'th']
    };

    function buildTree(toks) {
        var root = { type: 'el', name: 'root', attrs: {}, children: [] };
        var stack = [root];
        for (var k = 0; k < toks.length; k++) {
            var t = toks[k];
            if (t.type === 'text') {
                stack[stack.length - 1].children.push({ type: 'text', value: t.value });
            } else if (t.type === 'rawtext') {
                var rnode = { type: 'el', name: t.name, attrs: t.attrs, children: [{ type: 'text', value: t.value, raw: true }] };
                stack[stack.length - 1].children.push(rnode);
            } else if (t.type === 'open') {
                var ac = AUTO_CLOSE[t.name];
                if (ac) {
                    while (stack.length > 1 && ac.indexOf(stack[stack.length - 1].name) !== -1) stack.pop();
                }
                var el = { type: 'el', name: t.name, attrs: t.attrs, children: [] };
                stack[stack.length - 1].children.push(el);
                if (!t.selfClose) stack.push(el);
            } else if (t.type === 'close') {
                if (t.name === 'br') {
                    stack[stack.length - 1].children.push({ type: 'el', name: 'br', attrs: {}, children: [] });
                    continue;
                }
                var idx = -1;
                for (var s = stack.length - 1; s >= 1; s--) {
                    if (stack[s].name === t.name) { idx = s; break; }
                }
                if (idx !== -1) stack.length = idx;
            }
        }
        // Merge adjacent text nodes (dropped tags may have split them).
        (function merge(node) {
            var merged = [];
            for (var i = 0; i < node.children.length; i++) {
                var c = node.children[i];
                if (c.type === 'text' && merged.length && merged[merged.length - 1].type === 'text') {
                    merged[merged.length - 1].value += c.value;
                } else {
                    merged.push(c);
                }
            }
            node.children = merged;
            for (var j = 0; j < merged.length; j++) {
                if (merged[j].type !== 'text') merge(merged[j]);
            }
        })(root);
        return root;
    }

    /* Drop dangerous/structural subtrees. rawtext tokens (script/style/...)
       are single tokens already consumed by the tokenizer. */
    function dropTags(toks) {
        var out = [];
        for (var i = 0; i < toks.length; i++) {
            var t = toks[i];
            var nm = t.name;
            if ((t.type === 'open' || t.type === 'rawtext') && nm && DROP_TAGS[nm]) {
                if (t.type === 'rawtext') continue;
                if (t.selfClose) continue;
                var depth = 1, j = i + 1;
                while (j < toks.length && depth > 0) {
                    var u = toks[j];
                    if (u.type === 'open' && u.name === nm && !u.selfClose) depth++;
                    else if (u.type === 'close' && u.name === nm) depth--;
                    j++;
                }
                i = j - 1;
                continue;
            }
            out.push(t);
        }
        return out;
    }

    /* ---------------- validation helpers ---------------- */
    var NAMED_COLORS = (
        'aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue ' +
        'blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk ' +
        'crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki ' +
        'darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen ' +
        'darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue ' +
        'dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite ' +
        'gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki ' +
        'lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan ' +
        'lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen ' +
        'lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen ' +
        'magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen ' +
        'mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream ' +
        'mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid ' +
        'palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum ' +
        'powderblue purple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell ' +
        'sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal ' +
        'thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen'
    ).split(' ');

    function validColor(c) {
        c = String(c).trim();
        if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(c)) return c;
        if (/^[a-zA-Z]+$/.test(c) && NAMED_COLORS.indexOf(c.toLowerCase()) !== -1) return c.toLowerCase();
        return null;
    }

    function safeUrl(u) {
        if (!u) return false;
        u = String(u).trim();
        if (!u) return false;
        var m = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.exec(u);
        if (m) {
            var scheme = m[0].slice(0, -1).toLowerCase();
            return scheme === 'http' || scheme === 'https';
        }
        return true; // relative URL
    }

    function getAttr(node, name) {
        var v = node.attrs[name];
        return v === undefined ? null : v;
    }

    function extractStyleColor(style) {
        var parts = String(style).split(';');
        for (var i = 0; i < parts.length; i++) {
            var kv = parts[i].split(':');
            if (kv.length >= 2 && kv[0].trim().toLowerCase() === 'color') {
                var c = validColor(kv.slice(1).join(':'));
                if (c) return c;
            }
        }
        return null;
    }

    /* textContent of a subtree (entities already decoded at tokenize time) */
    function textContent(node, verbatim) {
        if (node.type === 'text') {
            var t = node.value.replace(/\r\n?/g, '\n');
            return verbatim ? t : t;
        }
        var out = '';
        for (var i = 0; i < node.children.length; i++) out += textContent(node.children[i], verbatim);
        return out;
    }

    var BLOCK_TAGS = {
        p: 1, div: 1, section: 1, article: 1, header: 1, footer: 1, main: 1,
        aside: 1, nav: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1,
        ul: 1, ol: 1, blockquote: 1, pre: 1, hr: 1, table: 1, figure: 1,
        figcaption: 1, center: 1, dl: 1, dt: 1, dd: 1, details: 1,
        summary: 1, fieldset: 1, address: 1
    };

    function isWsOnly(s) { return /^\s*$/.test(s); }

    /* Render a text node. ctx: {inPre} */
    function renderText(node, ctx) {
        var t = node.value.replace(/\r\n?/g, '\n');
        if (ctx.inPre || node.raw) return t;
        if (isWsOnly(t)) {
            return t.indexOf('\n') === -1 ? ' ' : '\u0000WSNL\u0000';
        }
        return t.replace(/[ \t]+/g, ' ');
    }

    var WSNL = '\u0000WSNL\u0000';

    /* Inline render of children: WSNL -> ' ' */
    function renderInlineChildren(node, ctx) {
        ctx = ctx || {};
        var out = '';
        for (var i = 0; i < node.children.length; i++) {
            var c = node.children[i];
            out += c.type === 'text' ? renderText(c, ctx) : renderEl(c, ctx);
        }
        return out.split(WSNL).join(' ');
    }

    /* Block render of children: WSNL / whitespace-only text dropped,
       block children flushed with blank-line separation. */
    function renderBlockChildren(node, ctx) {
        ctx = ctx || {};
        var out = '', buf = '';
        function flush() {
            var t = buf.replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '');
            if (t) out += t + '\n\n';
            buf = '';
        }
        for (var i = 0; i < node.children.length; i++) {
            var c = node.children[i];
            if (c.type === 'text') {
                var tnorm = c.value.replace(/\r\n?/g, '\n');
                if (isWsOnly(tnorm)) {
                    if (tnorm.indexOf('\n') === -1) buf += ' ';
                    continue;
                }
                buf += renderText(c, ctx).split(WSNL).join(' ');
            } else if (BLOCK_TAGS[c.name]) {
                flush();
                out += renderEl(c, ctx);
            } else {
                buf += renderEl(c, ctx);
            }
        }
        flush();
        return out;
    }

    function renderLi(node, ctx) {
        var parts = [];
        for (var i = 0; i < node.children.length; i++) {
            var c = node.children[i];
            if (c.type === 'text') {
                if (isWsOnly(c.value)) continue;
                parts.push(renderText(c, ctx).split(WSNL).join(' '));
            } else if (c.name === 'ul' || c.name === 'ol') {
                parts.push('\n' + renderEl(c, ctx).replace(/\n+$/, ''));
            } else {
                parts.push(renderEl(c, ctx));
            }
        }
        return '[*]' + parts.join('').replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '') + '\n';
    }

    function renderList(node, ctx) {
        var items = '';
        for (var i = 0; i < node.children.length; i++) {
            var c = node.children[i];
            if (c.type === 'text') {
                if (!isWsOnly(c.value)) items += renderText(c, ctx).split(WSNL).join(' ');
                continue;
            }
            if (c.name === 'li') items += renderLi(c, ctx);
            else if (BLOCK_TAGS[c.name]) items += renderEl(c, ctx);
            else items += renderEl(c, ctx);
        }
        var open = node.name === 'ol' ? '[list=1]' : '[list]';
        return open + '\n' + items + '[/list]\n\n';
    }

    function rowCells(tr, ctx) {
        var cells = [];
        for (var i = 0; i < tr.children.length; i++) {
            var c = tr.children[i];
            if (c.type === 'text') {
                if (!isWsOnly(c.value)) cells.push(renderText(c, ctx).split(WSNL).join(' ').trim());
                continue;
            }
            if (c.name === 'td' || c.name === 'th') cells.push(renderInlineChildren(c, ctx).trim());
        }
        return cells;
    }

    function renderTable(node, ctx) {
        var rows = [];
        function grab(n) {
            for (var i = 0; i < n.children.length; i++) {
                var c = n.children[i];
                if (c.type !== 'text' && (c.name === 'thead' || c.name === 'tbody' || c.name === 'tfoot')) grab(c);
                else if (c.type !== 'text' && c.name === 'tr') rows.push(rowCells(c, ctx).join(' | '));
            }
        }
        grab(node);
        var body = rows.filter(function (r) { return r.trim() !== ''; }).join('\n');
        return body ? body + '\n\n' : '';
    }

    function renderEl(node, ctx) {
        ctx = ctx || {};
        var name = node.name;
        var inline = function () { return renderInlineChildren(node, ctx); };
        switch (name) {
            case 'b': case 'strong': return '[b]' + inline() + '[/b]';
            case 'i': case 'em': return '[i]' + inline() + '[/i]';
            case 'u': case 'ins': return '[u]' + inline() + '[/u]';
            case 's': case 'strike': case 'del': return '[s]' + inline() + '[/s]';
            case 'sub': return '[sub]' + inline() + '[/sub]';
            case 'sup': return '[sup]' + inline() + '[/sup]';
            case 'br': return '\n';
            case 'hr': return '[hr]\n\n';
            case 'a': {
                var href = getAttr(node, 'href');
                var text = inline().trim();
                if (ctx.plain || ctx.links === false) return text || (href && safeUrl(href) ? href : '');
                if (!href || !safeUrl(href)) return text;
                href = href.trim();
                if (!text) text = href;
                return text === href ? '[url]' + href + '[/url]' : '[url=' + href + ']' + text + '[/url]';
            }
            case 'img': {
                var src = getAttr(node, 'src');
                var alt = getAttr(node, 'alt') || '';
                if (ctx.images === false || !src || !safeUrl(src)) return alt;
                return '[img]' + src.trim() + '[/img]';
            }
            case 'blockquote': {
                var author = null;
                var kids = [];
                for (var i = 0; i < node.children.length; i++) {
                    var c = node.children[i];
                    if (!author && c.type !== 'text' && c.name === 'cite') {
                        author = textContent(c, false).trim();
                    } else kids.push(c);
                }
                var inner = renderBlockChildren({ children: kids }, ctx).replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '');
                var open = author ? '[quote="' + author + '"]' : '[quote]';
                return inner ? open + '\n' + inner + '\n[/quote]\n\n' : '';
            }
            case 'pre': {
                var raw = textContent(node, true).replace(/^\r?\n/, '').replace(/\r?\n$/, '');
                return raw ? '[code]' + raw + '[/code]\n\n' : '';
            }
            case 'code': {
                if (ctx.inPre) return textContent(node, true);
                return '[code]' + textContent(node, false) + '[/code]';
            }
            case 'ul': case 'ol': return renderList(node, ctx);
            case 'li': return renderLi(node, ctx);
            case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': {
                var t = inline().trim();
                if (!t) return '';
                if (ctx.headings === false) return t + '\n\n';
                var lvl = parseInt(name.charAt(1), 10);
                return '[size=' + (8 - lvl) + ']' + t + '[/size]\n\n';
            }
            case 'p': {
                var pt = inline().trim();
                return pt ? pt + '\n\n' : '';
            }
            case 'center': {
                var innerC = renderBlockChildren(node, ctx);
                var tc = innerC.replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '');
                if (!tc) return '';
                if (tc.indexOf('\n') !== -1) return '[center]\n' + tc + '\n[/center]\n\n';
                return '[center]' + tc + '[/center]';
            }
            case 'font': {
                var col = getAttr(node, 'color');
                var out = inline();
                var sz = getAttr(node, 'size');
                if (sz && /^[1-7]$/.test(sz.trim())) out = '[size=' + sz.trim() + ']' + out + '[/size]';
                var vc = col ? validColor(col) : null;
                if (vc) out = '[color=' + vc + ']' + out + '[/color]';
                return out;
            }
            case 'span': {
                var sc = extractStyleColor(getAttr(node, 'style') || '');
                return sc ? '[color=' + sc + ']' + inline() + '[/color]' : inline();
            }
            case 'table': return renderTable(node, ctx);
            case 'tr': {
                var cells = rowCells(node, ctx).join(' | ');
                return cells ? cells + '\n\n' : '';
            }
            case 'dt': {
                var dt = inline().trim();
                return dt ? '[b]' + dt + '[/b]\n' : '';
            }
            case 'dd': {
                var dd = inline().trim();
                return dd ? dd + '\n' : '';
            }
            case 'q': return '"' + inline() + '"';
            case 'div': case 'section': case 'article': case 'header':
            case 'footer': case 'main': case 'aside': case 'nav':
            case 'figure': case 'figcaption': case 'dl': case 'details':
            case 'fieldset': case 'address':
                return renderBlockChildren(node, ctx);
            case 'summary': {
                var st = inline().trim();
                return st ? '[b]' + st + '[/b]\n' : '';
            }
            default:
                return inline();
        }
    }

    /* Plain-text mode: strip all formatting, keep paragraph structure. */
    function plainText(node) {
        if (node.type === 'text') return node.value.replace(/\r\n?/g, '\n');
        var name = node.name;
        if (name === 'br') return '\n';
        if (name === 'hr') return '\n\n';
        if (name === 'img') return getAttr(node, 'alt') || '';
        if (name === 'a') {
            var t = '';
            for (var i = 0; i < node.children.length; i++) t += plainText(node.children[i]);
            t = t.trim();
            if (t) return t;
            var h = getAttr(node, 'href');
            return h && safeUrl(h) ? h.trim() : '';
        }
        var out = '';
        for (var j = 0; j < node.children.length; j++) out += plainText(node.children[j]);
        if (name === 'li') {
            var lt = out.replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '');
            return lt ? '- ' + lt + '\n' : '';
        }
        if (name === 'tr') {
            var cells = rowCells(node, {});
            return cells.length ? cells.join(' | ') + '\n' : '';
        }
        if (name === 'table') {
            var rows = [];
            (function grab(n) {
                for (var k = 0; k < n.children.length; k++) {
                    var c = n.children[k];
                    if (c.type !== 'text' && (c.name === 'thead' || c.name === 'tbody' || c.name === 'tfoot')) grab(c);
                    else if (c.type !== 'text' && c.name === 'tr') rows.push(rowCells(c, {}).join(' | '));
                }
            })(node);
            return rows.join('\n') + '\n\n';
        }
        if (BLOCK_TAGS[name] || name === 'ul' || name === 'ol') {
            var bt = out.replace(/^[ \t\n]+/, '').replace(/[ \t\n]+$/, '');
            return bt ? bt + '\n\n' : '';
        }
        return out;
    }

    function normalizeFinal(s) {
        return s
            .replace(/[ \t]+\n/g, '\n')
            .replace(/\n{3,}/g, '\n\n')
            .replace(/^[ \t\n]+/, '')
            .replace(/[ \t\n]+$/, '');
    }

    function convert(html, options) {
        options = options || {};
        var ctx = {
            headings: options.headings !== false,
            images: options.images !== false,
            links: options.links !== false,
            plain: options.plain === true
        };
        var inChars = String(html).length;
        var toks = dropTags(tokenize(String(html)));
        var tree = buildTree(toks);
        var stats = { tags: 0 };
        var bbcode;
        if (ctx.plain) {
            bbcode = normalizeFinal(plainText(tree));
        } else {
            var out = renderBlockChildren(tree, ctx);
            // count mapped elements for stats
            (function walk(n) {
                if (n.type !== 'text') {
                    if (/^(b|strong|i|em|u|ins|s|strike|del|sub|sup|a|img|blockquote|pre|code|ul|ol|li|h1|h2|h3|h4|h5|h6|p|br|hr|center|font|table|tr)$/.test(n.name)) stats.tags++;
                    if (n.name === 'span' && extractStyleColor(getAttr(n, 'style') || '')) stats.tags++;
                    for (var k = 0; k < n.children.length; k++) walk(n.children[k]);
                }
            })(tree);
            bbcode = normalizeFinal(out);
        }
        return { bbcode: bbcode, stats: { tags: stats.tags, inChars: inChars, outChars: bbcode.length } };
    }

    /* ---------------- BBCode -> HTML preview renderer (safe, preview only) ---------------- */
    function escapeHtml(s) {
        return String(s)
            .replace(/&/g, '&')
            .replace(//g, '>')
            .replace(/"/g, '"');
    }

    var PV_SIZE = { 1: '0.7em', 2: '0.85em', 3: '1em', 4: '1.25em', 5: '1.5em', 6: '2em', 7: '2.6em' };

    function preview(bbcode) {
        var PH = '';
        var blocks = [];
        var src = String(bbcode).replace(/\[code\]([\s\S]*?)\[\/code\]/gi, function (m, body) {
            blocks.push('
' + escapeHtml(body) + '
'); return PH + (blocks.length - 1) + PH; }); var h = escapeHtml(src); h = h.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '$1') .replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '$1') .replace(/\[u\]([\s\S]*?)\[\/u\]/gi, '$1') .replace(/\[s\]([\s\S]*?)\[\/s\]/gi, '$1') .replace(/\[sub\]([\s\S]*?)\[\/sub\]/gi, '$1') .replace(/\[sup\]([\s\S]*?)\[\/sup\]/gi, '$1') .replace(/\[center\]([\s\S]*?)\[\/center\]/gi, '
$1
') .replace(/\[quote="([^"\]]*)"\]([\s\S]*?)\[\/quote\]/gi, '
$1$2
') .replace(/\[quote\]([\s\S]*?)\[\/quote\]/gi, '
$1
') .replace(/\[size=([1-7])\]([\s\S]*?)\[\/size\]/gi, function (m, n, body) { return '' + body + ''; }) .replace(/\[color=([a-zA-Z]+|#[0-9a-fA-F]{3,8})\]([\s\S]*?)\[\/color\]/gi, '$2') .replace(/\[url=([^\]]+)\]([\s\S]*?)\[\/url\]/gi, function (m, url, body) { var u = url.replace(/"/g, '"').replace(/&/g, '&'); if (!/^https?:\/\//i.test(u)) return body; return '' + body + ''; }) .replace(/\[url\]([\s\S]*?)\[\/url\]/gi, function (m, url) { var u = url.replace(/&/g, '&'); if (!/^https?:\/\//i.test(u)) return url; return '' + escapeHtml(u) + ''; }) .replace(/\[img\]([\s\S]*?)\[\/img\]/gi, function (m, src2) { var u = src2.replace(/&/g, '&'); if (!/^https?:\/\//i.test(u)) return ''; return ''; }) .replace(/\[list=1\]([\s\S]*?)\[\/list\]/gi, '
    $1
') .replace(/\[list\]([\s\S]*?)\[\/list\]/gi, '
    $1
') .replace(/\[\*\]/g, '
  • ') .replace(/\[hr\]/gi, '
    '); // close
  • items:
  • runs until next
  • /
      /
        end h = h.replace(/
      1. ([^<]*)(?=
      2. |<\/(ul|ol)>)/g, '
      3. $1
      4. '); h = h.replace(/\r\n|\r|\n/g, '
        '); h = h.split(PH).map(function (part, i) { if (i % 2 === 1) return blocks[parseInt(part, 10)]; return part; }).join(''); return h; } return { convert: convert, preview: preview, decodeEntities: decodeEntities, version: '1.0.0' }; })); /* ---------- UI glue (browser only) ---------- */ (function () { 'use strict'; if (typeof document === 'undefined') return; var E = (typeof module !== 'undefined' && module.exports) ? module.exports : (typeof self !== 'undefined' ? self.HTMLToBBCode : this.HTMLToBBCode); function $(id) { return document.getElementById(id); } var input = $('h2b-input'), outCode = $('h2b-code-inner'), outPre = $('h2b-code'), previewPane = $('h2b-preview'), stats = $('h2b-stats'), optHead = $('h2b-opt-headings'), optImg = $('h2b-opt-images'), optLinks = $('h2b-opt-links'), optPlain = $('h2b-opt-plain'), timer = null; function opts() { return { headings: optHead.checked, images: optImg.checked, links: optLinks.checked, plain: optPlain.checked }; } function convert() { var res = E.convert(input.value, opts()); outCode.textContent = res.bbcode; // Safe: preview() escapes HTML first and only emits an allow-list of tags. previewPane.innerHTML = E.preview(res.bbcode); stats.textContent = res.stats.inChars + ' chars in · ' + res.stats.outChars + ' chars out · ' + res.stats.tags + ' tags converted'; } function schedule() { clearTimeout(timer); timer = setTimeout(convert, 250); } input.addEventListener('input', schedule); [optHead, optImg, optLinks, optPlain].forEach(function (el) { el.addEventListener('change', convert); }); var tabs = document.querySelectorAll('.h2b-tab'); tabs.forEach(function (t) { t.addEventListener('click', function () { tabs.forEach(function (x) { x.classList.remove('h2b-tab-active'); }); t.classList.add('h2b-tab-active'); var isPreview = t.getAttribute('data-tab') === 'preview'; previewPane.hidden = !isPreview; outPre.hidden = isPreview; }); }); var SAMPLES = { article: [ '

        Welcome to Our Forum

        ', '

        Please read the community rules before posting. ', 'You can use bold, italic, underline and strikethrough in your messages.

        ', '
        AdminBe kind, stay on topic, and search before asking.
        ', '

        Quick checklist

        ', '
          ', '
        • Introduce yourself in the welcome thread
        • ', '
        • Share code with print("hello") blocks
        • ', '
        • Quote replies with context
        • ', '
        ', '

        Code example

        ', '
        def hello():',
                    '    return "world"
        ', '

        Forum logo

        ', '

        Colored text and styled text work too.

        ' ].join('\n'), newsletter: [ '

        Monthly Newsletter

        ', '

        Hello subscriber, here is what changed this month:

        ', '
          ', '
        1. New features launched
        2. ', '
        3. Bug fixes and performance improvements
        4. ', '
        ', '', '', '', '', '
        PlanPrice
        StarterFree
        Pro$9/mo
        ', '
        ', '

        Reply with STOP to unsubscribe.

        ' ].join('\n') }; document.querySelectorAll('.h2b-sample-btn').forEach(function (btn) { btn.addEventListener('click', function () { input.value = SAMPLES[btn.getAttribute('data-sample')]; convert(); input.focus(); }); }); $('h2b-clear').addEventListener('click', function () { input.value = ''; convert(); input.focus(); }); $('h2b-convert').addEventListener('click', convert); $('h2b-upload').addEventListener('change', function (ev) { var f = ev.target.files && ev.target.files[0]; if (!f) return; var rd = new FileReader(); rd.onload = function () { input.value = String(rd.result || ''); convert(); }; rd.readAsText(f); ev.target.value = ''; }); $('h2b-copy').addEventListener('click', function () { var done = function (ok) { var b = $('h2b-copy'); b.textContent = ok ? 'Copied!' : 'Copy failed'; setTimeout(function () { b.textContent = 'Copy BBCode'; }, 1600); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(outCode.textContent).then(function () { done(true); }, function () { done(false); }); } else { var ta = document.createElement('textarea'); ta.value = outCode.textContent; document.body.appendChild(ta); ta.select(); try { done(document.execCommand('copy')); } catch (e) { done(false); } document.body.removeChild(ta); } }); $('h2b-download').addEventListener('click', function () { var blob = new Blob([outCode.textContent], { type: 'text/plain;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'html-to-bbcode.txt'; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500); }); convert(); })();

        Free tool by NRTechStudio