var pendingLoadsByBasePath = {};

var INLINE_CONTAINER_TAGS = {
    a: true,
    abbr: true,
    b: true,
    big: true,
    cite: true,
    code: true,
    del: true,
    em: true,
    font: true,
    i: true,
    label: true,
    mark: true,
    q: true,
    s: true,
    small: true,
    span: true,
    strike: true,
    strong: true,
    sub: true,
    sup: true,
    u: true
};

var BLOCK_CONTAINER_TAGS = {
    article: true,
    aside: true,
    div: true,
    footer: true,
    header: true,
    main: true,
    section: true
};

var HTML_ENTITY_MAP = {
    amp: "&",
    apos: "'",
    gt: ">",
    lt: "<",
    nbsp: " ",
    quot: '"'
};

var HTML_VOID_TAGS = {
    area: true,
    base: true,
    br: true,
    col: true,
    embed: true,
    hr: true,
    img: true,
    input: true,
    link: true,
    meta: true,
    param: true,
    source: true,
    track: true,
    wbr: true
};

var BLOCK_NODE_TYPES = {
    blockquote: true,
    bulletList: true,
    codeBlock: true,
    heading: true,
    horizontalRule: true,
    htmlBlock: true,
    image: true,
    orderedList: true,
    paragraph: true,
    table: true
};

var INLINE_NODE_TYPES = {
    hardBreak: true,
    htmlInline: true,
    image: true,
    text: true
};

var MARK_TYPES = {
    bold: true,
    code: true,
    italic: true,
    link: true,
    strike: true,
    subscript: true,
    superscript: true,
    textStyle: true,
    underline: true
};

function ensureBrowserEnvironment() {
    if (typeof window === "undefined" || typeof document === "undefined") {
        throw new Error("RichTextEditor integrations require a browser environment.");
    }
}

function ensureRichTextEditorConstructor() {
    ensureBrowserEnvironment();
    if (!window.RichTextEditor) {
        throw new Error("RichTextEditor core is not loaded. Serve the /richtexteditor assets or load rte.js before mounting the framework wrapper.");
    }
    return window.RichTextEditor;
}

function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}

function getElementTagName(node) {
    return node && node.tagName ? String(node.tagName).toLowerCase() : "";
}

function normalizePlainText(value) {
    return String(value || "").replace(/\s+/g, " ").trim();
}

function getPlainTextFromHtml(html) {
    var container = getDocumentBody(html);
    if (!container) {
        return normalizePlainText(String(html || "").replace(/<[^>]+>/g, " "));
    }

    return normalizePlainText(container.textContent || "");
}

function decodeHtmlEntities(value) {
    return String(value || "").replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, function (match, entity) {
        var normalized = String(entity || "").toLowerCase();

        if (normalized.charAt(0) === "#") {
            var isHex = normalized.charAt(1) === "x";
            var codePoint = parseInt(normalized.substring(isHex ? 2 : 1), isHex ? 16 : 10);
            if (!isNaN(codePoint)) {
                try {
                    return String.fromCodePoint(codePoint);
                } catch (error) {
                    return match;
                }
            }
        }

        if (Object.prototype.hasOwnProperty.call(HTML_ENTITY_MAP, normalized)) {
            return HTML_ENTITY_MAP[normalized];
        }

        return match;
    });
}

function kebabToCamelCase(value) {
    return String(value || "").replace(/-([a-z])/g, function (match, letter) {
        return letter.toUpperCase();
    });
}

function parseStyleText(value) {
    var style = {};
    var declarations = String(value || "").split(";");

    for (var index = 0; index < declarations.length; index++) {
        var declaration = declarations[index];
        if (!declaration) {
            continue;
        }

        var separatorIndex = declaration.indexOf(":");
        if (separatorIndex < 0) {
            continue;
        }

        var key = kebabToCamelCase(declaration.substring(0, separatorIndex).replace(/^\s+|\s+$/g, ""));
        var cssValue = declaration.substring(separatorIndex + 1).replace(/^\s+|\s+$/g, "");
        if (!key || !cssValue) {
            continue;
        }

        style[key] = cssValue;
    }

    return style;
}

function createVirtualTextNode(value) {
    var decoded = decodeHtmlEntities(value);
    return {
        childNodes: [],
        nodeType: 3,
        nodeValue: decoded,
        outerHTML: "",
        textContent: decoded
    };
}

function createVirtualElementNode(tagName, attrs) {
    var attributeMap = attrs || {};
    var normalizedTagName = String(tagName || "").toLowerCase();

    return {
        _attrs: attributeMap,
        childNodes: [],
        getAttribute: function (name) {
            var normalizedName = String(name || "").toLowerCase();
            return Object.prototype.hasOwnProperty.call(attributeMap, normalizedName) ? attributeMap[normalizedName] : null;
        },
        nodeType: 1,
        outerHTML: "",
        style: parseStyleText(attributeMap.style || ""),
        tagName: normalizedTagName.toUpperCase(),
        textContent: ""
    };
}

function parseHtmlAttributes(source) {
    var attrs = {};
    var pattern = /([^\s=\/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
    var match = null;

    while ((match = pattern.exec(source || ""))) {
        var name = String(match[1] || "").toLowerCase();
        if (!name) {
            continue;
        }

        var value = match[2];
        if (value === undefined) {
            value = match[3];
        }
        if (value === undefined) {
            value = match[4];
        }

        attrs[name] = decodeHtmlEntities(value === undefined ? "" : value);
    }

    return attrs;
}

function finalizeVirtualNode(node) {
    if (!node) {
        return null;
    }

    if (node.nodeType === 3) {
        node.textContent = node.nodeValue || "";
        node.outerHTML = escapeHtml(node.nodeValue || "");
        return node;
    }

    var childHtml = "";
    var textContent = "";

    for (var index = 0; index < node.childNodes.length; index++) {
        var child = finalizeVirtualNode(node.childNodes[index]);
        if (!child) {
            continue;
        }

        childHtml += child.nodeType === 3 ? escapeHtml(child.nodeValue || "") : child.outerHTML || "";
        textContent += child.textContent || "";
    }

    node.textContent = textContent;

    var tagName = getElementTagName(node);
    if (!tagName) {
        node.outerHTML = childHtml;
        return node;
    }

    if (tagName === "body") {
        node.outerHTML = childHtml;
        return node;
    }

    var attrs = node._attrs || {};
    var attrString = buildAttributeString(attrs);
    if (HTML_VOID_TAGS[tagName]) {
        node.outerHTML = "<" + tagName + attrString + " />";
        return node;
    }

    node.outerHTML = "<" + tagName + attrString + ">" + childHtml + "</" + tagName + ">";
    return node;
}

function createVirtualDocumentBody(html) {
    var root = createVirtualElementNode("body", {});
    var stack = [root];
    var tokenPattern = /<!--[\s\S]*?-->|<!doctype[^>]*>|<\/?[^>]+>|[^<]+/gi;
    var match = null;

    while ((match = tokenPattern.exec(String(html || "")))) {
        var token = match[0];
        if (!token) {
            continue;
        }

        if (/^<!--/.test(token) || /^<!doctype/i.test(token)) {
            continue;
        }

        if (token.charAt(0) !== "<") {
            stack[stack.length - 1].childNodes.push(createVirtualTextNode(token));
            continue;
        }

        if (/^<\//.test(token)) {
            var closeTagMatch = /^<\s*\/\s*([^\s>\/]+)/.exec(token);
            if (!closeTagMatch) {
                continue;
            }

            var closingTag = String(closeTagMatch[1] || "").toLowerCase();
            while (stack.length > 1) {
                var currentTag = getElementTagName(stack[stack.length - 1]);
                stack.pop();
                if (currentTag === closingTag) {
                    break;
                }
            }

            continue;
        }

        var openTagMatch = /^<\s*([^\s>\/]+)/.exec(token);
        if (!openTagMatch) {
            continue;
        }

        var tagName = String(openTagMatch[1] || "").toLowerCase();
        var attrSource = token
            .replace(/^<\s*[^\s>\/]+/, "")
            .replace(/\/?\s*>$/, "");
        var element = createVirtualElementNode(tagName, parseHtmlAttributes(attrSource));
        stack[stack.length - 1].childNodes.push(element);

        if (!HTML_VOID_TAGS[tagName] && !/\/\s*>$/.test(token)) {
            stack.push(element);
        }
    }

    return finalizeVirtualNode(root);
}

function getDocumentBody(html) {
    if (typeof document !== "undefined" && document.implementation && document.implementation.createHTMLDocument) {
        var doc = document.implementation.createHTMLDocument("");
        doc.body.innerHTML = html || "";
        return doc.body;
    }

    if (typeof DOMParser !== "undefined") {
        return new DOMParser().parseFromString("<!doctype html><html><body>" + (html || "") + "</body></html>", "text/html").body;
    }

    return createVirtualDocumentBody(html);
}

function copyAttributes(attrs) {
    var next = {};
    var hasValues = false;
    for (var key in attrs) {
        if (!Object.prototype.hasOwnProperty.call(attrs, key)) {
            continue;
        }

        if (attrs[key] === undefined || attrs[key] === null || attrs[key] === "") {
            continue;
        }

        next[key] = attrs[key];
        hasValues = true;
    }

    return hasValues ? next : undefined;
}

function cloneMark(mark) {
    return {
        type: mark.type,
        attrs: copyAttributes(mark.attrs || {})
    };
}

function cloneMarks(marks) {
    if (!marks || !marks.length) {
        return undefined;
    }

    var next = [];
    for (var index = 0; index < marks.length; index++) {
        next.push(cloneMark(marks[index]));
    }
    return next;
}

function getMarkKey(mark) {
    return mark.type + ":" + JSON.stringify(mark.attrs || {});
}

function appendMark(marks, mark) {
    var next = marks.slice();
    var markKey = getMarkKey(mark);

    for (var index = 0; index < next.length; index++) {
        if (getMarkKey(next[index]) === markKey) {
            return next;
        }
    }

    next.push(cloneMark(mark));
    return next;
}

function sameMarks(left, right) {
    var leftLength = left ? left.length : 0;
    var rightLength = right ? right.length : 0;

    if (leftLength !== rightLength) {
        return false;
    }

    for (var index = 0; index < leftLength; index++) {
        if (getMarkKey(left[index]) !== getMarkKey(right[index])) {
            return false;
        }
    }

    return true;
}

function mergeAdjacentTextNodes(nodes) {
    var merged = [];

    for (var index = 0; index < nodes.length; index++) {
        var current = nodes[index];
        if (!current) {
            continue;
        }

        if (current.type === "text" && current.text) {
            var last = merged.length ? merged[merged.length - 1] : null;
            if (last && last.type === "text" && sameMarks(last.marks, current.marks)) {
                last.text += current.text;
                continue;
            }
        }

        merged.push(current);
    }

    return merged;
}

function createNode(type, options) {
    var node = { type: type };
    var source = options || {};

    if (source.attrs) {
        var attrs = copyAttributes(source.attrs);
        if (attrs) {
            node.attrs = attrs;
        }
    }

    if (source.content && source.content.length) {
        node.content = source.content;
    }

    if (source.text) {
        node.text = source.text;
    }

    if (source.marks && source.marks.length) {
        node.marks = source.marks;
    }

    if (source.html) {
        node.html = source.html;
    }

    return node;
}

function createStructuredContent(editorOrHtml) {
    var html = "";
    var text = "";

    if (typeof editorOrHtml === "string") {
        html = editorOrHtml;
        text = getPlainTextFromHtml(html);
    } else if (editorOrHtml) {
        html = editorOrHtml.getHTMLCode ? editorOrHtml.getHTMLCode() : "";
        text = editorOrHtml.getPlainText ? editorOrHtml.getPlainText() : "";
        if (!text) {
            text = getPlainTextFromHtml(html);
        }
    }

    return {
        type: "doc",
        version: 2,
        format: "richtexteditor-json",
        html: html || "",
        text: text || "",
        content: parseStructuredContentHtml(html || "")
    };
}

function parseStructuredContentHtml(html) {
    var root = getDocumentBody(html);
    if (!root) {
        return [];
    }

    return parseBlockNodes(root.childNodes);
}

function parseBlockNodes(nodeList) {
    var blocks = [];
    var inlineBuffer = [];

    function flushInlineBuffer() {
        if (!inlineBuffer.length) {
            return;
        }

        blocks.push(createNode("paragraph", {
            content: mergeAdjacentTextNodes(inlineBuffer)
        }));
        inlineBuffer = [];
    }

    for (var index = 0; index < nodeList.length; index++) {
        var node = nodeList[index];

        if (!node) {
            continue;
        }

        if (node.nodeType === 3) {
            var text = node.nodeValue || "";
            if (text.replace(/\s+/g, "") !== "") {
                inlineBuffer = inlineBuffer.concat(parseInlineNode(node, []));
            }
            continue;
        }

        if (node.nodeType !== 1) {
            continue;
        }

        var tagName = getElementTagName(node);

        if (tagName === "br" || INLINE_CONTAINER_TAGS[tagName]) {
            inlineBuffer = inlineBuffer.concat(parseInlineNode(node, []));
            continue;
        }

        flushInlineBuffer();

        var blockNode = parseBlockNode(node);
        if (!blockNode) {
            continue;
        }

        if (isArray(blockNode)) {
            for (var childIndex = 0; childIndex < blockNode.length; childIndex++) {
                blocks.push(blockNode[childIndex]);
            }
            continue;
        }

        blocks.push(blockNode);
    }

    flushInlineBuffer();
    return blocks;
}

function parseBlockNode(node) {
    var tagName = getElementTagName(node);

    if (BLOCK_CONTAINER_TAGS[tagName]) {
        var childBlocks = parseBlockNodes(node.childNodes);
        if (childBlocks.length) {
            return childBlocks;
        }

        return null;
    }

    if (tagName === "p") {
        return createNode("paragraph", {
            content: parseInlineChildren(node)
        });
    }

    if (/^h[1-6]$/.test(tagName)) {
        return createNode("heading", {
            attrs: { level: parseInt(tagName.charAt(1), 10) || 1 },
            content: parseInlineChildren(node)
        });
    }

    if (tagName === "blockquote") {
        return createNode("blockquote", {
            content: parseBlockNodes(node.childNodes)
        });
    }

    if (tagName === "pre") {
        return createNode("codeBlock", {
            text: node.textContent || ""
        });
    }

    if (tagName === "ul") {
        return createNode("bulletList", {
            content: parseListItems(node)
        });
    }

    if (tagName === "ol") {
        var startValue = node.getAttribute("start");
        return createNode("orderedList", {
            attrs: startValue ? { start: parseInt(startValue, 10) || 1 } : undefined,
            content: parseListItems(node)
        });
    }

    if (tagName === "li") {
        return createNode("listItem", {
            content: parseBlockNodes(node.childNodes)
        });
    }

    if (tagName === "hr") {
        return createNode("horizontalRule");
    }

    if (tagName === "img") {
        return parseImageNode(node);
    }

    if (tagName === "table") {
        return createNode("table", {
            content: parseTableRows(node)
        });
    }

    return createNode("htmlBlock", {
        html: node.outerHTML || "",
        text: normalizePlainText(node.textContent || "")
    });
}

function parseListItems(listElement) {
    var items = [];

    for (var index = 0; index < listElement.childNodes.length; index++) {
        var child = listElement.childNodes[index];
        if (child && child.nodeType === 1 && getElementTagName(child) === "li") {
            items.push(parseBlockNode(child));
        }
    }

    return items;
}

function parseTableRows(tableElement) {
    var rows = [];

    function collectRows(node) {
        for (var index = 0; index < node.childNodes.length; index++) {
            var child = node.childNodes[index];
            if (!child || child.nodeType !== 1) {
                continue;
            }

            var tagName = getElementTagName(child);
            if (tagName === "tr") {
                rows.push(parseTableRow(child));
                continue;
            }

            if (tagName === "thead" || tagName === "tbody" || tagName === "tfoot") {
                collectRows(child);
            }
        }
    }

    collectRows(tableElement);
    return rows;
}

function parseTableRow(rowElement) {
    var cells = [];

    for (var index = 0; index < rowElement.childNodes.length; index++) {
        var cell = rowElement.childNodes[index];
        if (!cell || cell.nodeType !== 1) {
            continue;
        }

        var tagName = getElementTagName(cell);
        if (tagName === "td" || tagName === "th") {
            cells.push(parseTableCell(cell));
        }
    }

    return createNode("tableRow", {
        content: cells
    });
}

function parseTableCell(cellElement) {
    var attrs = {};
    var colspan = cellElement.getAttribute("colspan");
    var rowspan = cellElement.getAttribute("rowspan");

    if (getElementTagName(cellElement) === "th") {
        attrs.header = true;
    }

    if (colspan) {
        attrs.colspan = parseInt(colspan, 10) || 1;
    }

    if (rowspan) {
        attrs.rowspan = parseInt(rowspan, 10) || 1;
    }

    return createNode("tableCell", {
        attrs: attrs,
        content: parseBlockNodes(cellElement.childNodes)
    });
}

function parseImageNode(element) {
    return createNode("image", {
        attrs: {
            src: element.getAttribute("src") || "",
            alt: element.getAttribute("alt") || "",
            title: element.getAttribute("title") || "",
            width: element.getAttribute("width") || "",
            height: element.getAttribute("height") || ""
        }
    });
}

function parseInlineChildren(element, marks) {
    return mergeAdjacentTextNodes(parseInlineNodes(element.childNodes, marks || []));
}

function parseInlineNodes(nodeList, activeMarks) {
    var nodes = [];

    for (var index = 0; index < nodeList.length; index++) {
        nodes = nodes.concat(parseInlineNode(nodeList[index], activeMarks));
    }

    return nodes;
}

function parseInlineNode(node, activeMarks) {
    if (!node) {
        return [];
    }

    if (node.nodeType === 3) {
        if (!node.nodeValue) {
            return [];
        }

        return [createNode("text", {
            text: node.nodeValue,
            marks: activeMarks.length ? activeMarks.slice() : undefined
        })];
    }

    if (node.nodeType !== 1) {
        return [];
    }

    var tagName = getElementTagName(node);
    if (tagName === "br") {
        return [createNode("hardBreak")];
    }

    if (tagName === "img") {
        return [parseImageNode(node)];
    }

    var marks = activeMarks.slice();
    var extractedMarks = extractElementMarks(node);
    for (var index = 0; index < extractedMarks.length; index++) {
        marks = appendMark(marks, extractedMarks[index]);
    }

    return parseInlineNodes(node.childNodes, marks);
}

function extractElementMarks(element) {
    var marks = [];
    var tagName = getElementTagName(element);
    var style = element.style || {};
    var textDecoration = style.textDecorationLine || style.textDecoration || "";
    var styleAttrs = {};

    function pushMark(type, attrs) {
        marks.push({
            type: type,
            attrs: copyAttributes(attrs || {})
        });
    }

    if (tagName === "strong" || tagName === "b") {
        pushMark("bold");
    }

    if (tagName === "em" || tagName === "i") {
        pushMark("italic");
    }

    if (tagName === "u") {
        pushMark("underline");
    }

    if (tagName === "s" || tagName === "strike" || tagName === "del") {
        pushMark("strike");
    }

    if (tagName === "code") {
        pushMark("code");
    }

    if (tagName === "sub") {
        pushMark("subscript");
    }

    if (tagName === "sup") {
        pushMark("superscript");
    }

    if (tagName === "a") {
        pushMark("link", {
            href: element.getAttribute("href") || "",
            rel: element.getAttribute("rel") || "",
            target: element.getAttribute("target") || "",
            title: element.getAttribute("title") || ""
        });
    }

    if (style.fontWeight && (/bold|[5-9]00/).test(style.fontWeight)) {
        pushMark("bold");
    }

    if (style.fontStyle === "italic") {
        pushMark("italic");
    }

    if (textDecoration.indexOf("underline") >= 0) {
        pushMark("underline");
    }

    if (textDecoration.indexOf("line-through") >= 0) {
        pushMark("strike");
    }

    if (style.color) {
        styleAttrs.color = style.color;
    }

    if (style.backgroundColor) {
        styleAttrs.backgroundColor = style.backgroundColor;
    } else if (tagName === "mark") {
        styleAttrs.backgroundColor = "#fff2ac";
    }

    if (style.fontSize) {
        styleAttrs.fontSize = style.fontSize;
    }

    if (style.fontFamily) {
        styleAttrs.fontFamily = style.fontFamily;
    }

    if (!styleAttrs.color && element.getAttribute("color")) {
        styleAttrs.color = element.getAttribute("color");
    }

    if (!styleAttrs.fontFamily && element.getAttribute("face")) {
        styleAttrs.fontFamily = element.getAttribute("face");
    }

    if (!styleAttrs.fontSize && element.getAttribute("size")) {
        styleAttrs.fontSize = element.getAttribute("size");
    }

    if (tagName === "small" && !styleAttrs.fontSize) {
        styleAttrs.fontSize = "0.85em";
    }

    if (tagName === "big" && !styleAttrs.fontSize) {
        styleAttrs.fontSize = "1.15em";
    }

    if (copyAttributes(styleAttrs)) {
        pushMark("textStyle", styleAttrs);
    }

    return marks;
}

function escapeHtml(value) {
    return String(value || "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");
}

function escapeAttribute(value) {
    return escapeHtml(value).replace(/"/g, "&quot;");
}

function buildAttributeString(attrs) {
    var result = "";

    if (!attrs) {
        return result;
    }

    for (var key in attrs) {
        if (!Object.prototype.hasOwnProperty.call(attrs, key)) {
            continue;
        }

        var value = attrs[key];
        if (value === undefined || value === null || value === "") {
            continue;
        }

        result += " " + key + '="' + escapeAttribute(value) + '"';
    }

    return result;
}

function buildStyleString(attrs) {
    if (!attrs) {
        return "";
    }

    var styles = [];
    if (attrs.color) {
        styles.push("color: " + attrs.color);
    }
    if (attrs.backgroundColor) {
        styles.push("background-color: " + attrs.backgroundColor);
    }
    if (attrs.fontSize) {
        styles.push("font-size: " + attrs.fontSize);
    }
    if (attrs.fontFamily) {
        styles.push("font-family: " + attrs.fontFamily);
    }

    return styles.join("; ");
}

function serializeInlineNodes(nodes) {
    var html = "";

    for (var index = 0; index < (nodes || []).length; index++) {
        html += serializeNode(nodes[index], true);
    }

    return html;
}

function serializeBlockNodes(nodes) {
    var html = "";

    for (var index = 0; index < (nodes || []).length; index++) {
        html += serializeNode(nodes[index], false);
    }

    return html;
}

function serializeListItemContent(nodes) {
    if (!nodes || !nodes.length) {
        return "";
    }

    if (nodes.length === 1 && nodes[0].type === "paragraph") {
        return serializeInlineNodes(nodes[0].content || []);
    }

    return serializeBlockNodes(nodes);
}

function serializeTableCellContent(nodes) {
    if (!nodes || !nodes.length) {
        return "";
    }

    if (nodes.length === 1 && nodes[0].type === "paragraph") {
        return serializeInlineNodes(nodes[0].content || []);
    }

    return serializeBlockNodes(nodes);
}

function applyMark(mark, html) {
    var attrs = mark.attrs || {};

    if (mark.type === "bold") {
        return "<strong>" + html + "</strong>";
    }

    if (mark.type === "italic") {
        return "<em>" + html + "</em>";
    }

    if (mark.type === "underline") {
        return "<u>" + html + "</u>";
    }

    if (mark.type === "strike") {
        return "<s>" + html + "</s>";
    }

    if (mark.type === "code") {
        return "<code>" + html + "</code>";
    }

    if (mark.type === "subscript") {
        return "<sub>" + html + "</sub>";
    }

    if (mark.type === "superscript") {
        return "<sup>" + html + "</sup>";
    }

    if (mark.type === "link") {
        return "<a" + buildAttributeString({
            href: attrs.href,
            rel: attrs.rel,
            target: attrs.target,
            title: attrs.title
        }) + ">" + html + "</a>";
    }

    if (mark.type === "textStyle") {
        var style = buildStyleString(attrs);
        return style ? '<span style="' + escapeAttribute(style) + '">' + html + "</span>" : html;
    }

    return html;
}

function serializeNode(node, inlineMode) {
    if (!node || typeof node !== "object") {
        return "";
    }

    if (node.type === "doc") {
        return serializeBlockNodes(node.content || []);
    }

    if (node.type === "text") {
        var textHtml = escapeHtml(node.text || "");
        var marks = node.marks || [];

        for (var markIndex = marks.length - 1; markIndex >= 0; markIndex--) {
            textHtml = applyMark(marks[markIndex], textHtml);
        }

        return textHtml;
    }

    if (node.type === "hardBreak") {
        return "<br />";
    }

    if (node.type === "image") {
        return "<img" + buildAttributeString(node.attrs || {}) + " />";
    }

    if (node.type === "paragraph") {
        return inlineMode
            ? serializeInlineNodes(node.content || [])
            : "<p>" + serializeInlineNodes(node.content || []) + "</p>";
    }

    if (node.type === "heading") {
        var level = 1;
        if (node.attrs && node.attrs.level) {
            level = parseInt(node.attrs.level, 10) || 1;
        }
        if (level < 1) {
            level = 1;
        }
        if (level > 6) {
            level = 6;
        }

        return "<h" + level + ">" + serializeInlineNodes(node.content || []) + "</h" + level + ">";
    }

    if (node.type === "blockquote") {
        return "<blockquote>" + serializeBlockNodes(node.content || []) + "</blockquote>";
    }

    if (node.type === "codeBlock") {
        return "<pre><code>" + escapeHtml(node.text || "") + "</code></pre>";
    }

    if (node.type === "bulletList") {
        return "<ul>" + serializeBlockNodes(node.content || []) + "</ul>";
    }

    if (node.type === "orderedList") {
        var listAttrs = {};
        if (node.attrs && node.attrs.start) {
            listAttrs.start = node.attrs.start;
        }
        return "<ol" + buildAttributeString(listAttrs) + ">" + serializeBlockNodes(node.content || []) + "</ol>";
    }

    if (node.type === "listItem") {
        return "<li>" + serializeListItemContent(node.content || []) + "</li>";
    }

    if (node.type === "horizontalRule") {
        return "<hr />";
    }

    if (node.type === "table") {
        return "<table>" + serializeBlockNodes(node.content || []) + "</table>";
    }

    if (node.type === "tableRow") {
        return "<tr>" + serializeBlockNodes(node.content || []) + "</tr>";
    }

    if (node.type === "tableCell") {
        var cellTag = node.attrs && node.attrs.header ? "th" : "td";
        var cellAttrs = {};

        if (node.attrs) {
            if (node.attrs.colspan) {
                cellAttrs.colspan = node.attrs.colspan;
            }
            if (node.attrs.rowspan) {
                cellAttrs.rowspan = node.attrs.rowspan;
            }
        }

        return "<" + cellTag + buildAttributeString(cellAttrs) + ">" + serializeTableCellContent(node.content || []) + "</" + cellTag + ">";
    }

    if (node.type === "htmlBlock" || node.type === "htmlInline") {
        return node.html || "";
    }

    if (isArray(node.content)) {
        return inlineMode ? serializeInlineNodes(node.content) : serializeBlockNodes(node.content);
    }

    if (typeof node.html === "string") {
        return node.html;
    }

    if (typeof node.text === "string") {
        return escapeHtml(node.text);
    }

    return "";
}

function serializeStructuredContent(value) {
    if (value == null) {
        return "";
    }

    if (typeof value === "string") {
        var trimmed = value.trim();
        if (trimmed && (trimmed.charAt(0) === "{" || trimmed.charAt(0) === "[")) {
            try {
                return serializeStructuredContent(JSON.parse(trimmed));
            } catch (error) {
                return value;
            }
        }

        return value;
    }

    if (typeof value !== "object") {
        return String(value);
    }

    if (value.type === "doc" && isArray(value.content)) {
        return serializeBlockNodes(value.content);
    }

    if (value.type && value.type !== "doc") {
        return serializeNode(value, false);
    }

    if (isArray(value.content)) {
        return serializeBlockNodes(value.content);
    }

    if (value.content && typeof value.content.html === "string") {
        return value.content.html;
    }

    if (typeof value.html === "string") {
        return value.html;
    }

    return "";
}

function normalizeStructuredContent(value) {
    return serializeStructuredContent(value);
}

function createValidationIssue(path, message) {
    return {
        message: message,
        path: path
    };
}

function isStructuredNodeObject(value) {
    return !!value && typeof value === "object" && !isArray(value);
}

function createValidationDocument(value) {
    if (value == null) {
        return createStructuredDocument(value);
    }

    if (typeof value === "string") {
        return createStructuredDocument(value);
    }

    if (!isStructuredNodeObject(value)) {
        return createStructuredDocument(String(value));
    }

    if (value.type === "doc") {
        return {
            type: "doc",
            version: typeof value.version === "number" ? value.version : 2,
            format: typeof value.format === "string" ? value.format : "richtexteditor-json",
            html: typeof value.html === "string" ? value.html : undefined,
            text: typeof value.text === "string" ? value.text : undefined,
            content: isArray(value.content) ? value.content : []
        };
    }

    if (value.type && value.type !== "doc") {
        return {
            type: "doc",
            version: 2,
            format: "richtexteditor-json",
            content: [value]
        };
    }

    if (isArray(value.content)) {
        return {
            type: "doc",
            version: typeof value.version === "number" ? value.version : 2,
            format: typeof value.format === "string" ? value.format : "richtexteditor-json",
            html: typeof value.html === "string" ? value.html : undefined,
            text: typeof value.text === "string" ? value.text : undefined,
            content: value.content
        };
    }

    if (value.content && isStructuredNodeObject(value.content)) {
        return {
            type: "doc",
            version: typeof value.version === "number" ? value.version : 2,
            format: typeof value.format === "string" ? value.format : "richtexteditor-json",
            html: typeof value.html === "string" ? value.html : undefined,
            text: typeof value.text === "string" ? value.text : undefined,
            content: [value.content]
        };
    }

    return createStructuredDocument(serializeStructuredContent(value));
}

function validateMark(mark, path, issues) {
    if (!isStructuredNodeObject(mark)) {
        issues.push(createValidationIssue(path, "Marks must be objects."));
        return;
    }

    if (typeof mark.type !== "string" || !mark.type) {
        issues.push(createValidationIssue(path + ".type", "Marks require a type."));
        return;
    }

    if (!MARK_TYPES[mark.type]) {
        issues.push(createValidationIssue(path + ".type", 'Unsupported mark type "' + mark.type + '".'));
    }

    if (mark.type === "link") {
        if (!mark.attrs || typeof mark.attrs.href !== "string" || !mark.attrs.href) {
            issues.push(createValidationIssue(path + ".attrs.href", "Link marks require a non-empty href."));
        }
    }
}

function validateNodeChildren(node, path, mode, issues) {
    if (!isArray(node.content)) {
        issues.push(createValidationIssue(path + ".content", "Node content must be an array."));
        return;
    }

    for (var index = 0; index < node.content.length; index++) {
        validateNode(node.content[index], path + ".content[" + index + "]", mode, issues);
    }
}

function validateNode(node, path, mode, issues) {
    if (!isStructuredNodeObject(node)) {
        issues.push(createValidationIssue(path, "Nodes must be objects."));
        return;
    }

    if (typeof node.type !== "string" || !node.type) {
        issues.push(createValidationIssue(path + ".type", "Nodes require a type."));
        return;
    }

    if (node.marks) {
        if (!isArray(node.marks)) {
            issues.push(createValidationIssue(path + ".marks", "Marks must be an array."));
        } else {
            for (var markIndex = 0; markIndex < node.marks.length; markIndex++) {
                validateMark(node.marks[markIndex], path + ".marks[" + markIndex + "]", issues);
            }
        }
    }

    if (mode === "inline" && !INLINE_NODE_TYPES[node.type]) {
        issues.push(createValidationIssue(path + ".type", 'Inline content cannot contain "' + node.type + '" nodes.'));
    }

    if (mode === "block" && !(BLOCK_NODE_TYPES[node.type] || node.type === "tableRow" || node.type === "tableCell" || node.type === "listItem")) {
        issues.push(createValidationIssue(path + ".type", 'Block content cannot contain "' + node.type + '" nodes.'));
    }

    if (node.type === "text") {
        if (typeof node.text !== "string") {
            issues.push(createValidationIssue(path + ".text", "Text nodes require a string text value."));
        }
        if (node.content) {
            issues.push(createValidationIssue(path + ".content", "Text nodes cannot contain child content."));
        }
        return;
    }

    if (node.type === "hardBreak") {
        if (node.content) {
            issues.push(createValidationIssue(path + ".content", "Hard break nodes cannot contain child content."));
        }
        return;
    }

    if (node.type === "image") {
        if (!node.attrs || typeof node.attrs.src !== "string" || !node.attrs.src) {
            issues.push(createValidationIssue(path + ".attrs.src", "Image nodes require a non-empty src."));
        }
        return;
    }

    if (node.type === "heading") {
        if (!node.attrs || typeof node.attrs.level !== "number" || node.attrs.level < 1 || node.attrs.level > 6) {
            issues.push(createValidationIssue(path + ".attrs.level", "Heading nodes require a level between 1 and 6."));
        }
        validateNodeChildren(node, path, "inline", issues);
        return;
    }

    if (node.type === "paragraph") {
        validateNodeChildren(node, path, "inline", issues);
        return;
    }

    if (node.type === "blockquote") {
        validateNodeChildren(node, path, "block", issues);
        return;
    }

    if (node.type === "codeBlock") {
        if (typeof node.text !== "string") {
            issues.push(createValidationIssue(path + ".text", "Code block nodes require a string text value."));
        }
        return;
    }

    if (node.type === "bulletList" || node.type === "orderedList") {
        if (node.type === "orderedList" && node.attrs && node.attrs.start !== undefined) {
            if (typeof node.attrs.start !== "number" || node.attrs.start < 1) {
                issues.push(createValidationIssue(path + ".attrs.start", "Ordered lists require a positive numeric start value."));
            }
        }

        if (!isArray(node.content)) {
            issues.push(createValidationIssue(path + ".content", "List nodes require a content array."));
            return;
        }

        for (var listIndex = 0; listIndex < node.content.length; listIndex++) {
            var listItem = node.content[listIndex];
            if (!listItem || listItem.type !== "listItem") {
                issues.push(createValidationIssue(path + ".content[" + listIndex + "]", "Lists can only contain listItem nodes."));
                continue;
            }
            validateNode(listItem, path + ".content[" + listIndex + "]", "block", issues);
        }
        return;
    }

    if (node.type === "listItem") {
        validateNodeChildren(node, path, "block", issues);
        return;
    }

    if (node.type === "horizontalRule") {
        return;
    }

    if (node.type === "table") {
        if (!isArray(node.content)) {
            issues.push(createValidationIssue(path + ".content", "Table nodes require a content array."));
            return;
        }

        for (var rowIndex = 0; rowIndex < node.content.length; rowIndex++) {
            var row = node.content[rowIndex];
            if (!row || row.type !== "tableRow") {
                issues.push(createValidationIssue(path + ".content[" + rowIndex + "]", "Tables can only contain tableRow nodes."));
                continue;
            }
            validateNode(row, path + ".content[" + rowIndex + "]", "block", issues);
        }
        return;
    }

    if (node.type === "tableRow") {
        if (!isArray(node.content)) {
            issues.push(createValidationIssue(path + ".content", "Table rows require a content array."));
            return;
        }

        for (var cellIndex = 0; cellIndex < node.content.length; cellIndex++) {
            var cell = node.content[cellIndex];
            if (!cell || cell.type !== "tableCell") {
                issues.push(createValidationIssue(path + ".content[" + cellIndex + "]", "Table rows can only contain tableCell nodes."));
                continue;
            }
            validateNode(cell, path + ".content[" + cellIndex + "]", "block", issues);
        }
        return;
    }

    if (node.type === "tableCell") {
        validateNodeChildren(node, path, "block", issues);
        return;
    }

    if (node.type === "htmlBlock" || node.type === "htmlInline") {
        if (typeof node.html !== "string") {
            issues.push(createValidationIssue(path + ".html", "HTML nodes require an html string."));
        }
        return;
    }

    issues.push(createValidationIssue(path + ".type", 'Unsupported node type "' + node.type + '".'));
}

function validateStructuredContent(value) {
    var documentModel = createValidationDocument(value);
    var issues = [];

    if (documentModel.type !== "doc") {
        issues.push(createValidationIssue("type", 'Structured content roots must use the "doc" type.'));
    }

    if (typeof documentModel.version !== "number") {
        issues.push(createValidationIssue("version", "Structured content documents require a numeric version."));
    }

    if (typeof documentModel.format !== "string" || !documentModel.format) {
        issues.push(createValidationIssue("format", "Structured content documents require a format string."));
    }

    if (!isArray(documentModel.content)) {
        issues.push(createValidationIssue("content", "Structured content documents require a content array."));
    } else {
        for (var index = 0; index < documentModel.content.length; index++) {
            validateNode(documentModel.content[index], "content[" + index + "]", "block", issues);
        }
    }

    return {
        document: documentModel,
        issues: issues,
        valid: issues.length === 0
    };
}

function createStructuredDocument(value) {
    if (value == null) {
        return createStructuredContent("");
    }

    if (typeof value === "string") {
        var trimmed = value.trim();
        if (trimmed && (trimmed.charAt(0) === "{" || trimmed.charAt(0) === "[")) {
            try {
                return createStructuredDocument(JSON.parse(trimmed));
            } catch (error) {
                return createStructuredContent(value);
            }
        }

        return createStructuredContent(value);
    }

    if (typeof value !== "object") {
        return createStructuredContent(String(value));
    }

    if (value.type === "doc" && isArray(value.content)) {
        var docHtml = serializeStructuredContent(value);
        return {
            type: "doc",
            version: typeof value.version === "number" ? value.version : 2,
            format: value.format || "richtexteditor-json",
            html: docHtml,
            text: getPlainTextFromHtml(docHtml),
            content: value.content
        };
    }

    return createStructuredContent(serializeStructuredContent(value));
}

function normalizeMarkdownSource(markdown) {
    return String(markdown || "").replace(/\r\n?/g, "\n");
}

function stripMarkdownIndent(line, maxIndent) {
    var index = 0;
    var limit = Math.max(0, maxIndent || 0);

    while (index < line.length && index < limit && line.charAt(index) === " ") {
        index++;
    }

    return line.substring(index);
}

function getMarkdownListMatch(line) {
    var bulletMatch = /^(\s*)([-+*])\s+(.*)$/.exec(line);
    if (bulletMatch) {
        return {
            indent: bulletMatch[1].length,
            ordered: false,
            start: 1,
            text: bulletMatch[3],
            marker: bulletMatch[2],
            contentIndent: bulletMatch[1].length + bulletMatch[2].length + 1
        };
    }

    var orderedMatch = /^(\s*)(\d+)\.\s+(.*)$/.exec(line);
    if (orderedMatch) {
        return {
            indent: orderedMatch[1].length,
            ordered: true,
            start: parseInt(orderedMatch[2], 10) || 1,
            text: orderedMatch[3],
            marker: orderedMatch[2] + ".",
            contentIndent: orderedMatch[1].length + orderedMatch[2].length + 2
        };
    }

    return null;
}

function getMarkdownLineIndent(line) {
    var match = /^(\s*)/.exec(line);
    return match ? match[1].length : 0;
}

function isMarkdownFence(line) {
    return /^\s*```/.test(line || "");
}

function isMarkdownHeading(line) {
    return /^\s{0,3}#{1,6}\s+/.test(line || "");
}

function isMarkdownHorizontalRule(line) {
    var trimmed = String(line || "").trim();
    return /^(\*\s*){3,}$/.test(trimmed) || /^(-\s*){3,}$/.test(trimmed) || /^(_\s*){3,}$/.test(trimmed);
}

function isMarkdownBlockquote(line) {
    return /^\s{0,3}>/.test(line || "");
}

function isMarkdownBlockStarter(line) {
    return isMarkdownFence(line) || isMarkdownHeading(line) || isMarkdownHorizontalRule(line) || isMarkdownBlockquote(line) || !!getMarkdownListMatch(line);
}

function isMarkdownBlankLine(line) {
    return String(line || "").trim() === "";
}

function parseMarkdownBlocksFromLines(lines) {
    return parseMarkdownBlocks({ lines: lines, index: 0 });
}

function parseMarkdownBlocks(state) {
    var nodes = [];

    while (state.index < state.lines.length) {
        var line = state.lines[state.index];

        if (isMarkdownBlankLine(line)) {
            state.index++;
            continue;
        }

        if (isMarkdownFence(line)) {
            nodes.push(parseMarkdownCodeBlock(state));
            continue;
        }

        if (isMarkdownHeading(line)) {
            nodes.push(parseMarkdownHeading(state));
            continue;
        }

        if (isMarkdownHorizontalRule(line)) {
            state.index++;
            nodes.push(createNode("horizontalRule"));
            continue;
        }

        if (isMarkdownBlockquote(line)) {
            nodes.push(parseMarkdownBlockquote(state));
            continue;
        }

        var listMatch = getMarkdownListMatch(line);
        if (listMatch) {
            nodes.push(parseMarkdownList(state, listMatch));
            continue;
        }

        nodes.push(parseMarkdownParagraph(state));
    }

    return nodes;
}

function parseMarkdownCodeBlock(state) {
    var infoMatch = /^\s*```(.*)$/.exec(state.lines[state.index] || "");
    var language = infoMatch && infoMatch[1] ? infoMatch[1].replace(/\s+$/, "") : "";
    var buffer = [];

    state.index++;

    while (state.index < state.lines.length && !isMarkdownFence(state.lines[state.index])) {
        buffer.push(state.lines[state.index]);
        state.index++;
    }

    if (state.index < state.lines.length) {
        state.index++;
    }

    return createNode("codeBlock", {
        attrs: language ? { language: language } : undefined,
        text: buffer.join("\n")
    });
}

function parseMarkdownHeading(state) {
    var match = /^\s{0,3}(#{1,6})\s+(.*)$/.exec(state.lines[state.index] || "");
    var level = match ? match[1].length : 1;
    var text = match ? match[2].replace(/\s+#+\s*$/, "") : "";

    state.index++;

    return createNode("heading", {
        attrs: { level: level },
        content: parseMarkdownInline(text, [])
    });
}

function parseMarkdownBlockquote(state) {
    var lines = [];

    while (state.index < state.lines.length) {
        var line = state.lines[state.index];
        if (isMarkdownBlankLine(line)) {
            lines.push("");
            state.index++;
            continue;
        }

        if (!isMarkdownBlockquote(line)) {
            break;
        }

        lines.push(String(line).replace(/^\s{0,3}>\s?/, ""));
        state.index++;
    }

    return createNode("blockquote", {
        content: parseMarkdownBlocksFromLines(lines)
    });
}

function parseMarkdownList(state, firstMatch) {
    var items = [];
    var ordered = firstMatch.ordered;
    var listIndent = firstMatch.indent;
    var start = firstMatch.start;

    while (state.index < state.lines.length) {
        var match = getMarkdownListMatch(state.lines[state.index]);
        if (!match || match.indent !== listIndent || match.ordered !== ordered) {
            break;
        }

        items.push(parseMarkdownListItem(state, match));
    }

    return createNode(ordered ? "orderedList" : "bulletList", {
        attrs: ordered && start !== 1 ? { start: start } : undefined,
        content: items
    });
}

function parseMarkdownListItem(state, match) {
    var itemLines = [match.text];

    state.index++;

    while (state.index < state.lines.length) {
        var line = state.lines[state.index];
        if (isMarkdownBlankLine(line)) {
            itemLines.push("");
            state.index++;
            continue;
        }

        var nextMatch = getMarkdownListMatch(line);
        if (nextMatch && nextMatch.indent === match.indent && nextMatch.ordered === match.ordered) {
            break;
        }

        var currentIndent = getMarkdownLineIndent(line);
        if (currentIndent > match.indent) {
            itemLines.push(stripMarkdownIndent(line, match.contentIndent));
            state.index++;
            continue;
        }

        if (match.indent === 0 && !nextMatch && !isMarkdownBlockStarter(line)) {
            itemLines.push(line);
            state.index++;
            continue;
        }

        break;
    }

    var itemContent = parseMarkdownBlocksFromLines(itemLines);
    if (!itemContent.length) {
        itemContent = [createNode("paragraph", {
            content: parseMarkdownInline(match.text, [])
        })];
    }

    return createNode("listItem", {
        content: itemContent
    });
}

function parseMarkdownParagraph(state) {
    var lines = [];

    while (state.index < state.lines.length) {
        var line = state.lines[state.index];
        if (isMarkdownBlankLine(line)) {
            break;
        }

        if (lines.length && isMarkdownBlockStarter(line)) {
            break;
        }

        lines.push(line);
        state.index++;
    }

    return createNode("paragraph", {
        content: parseMarkdownParagraphLines(lines)
    });
}

function parseMarkdownParagraphLines(lines) {
    var nodes = [];
    var insertHardBreak = false;

    for (var index = 0; index < lines.length; index++) {
        var line = String(lines[index] || "");
        var hasHardBreak = false;

        if (/\\$/.test(line)) {
            line = line.replace(/\\$/, "");
            hasHardBreak = true;
        } else if (/\s{2,}$/.test(line)) {
            line = line.replace(/\s+$/, "");
            hasHardBreak = true;
        }

        if (nodes.length) {
            nodes.push(insertHardBreak ? createNode("hardBreak") : createNode("text", { text: " " }));
        }

        nodes = nodes.concat(parseMarkdownInline(line, []));
        insertHardBreak = hasHardBreak;
    }

    return mergeAdjacentTextNodes(nodes);
}

function isMarkdownEscaped(value, index) {
    var slashCount = 0;
    for (var cursor = index - 1; cursor >= 0 && value.charAt(cursor) === "\\"; cursor--) {
        slashCount++;
    }
    return slashCount % 2 === 1;
}

function findMarkdownClosingDelimiter(value, delimiter, startIndex) {
    var cursor = startIndex;

    while (cursor < value.length) {
        var matchIndex = value.indexOf(delimiter, cursor);
        if (matchIndex < 0) {
            return -1;
        }

        if (!isMarkdownEscaped(value, matchIndex)) {
            return matchIndex;
        }

        cursor = matchIndex + delimiter.length;
    }

    return -1;
}

function createMarkdownTextNode(text, marks) {
    if (!text) {
        return null;
    }

    return createNode("text", {
        text: text,
        marks: cloneMarks(marks || [])
    });
}

function parseMarkdownInline(value, activeMarks) {
    var text = String(value || "");
    var nodes = [];
    var index = 0;

    while (index < text.length) {
        if (text.charAt(index) === "\\" && index + 1 < text.length) {
            var escapedNode = createMarkdownTextNode(text.charAt(index + 1), activeMarks);
            if (escapedNode) {
                nodes.push(escapedNode);
            }
            index += 2;
            continue;
        }

        if (text.substr(index, 2) === "![") {
            var imageToken = parseMarkdownImage(text, index);
            if (imageToken) {
                nodes.push(createNode("image", {
                    attrs: {
                        src: imageToken.src,
                        alt: imageToken.alt
                    }
                }));
                index = imageToken.nextIndex;
                continue;
            }
        }

        if (text.charAt(index) === "[") {
            var linkToken = parseMarkdownLink(text, index);
            if (linkToken) {
                var linkMarks = appendMark(activeMarks || [], {
                    type: "link",
                    attrs: { href: linkToken.href }
                });
                nodes = nodes.concat(parseMarkdownInline(linkToken.label, linkMarks));
                index = linkToken.nextIndex;
                continue;
            }
        }

        if (text.charAt(index) === "`") {
            var codeEnd = findMarkdownClosingDelimiter(text, "`", index + 1);
            if (codeEnd > index + 1) {
                nodes.push(createNode("text", {
                    text: text.substring(index + 1, codeEnd),
                    marks: appendMark(activeMarks || [], { type: "code" })
                }));
                index = codeEnd + 1;
                continue;
            }
        }

        var strongDelimiter = text.substr(index, 2);
        if (strongDelimiter === "**" || strongDelimiter === "__") {
            var strongEnd = findMarkdownClosingDelimiter(text, strongDelimiter, index + 2);
            if (strongEnd > index + 2) {
                nodes = nodes.concat(parseMarkdownInline(
                    text.substring(index + 2, strongEnd),
                    appendMark(activeMarks || [], { type: "bold" })
                ));
                index = strongEnd + 2;
                continue;
            }
        }

        var emphasisDelimiter = text.charAt(index);
        if (emphasisDelimiter === "*" || emphasisDelimiter === "_") {
            var emphasisEnd = findMarkdownClosingDelimiter(text, emphasisDelimiter, index + 1);
            if (emphasisEnd > index + 1) {
                nodes = nodes.concat(parseMarkdownInline(
                    text.substring(index + 1, emphasisEnd),
                    appendMark(activeMarks || [], { type: "italic" })
                ));
                index = emphasisEnd + 1;
                continue;
            }
        }

        var nextSpecialIndex = findNextMarkdownSpecial(text, index);
        var plainText = text.substring(index, nextSpecialIndex < 0 ? text.length : nextSpecialIndex);
        var textNode = createMarkdownTextNode(plainText, activeMarks);
        if (textNode) {
            nodes.push(textNode);
        }

        index = nextSpecialIndex < 0 ? text.length : nextSpecialIndex;
    }

    return mergeAdjacentTextNodes(nodes);
}

function parseMarkdownImage(value, startIndex) {
    var closeBracket = value.indexOf("]", startIndex + 2);
    if (closeBracket < 0 || value.charAt(closeBracket + 1) !== "(") {
        return null;
    }

    var closeParen = findMarkdownClosingDelimiter(value, ")", closeBracket + 2);
    if (closeParen < 0) {
        return null;
    }

    return {
        alt: value.substring(startIndex + 2, closeBracket),
        src: value.substring(closeBracket + 2, closeParen),
        nextIndex: closeParen + 1
    };
}

function parseMarkdownLink(value, startIndex) {
    var closeBracket = value.indexOf("]", startIndex + 1);
    if (closeBracket < 0 || value.charAt(closeBracket + 1) !== "(") {
        return null;
    }

    var closeParen = findMarkdownClosingDelimiter(value, ")", closeBracket + 2);
    if (closeParen < 0) {
        return null;
    }

    return {
        label: value.substring(startIndex + 1, closeBracket),
        href: value.substring(closeBracket + 2, closeParen),
        nextIndex: closeParen + 1
    };
}

function findNextMarkdownSpecial(value, startIndex) {
    for (var index = startIndex; index < value.length; index++) {
        var char = value.charAt(index);
        if (char === "\\" || char === "[" || char === "*" || char === "_" || char === "`") {
            return index;
        }

        if (char === "!" && value.charAt(index + 1) === "[") {
            return index;
        }
    }

    return -1;
}

function fromMarkdown(markdown) {
    var source = normalizeMarkdownSource(markdown);
    var content = parseMarkdownBlocksFromLines(source.split("\n"));
    var html = serializeBlockNodes(content);

    return {
        type: "doc",
        version: 2,
        format: "richtexteditor-json",
        html: html,
        text: getPlainTextFromHtml(html),
        content: content
    };
}

function escapeMarkdownText(value) {
    return String(value || "").replace(/([\\`*_\\[\\]])/g, "\\$1");
}

function escapeMarkdownCodeText(value) {
    return String(value || "").replace(/\\/g, "\\\\").replace(/`/g, "\\`");
}

function wrapMarkdownMark(content, mark) {
    if (!mark || !mark.type) {
        return content;
    }

    if (mark.type === "bold") {
        return "**" + content + "**";
    }

    if (mark.type === "italic") {
        return "*" + content + "*";
    }

    if (mark.type === "strike") {
        return "~~" + content + "~~";
    }

    if (mark.type === "code") {
        return "`" + escapeMarkdownCodeText(content) + "`";
    }

    if (mark.type === "link" && mark.attrs && mark.attrs.href) {
        return "[" + content + "](" + String(mark.attrs.href) + ")";
    }

    if (mark.type === "underline") {
        return "<u>" + content + "</u>";
    }

    if (mark.type === "subscript") {
        return "<sub>" + content + "</sub>";
    }

    if (mark.type === "superscript") {
        return "<sup>" + content + "</sup>";
    }

    if (mark.type === "textStyle") {
        var style = buildStyleString(mark.attrs || {});
        return style ? '<span style="' + escapeAttribute(style) + '">' + content + "</span>" : content;
    }

    return content;
}

function serializeMarkdownInlineNodes(nodes) {
    var markdown = "";

    for (var index = 0; index < nodes.length; index++) {
        var node = nodes[index];
        if (!node) {
            continue;
        }

        if (node.type === "text") {
            var hasCodeMark = false;
            var marks = node.marks || [];
            for (var markIndex = 0; markIndex < marks.length; markIndex++) {
                if (marks[markIndex].type === "code") {
                    hasCodeMark = true;
                    break;
                }
            }

            var content = hasCodeMark ? String(node.text || "") : escapeMarkdownText(node.text || "");
            for (var activeIndex = 0; activeIndex < marks.length; activeIndex++) {
                content = wrapMarkdownMark(content, marks[activeIndex]);
            }
            markdown += content;
            continue;
        }

        if (node.type === "hardBreak") {
            markdown += "  \n";
            continue;
        }

        if (node.type === "image") {
            var imageAttrs = node.attrs || {};
            markdown += "![" + String(imageAttrs.alt || "") + "](" + String(imageAttrs.src || "") + ")";
            continue;
        }

        if (node.type === "htmlInline") {
            markdown += node.html || "";
            continue;
        }

        if (isArray(node.content)) {
            markdown += serializeMarkdownInlineNodes(node.content);
            continue;
        }

        markdown += serializeNode(node, true);
    }

    return markdown;
}

function normalizeMarkdownListItemText(value) {
    return String(value || "").replace(/\s+/g, " ").replace(/^\s+|\s+$/g, "");
}

function indentMarkdown(value, indent) {
    var text = String(value || "");
    if (!text) {
        return "";
    }

    return indent + text.replace(/\n/g, "\n" + indent);
}

function serializeMarkdownListItem(blocks, prefix) {
    if (!blocks.length) {
        return prefix.trim();
    }

    var firstBlock = blocks[0];
    var firstLine = "";
    var trailingBlocks = [];

    if (firstBlock.type === "paragraph") {
        firstLine = prefix + normalizeMarkdownListItemText(serializeMarkdownInlineNodes(firstBlock.content || []));
        trailingBlocks = blocks.slice(1);
    } else {
        firstLine = prefix.trim();
        trailingBlocks = blocks.slice();
    }

    if (!trailingBlocks.length) {
        return firstLine;
    }

    var nested = serializeMarkdownBlocks(trailingBlocks);
    if (!nested) {
        return firstLine;
    }

    return firstLine + "\n" + indentMarkdown(nested, new Array(prefix.length + 1).join(" "));
}

function serializeMarkdownBlocks(nodes) {
    var parts = [];

    for (var index = 0; index < nodes.length; index++) {
        var node = nodes[index];
        if (!node) {
            continue;
        }

        if (node.type === "paragraph") {
            parts.push(serializeMarkdownInlineNodes(node.content || []));
            continue;
        }

        if (node.type === "heading") {
            var level = node.attrs && node.attrs.level ? parseInt(node.attrs.level, 10) : 1;
            if (!level || level < 1) {
                level = 1;
            }
            if (level > 6) {
                level = 6;
            }
            parts.push(new Array(level + 1).join("#") + " " + serializeMarkdownInlineNodes(node.content || []));
            continue;
        }

        if (node.type === "blockquote") {
            var quoted = serializeMarkdownBlocks(node.content || []);
            parts.push(quoted ? indentMarkdown(quoted, "> ") : ">");
            continue;
        }

        if (node.type === "codeBlock") {
            var language = node.attrs && (node.attrs.language || node.attrs.lang) ? String(node.attrs.language || node.attrs.lang) : "";
            parts.push("```" + language + "\n" + String(node.text || "") + "\n```");
            continue;
        }

        if (node.type === "bulletList" || node.type === "orderedList") {
            var listParts = [];
            var start = node.type === "orderedList" && node.attrs && node.attrs.start ? parseInt(node.attrs.start, 10) || 1 : 1;
            var listItems = node.content || [];
            for (var itemIndex = 0; itemIndex < listItems.length; itemIndex++) {
                var prefix = node.type === "orderedList" ? (start + itemIndex) + ". " : "- ";
                listParts.push(serializeMarkdownListItem(listItems[itemIndex].content || [], prefix));
            }
            parts.push(listParts.join("\n"));
            continue;
        }

        if (node.type === "horizontalRule") {
            parts.push("---");
            continue;
        }

        if (node.type === "image") {
            var attrs = node.attrs || {};
            parts.push("![" + String(attrs.alt || "") + "](" + String(attrs.src || "") + ")");
            continue;
        }

        if (node.type === "htmlBlock") {
            parts.push(node.html || "");
            continue;
        }

        parts.push(serializeNode(node, false));
    }

    return parts.join("\n\n").replace(/\n{3,}/g, "\n\n");
}

function toMarkdown(value) {
    var documentModel = createStructuredDocument(value);
    return serializeMarkdownBlocks(documentModel.content || []).replace(/^\s+|\s+$/g, "");
}

function renderHTML(value) {
    return serializeStructuredContent(value);
}

function installStructuredContentBridge() {
    var RichTextEditor = ensureRichTextEditorConstructor();

    RichTextEditor.prototype.getJSON = function () {
        return createStructuredContent(this);
    };

    RichTextEditor.prototype.setJSON = function (value) {
        this.setHTMLCode(serializeStructuredContent(value));
        return this;
    };

    RichTextEditor.fromMarkdown = fromMarkdown;
    RichTextEditor.renderHTML = renderHTML;
    RichTextEditor.toMarkdown = toMarkdown;
    RichTextEditor.validateStructuredContent = validateStructuredContent;

    return RichTextEditor;
}

function ensureStylesheet(href) {
    if (document.querySelector('link[data-rte-style="' + href + '"]')) {
        return;
    }

    var link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = href;
    link.setAttribute("data-rte-style", href);
    document.head.appendChild(link);
}

function loadScript(src) {
    return new Promise(function (resolve, reject) {
        var existing = document.querySelector('script[data-rte-script="' + src + '"]');
        if (existing) {
            if (existing.getAttribute("data-rte-loaded") === "true") {
                resolve(existing);
                return;
            }

            existing.addEventListener("load", function () {
                resolve(existing);
            }, { once: true });
            existing.addEventListener("error", function () {
                reject(new Error("Failed to load " + src));
            }, { once: true });
            return;
        }

        var script = document.createElement("script");
        script.src = src;
        script.async = false;
        script.setAttribute("data-rte-script", src);
        script.onload = function () {
            script.setAttribute("data-rte-loaded", "true");
            resolve(script);
        };
        script.onerror = function () {
            reject(new Error("Failed to load " + src));
        };

        document.head.appendChild(script);
    });
}

function trimTrailingSlash(value) {
    return value ? value.replace(/\/+$/, "") : "";
}

function loadRichTextEditorAssets(options) {
    ensureBrowserEnvironment();

    var basePath = trimTrailingSlash(options && options.basePath ? options.basePath : "/richtexteditor");

    if (window.RichTextEditor) {
        return Promise.resolve(installStructuredContentBridge());
    }

    if (!pendingLoadsByBasePath[basePath]) {
        ensureStylesheet(basePath + "/rte_theme_default.css");
        pendingLoadsByBasePath[basePath] = loadScript(basePath + "/rte.js")
            .then(function () {
                return loadScript(basePath + "/plugins/all_plugins.js");
            })
            .then(function () {
                return installStructuredContentBridge();
            });
    }

    return pendingLoadsByBasePath[basePath];
}

function createEditor(element, config) {
    installStructuredContentBridge();
    return new window.RichTextEditor(element, config || {});
}

function getEditorValue(editor, format) {
    return format === "json" ? editor.getJSON() : editor.getHTMLCode();
}

function setEditorValue(editor, value, format) {
    if (format === "json") {
        editor.setJSON(value);
        return;
    }

    editor.setHTMLCode(serializeStructuredContent(value));
}

var RichTextEditorStructuredContent = {
    createEditor: createEditor,
    createStructuredDocument: createStructuredDocument,
    createStructuredContent: createStructuredContent,
    ensureRichTextEditorConstructor: ensureRichTextEditorConstructor,
    fromMarkdown: fromMarkdown,
    getEditorValue: getEditorValue,
    installStructuredContentBridge: installStructuredContentBridge,
    loadRichTextEditorAssets: loadRichTextEditorAssets,
    normalizeStructuredContent: normalizeStructuredContent,
    parseStructuredContentHtml: parseStructuredContentHtml,
    renderHTML: renderHTML,
    serializeStructuredContent: serializeStructuredContent,
    setEditorValue: setEditorValue,
    toMarkdown: toMarkdown,
    validateStructuredContent: validateStructuredContent
};

if (typeof window !== "undefined") {
    window.RichTextEditorStructuredContent = RichTextEditorStructuredContent;

    if (window.RichTextEditor) {
        try {
            installStructuredContentBridge();
        }
        catch (ignore) {
        }
    }
}

if (typeof module !== "undefined" && module.exports) {
    module.exports = RichTextEditorStructuredContent;
}
