UNPKG

@rcb-plugins/html-renderer

Version:

A simple plugin for rendering html messages in React ChatBotify.

728 lines (725 loc) 20.7 kB
import A, { useEffect as X } from "react"; import { useFlow as Q, useMessages as Z, useSettings as ee, useChatHistory as te, useOnRcbEvent as L, RcbEvent as P } from "react-chatbotify"; import { jsx as W } from "react/jsx-runtime"; const d = 1, G = 2, U = 4, b = 8, H = 16, R = 32, N = 64, D = { a: { content: d | b, self: !1, type: d | b | R | N }, address: { invalid: ["h1", "h2", "h3", "h4", "h5", "h6", "address", "article", "aside", "section", "div", "header", "footer"], self: !1 }, audio: { children: ["track", "source"] }, br: { type: d | b, void: !0 }, body: { content: d | G | U | b | H | R | N }, button: { content: b, type: d | b | R | N }, caption: { content: d, parent: ["table"] }, col: { parent: ["colgroup"], void: !0 }, colgroup: { children: ["col"], parent: ["table"] }, details: { children: ["summary"], type: d | R | N }, dd: { content: d, parent: ["dl"] }, dl: { children: ["dt", "dd"], type: d }, dt: { content: d, invalid: ["footer", "header"], parent: ["dl"] }, figcaption: { content: d, parent: ["figure"] }, footer: { invalid: ["footer", "header"] }, header: { invalid: ["footer", "header"] }, hr: { type: d, void: !0 }, img: { void: !0 }, li: { content: d, parent: ["ul", "ol", "menu"] }, main: { self: !1 }, ol: { children: ["li"], type: d }, picture: { children: ["source", "img"], type: d | b | H }, rb: { parent: ["ruby", "rtc"] }, rp: { parent: ["ruby", "rtc"] }, rt: { content: b, parent: ["ruby", "rtc"] }, rtc: { content: b, parent: ["ruby"] }, ruby: { children: ["rb", "rp", "rt", "rtc"] }, source: { parent: ["audio", "video", "picture"], void: !0 }, summary: { content: b, parent: ["details"] }, table: { children: ["caption", "colgroup", "thead", "tbody", "tfoot", "tr"], type: d }, tbody: { parent: ["table"], children: ["tr"] }, td: { content: d, parent: ["tr"] }, tfoot: { parent: ["table"], children: ["tr"] }, th: { content: d, parent: ["tr"] }, thead: { parent: ["table"], children: ["tr"] }, tr: { parent: ["table", "tbody", "thead", "tfoot"], children: ["th", "td"] }, track: { parent: ["audio", "video"], void: !0 }, ul: { children: ["li"], type: d }, video: { children: ["track", "source"] }, wbr: { type: d | b, void: !0 } }; function S(a) { return (e) => { D[e] = { ...a, ...D[e] }; }; } ["address", "main", "div", "figure", "p", "pre"].forEach(S({ content: d, type: d | N })); ["abbr", "b", "bdi", "bdo", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "ruby", "samp", "strong", "sub", "sup", "time", "u", "var"].forEach(S({ content: b, type: d | b | N })); ["p", "pre"].forEach(S({ content: b, type: d | N })); ["s", "small", "span", "del", "ins"].forEach(S({ content: b, type: d | b })); ["article", "aside", "footer", "header", "nav", "section", "blockquote"].forEach(S({ content: d, type: d | G | N })); ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(S({ content: b, type: d | U | N })); ["audio", "canvas", "iframe", "img", "video"].forEach(S({ type: d | b | H | N })); const F = Object.freeze(D), re = ["applet", "base", "body", "command", "embed", "frame", "frameset", "head", "html", "link", "meta", "noscript", "object", "script", "style", "title"], ne = Object.keys(F).filter((a) => a !== "canvas" && a !== "iframe"), h = 1, se = 2, O = 3, x = 4, q = 5, j = Object.freeze({ alt: h, cite: h, class: h, colspan: O, controls: x, datetime: h, default: x, disabled: x, dir: h, height: h, href: h, id: h, kind: h, label: h, lang: h, loading: h, loop: x, media: h, muted: x, poster: h, rel: h, role: h, rowspan: O, scope: h, sizes: h, span: O, start: O, style: q, src: h, srclang: h, srcset: h, tabindex: h, target: h, title: h, type: h, width: h }), ae = Object.freeze({ class: "className", colspan: "colSpan", datetime: "dateTime", rowspan: "rowSpan", srclang: "srcLang", srcset: "srcSet", tabindex: "tabIndex" }); function k() { return k = Object.assign || function(a) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) Object.prototype.hasOwnProperty.call(t, r) && (a[r] = t[r]); } return a; }, k.apply(this, arguments); } function z({ attributes: a = {}, className: e, children: t = null, selfClose: r = !1, tagName: n }) { const s = n; return r ? /* @__PURE__ */ A.createElement(s, k({ className: e }, a)) : /* @__PURE__ */ A.createElement(s, k({ className: e }, a), t); } class oe { /** * Filter and clean an HTML attribute value. */ attribute(e, t) { return t; } /** * Filter and clean an HTML node. */ node(e, t) { return t; } } function ie(a) { return a && a.__esModule && Object.prototype.hasOwnProperty.call(a, "default") ? a.default : a; } /*! * escape-html * Copyright(c) 2012-2013 TJ Holowaychuk * Copyright(c) 2015 Andreas Lubbe * Copyright(c) 2015 Tiancheng "Timothy" Gu * MIT Licensed */ var C, V; function ce() { if (V) return C; V = 1; var a = /["'&<>]/; C = e; function e(t) { var r = "" + t, n = a.exec(r); if (!n) return r; var s, c = "", u = 0, o = 0; for (u = n.index; u < r.length; u++) { switch (r.charCodeAt(u)) { case 34: s = "&quot;"; break; case 38: s = "&amp;"; break; case 39: s = "&#39;"; break; case 60: s = "&lt;"; break; case 62: s = "&gt;"; break; default: continue; } o !== u && (c += r.substring(o, u)), o = u + 1, c += s; } return o !== u ? c + r.substring(o, u) : c; } return C; } var le = ce(); const ue = /* @__PURE__ */ ie(le); function w(a, e, t) { return e in a ? Object.defineProperty(a, e, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : a[e] = t, a; } const de = /(url|image|image-set)\(/i; class pe extends oe { attribute(e, t) { return e === "style" && Object.keys(t).forEach((r) => { String(t[r]).match(de) && delete t[r]; }), t; } } const $ = 1, fe = 3, he = /^<(!doctype|(html|head|body)(\s|>))/i, me = /^(aria-|data-|\w+:)/iu, ge = /{{{(\w+)\/?}}}/; function be() { if (!(typeof window > "u" || typeof document > "u")) return document.implementation.createHTMLDocument("Interweave"); } class K { constructor(e, t = {}, r = [], n = []) { var s; if (w(this, "allowed", void 0), w(this, "banned", void 0), w(this, "blocked", void 0), w(this, "container", void 0), w(this, "content", []), w(this, "props", void 0), w(this, "matchers", void 0), w(this, "filters", void 0), w(this, "keyIndex", void 0), process.env.NODE_ENV !== "production" && e && typeof e != "string") throw new TypeError("Interweave parser requires a valid string."); this.props = t, this.matchers = r, this.filters = [...n, new pe()], this.keyIndex = -1, this.container = this.createContainer(e || ""), this.allowed = new Set((s = t.allowList) !== null && s !== void 0 ? s : ne), this.banned = new Set(re), this.blocked = new Set(t.blockList); } /** * Loop through and apply all registered attribute filters. */ applyAttributeFilters(e, t) { return this.filters.reduce((r, n) => r !== null && typeof n.attribute == "function" ? n.attribute(e, r) : r, t); } /** * Loop through and apply all registered node filters. */ applyNodeFilters(e, t) { return this.filters.reduce((r, n) => r !== null && typeof n.node == "function" ? n.node(e, r) : r, t); } /** * Loop through and apply all registered matchers to the string. * If a match is found, create a React element, and build a new array. * This array allows React to interpolate and render accordingly. */ applyMatchers(e, t) { const r = {}, { props: n } = this; let s = e, c = 0, u = null; return this.matchers.forEach((o) => { const m = o.asTag().toLowerCase(), f = this.getTagConfig(m); if (n[o.inverseName] || !this.isTagAllowed(m) || !this.canRenderChild(t, f)) return; let i = ""; for (; s && (u = o.match(s)); ) { const { index: l, length: p, match: g, valid: E, void: T, ...y } = u, _ = o.propName + String(c); l > 0 && (i += s.slice(0, l)), E ? (i += T ? `{{{${_}/}}}` : `{{{${_}}}}${g}{{{/${_}}}}`, this.keyIndex += 1, c += 1, r[_] = { children: g, matcher: o, props: { ...n, ...y, key: this.keyIndex } }) : i += g, o.greedy ? (s = i + s.slice(l + p), i = "") : s = s.slice(l + (p || g.length)); } o.greedy || (s = i + s); }), c === 0 ? e : this.replaceTokens(s, r); } /** * Determine whether the child can be rendered within the parent. */ canRenderChild(e, t) { return !e.tagName || !t.tagName || e.void ? !1 : e.children.length > 0 ? e.children.includes(t.tagName) : e.invalid.length > 0 && e.invalid.includes(t.tagName) ? !1 : t.parent.length > 0 ? t.parent.includes(e.tagName) : !e.self && e.tagName === t.tagName ? !1 : !!(e && e.content & t.type); } /** * Convert line breaks in a string to HTML `<br/>` tags. * If the string contains HTML, we should not convert anything, * as line breaks should be handled by `<br/>`s in the markup itself. */ convertLineBreaks(e) { const { noHtml: t, disableLineBreaks: r } = this.props; if (t || r || e.match(/<((?:\/[ a-z]+)|(?:[ a-z]+\/))>/gi)) return e; let n = e.replace(/\r\n/g, ` `); return n = n.replace(/\n{3,}/g, ` `), n = n.replace(/\n/g, "<br/>"), n; } /** * Create a detached HTML document that allows for easy HTML * parsing while not triggering scripts or loading external * resources. */ createContainer(e) { var t; const n = (typeof global < "u" && global.INTERWEAVE_SSR_POLYFILL || be)(); if (!n) return; const s = (t = this.props.containerTagName) !== null && t !== void 0 ? t : "body", c = s === "body" || s === "fragment" ? n.body : n.createElement(s); if (e.match(he)) { if (process.env.NODE_ENV !== "production") throw new Error("HTML documents as Interweave content are not supported."); } else c.innerHTML = this.convertLineBreaks(this.props.escapeHtml ? ue(e) : e); return c; } /** * Convert an elements attribute map to an object map. * Returns null if no attributes are defined. */ extractAttributes(e) { const { allowAttributes: t } = this.props, r = {}; let n = 0; return e.nodeType !== $ || !e.attributes || ([...e.attributes].forEach((s) => { const { name: c, value: u } = s, o = c.toLowerCase(), m = j[o] || j[c]; if (!this.isSafe(e) || !o.match(me) && (!t && (!m || m === se) || o.startsWith("on") || u.replace(/(\s|\0|&#x0([9AD]);)/, "").match(/(javascript|vbscript|livescript|xss):/i))) return; let f = o === "style" ? this.extractStyleAttribute(e) : u; m === x ? f = !0 : m === O ? f = Number.parseFloat(String(f)) : m !== q && (f = String(f)), r[ae[o] || o] = this.applyAttributeFilters(o, f), n += 1; }), n === 0) ? null : r; } /** * Extract the style attribute as an object and remove values that allow for attack vectors. */ extractStyleAttribute(e) { const t = {}; return Array.from(e.style).forEach((r) => { const n = e.style[r]; (typeof n == "string" || typeof n == "number") && (t[r.replace(/-([a-z])/g, (s, c) => String(c).toUpperCase())] = n); }), t; } /** * Return configuration for a specific tag. */ getTagConfig(e) { const t = { children: [], content: 0, invalid: [], parent: [], self: !0, tagName: "", type: 0, void: !1 }; return F[e] ? { ...t, ...F[e], tagName: e } : t; } /** * Verify that a node is safe from XSS and injection attacks. */ isSafe(e) { if (typeof HTMLAnchorElement < "u" && e instanceof HTMLAnchorElement) { const t = e.getAttribute("href"); if (t != null && t.startsWith("#")) return !0; const r = e.protocol.toLowerCase(); return r === ":" || r === "http:" || r === "https:" || r === "mailto:" || r === "tel:"; } return !0; } /** * Verify that an HTML tag is allowed to render. */ isTagAllowed(e) { return this.banned.has(e) || this.blocked.has(e) ? !1 : this.props.allowElements || this.allowed.has(e); } /** * Parse the markup by injecting it into a detached document, * while looping over all child nodes and generating an * array to interpolate into JSX. */ parse() { return this.container ? this.parseNode(this.container, this.getTagConfig(this.container.nodeName.toLowerCase())) : []; } /** * Loop over the nodes children and generate a * list of text nodes and React elements. */ parseNode(e, t) { const { noHtml: r, noHtmlExceptMatchers: n, allowElements: s, transform: c, transformOnlyAllowList: u } = this.props; let o = [], m = ""; return [...e.childNodes].forEach((f) => { if (f.nodeType === $) { const l = f.nodeName.toLowerCase(), p = this.getTagConfig(l); m && (o.push(m), m = ""); const g = this.applyNodeFilters(l, f); if (!g) return; let E; if (c && !(u && !this.isTagAllowed(l))) { this.keyIndex += 1; const T = this.keyIndex; E = this.parseNode(g, p); const y = c(g, E, p); if (y === null) return; if (typeof y < "u") { o.push(/* @__PURE__ */ A.cloneElement(y, { key: T })); return; } this.keyIndex = T - 1; } if (this.banned.has(l)) return; if (!(r || n && l !== "br") && this.isTagAllowed(l) && (s || this.canRenderChild(t, p))) { var i; this.keyIndex += 1; const T = this.extractAttributes(g), y = { tagName: l }; T && (y.attributes = T), p.void && (y.selfClose = p.void), o.push(/* @__PURE__ */ A.createElement(z, { ...y, key: this.keyIndex }, (i = E) !== null && i !== void 0 ? i : this.parseNode(g, p))); } else o = [...o, ...this.parseNode(g, p.tagName ? p : t)]; } else if (f.nodeType === fe) { const l = r && !n ? f.textContent : ( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.applyMatchers(f.textContent || "", t) ); Array.isArray(l) ? o = [...o, ...l] : m += l; } }), m && o.push(m), o; } /** * Deconstruct the string into an array, by replacing custom tokens with React elements, * so that React can render it correctly. */ replaceTokens(e, t) { if (!e.includes("{{{")) return e; const r = []; let n = e, s = null; for (; s = n.match(ge); ) { const [c, u] = s, o = s.index, m = c.includes("/"); if (process.env.NODE_ENV !== "production" && !t[u]) throw new Error(`Token "${u}" found but no matching element to replace with.`); o > 0 && (r.push(n.slice(0, o)), n = n.slice(o)); const { children: f, matcher: i, props: l } = t[u]; let p; if (m) p = c.length, r.push(i.createElement(f, l)); else { const g = n.match(new RegExp(`{{{/${u}}}}`)); if (process.env.NODE_ENV !== "production" && !g) throw new Error(`Closing token missing for interpolated element "${u}".`); p = g.index + g[0].length, r.push(i.createElement(this.replaceTokens(n.slice(c.length, g.index), t), l)); } n = n.slice(p); } return n.length > 0 && r.push(n), r.length === 0 ? "" : r.length === 1 && typeof r[0] == "string" ? r[0] : r; } } function ye(a) { var e; const { attributes: t, className: r, containerTagName: n, content: s, emptyContent: c, parsedContent: u, tagName: o, noWrap: m } = a, f = (e = n ?? o) !== null && e !== void 0 ? e : "span", i = f === "fragment" ? !0 : m; let l; if (u) l = u; else { const p = new K(s ?? "", a).parse(); p.length > 0 && (l = p); } return l || (l = c), i ? /* @__PURE__ */ A.createElement(A.Fragment, null, l) : /* @__PURE__ */ A.createElement(z, { attributes: t, className: r, tagName: f }, l); } function Ee(a) { const { attributes: e, className: t, content: r = "", disableFilters: n = !1, disableMatchers: s = !1, emptyContent: c = null, filters: u = [], matchers: o = [], onAfterParse: m = null, onBeforeParse: f = null, tagName: i = "span", noWrap: l = !1, ...p } = a, g = s ? [] : o, E = n ? [] : u, T = f ? [f] : [], y = m ? [m] : []; g.forEach((v) => { v.onBeforeParse && T.push(v.onBeforeParse.bind(v)), v.onAfterParse && y.push(v.onAfterParse.bind(v)); }); const _ = T.reduce((v, M) => { const I = M(v, a); if (process.env.NODE_ENV !== "production" && typeof I != "string") throw new TypeError("Interweave `onBeforeParse` must return a valid HTML string."); return I; }, r ?? ""), J = new K(_, p, g, E), B = y.reduce((v, M) => { const I = M(v, a); if (process.env.NODE_ENV !== "production" && !Array.isArray(I)) throw new TypeError("Interweave `onAfterParse` must return an array of strings and React elements."); return I; }, J.parse()); return /* @__PURE__ */ A.createElement(ye, { attributes: e, className: t, containerTagName: a.containerTagName, emptyContent: c, noWrap: l, parsedContent: B.length === 0 ? void 0 : B, tagName: i }); } const Te = ({ children: a }) => /* @__PURE__ */ W("div", { style: { whiteSpace: "normal" }, children: /* @__PURE__ */ W(Ee, { content: typeof a == "string" ? a : "" }) }), ve = { autoConfig: !0 }, Y = (a, e, t) => { var n; if (!a.detail.currPath) return !1; const r = e[a.detail.currPath]; return r ? ((n = r.renderHtml) == null ? void 0 : n.map((s) => s.toUpperCase()).includes(t)) ?? !1 : !1; }, Ne = (a) => { const e = []; let t = "", r = !1; for (let n = 0; n < a.length; n++) { const s = a[n]; s === "<" ? r ? (e.push(t), t = s) : (r = !0, t = s) : s === ">" ? (t += s, e.push(t), t = "", r = !1) : r ? t += s : e.push(s); } return t !== "" && e.push(t), e; }, we = (a) => typeof window.DOMParser < "u" ? new DOMParser().parseFromString(a, "text/html").body.textContent || "" : a.replace(/<\/?[^>]+(>|$)/g, ""), Ae = (a) => { const { getFlow: e } = Q(), { messages: t, replaceMessages: r } = Z(), { settings: n } = ee(), { hasChatHistoryLoaded: s } = te(), c = { ...a, ...ve }, u = c.htmlComponent ? c.htmlComponent : Te; X(() => { var i, l; if (s) { const p = [...t]; for (let g = 0; g < p.length && g < (((i = n.chatHistory) == null ? void 0 : i.maxEntries) ?? 30); g++) { const E = p[g]; (l = E.tags) != null && l.includes("rcb-html-renderer-plugin:parsed") && (E.contentWrapper = u); } r(p); } }, [s]); const o = async (i) => { var p; const l = (p = i.data.message) == null ? void 0 : p.sender.toUpperCase(); typeof i.data.message.content == "string" && Y(i, e(), l) && (i.type === "rcb-start-simulate-stream-message" && (i.data.simulateStreamChunker = Ne), i.data.message.contentWrapper = u, i.data.message.tags || (i.data.message.tags = []), i.data.message.tags.push("rcb-html-renderer-plugin:parsed")); }, m = async (i) => { Y(i, e(), "BOT") && (i.data.textToRead = we(i.data.textToRead)); }; L(P.PRE_INJECT_MESSAGE, o), L(P.CHUNK_STREAM_MESSAGE, o), L(P.START_STREAM_MESSAGE, o), L(P.START_SIMULATE_STREAM_MESSAGE, o), L(P.START_SPEAK_AUDIO, m); const f = { name: "@rcb-plugins/html-renderer" }; return c != null && c.autoConfig && (f.settings = { event: { rcbPreInjectMessage: !0, rcbChunkStreamMessage: !0, rcbStartSimulateStreamMessage: !0, rcbStartStreamMessage: !0, rcbStartSpeakAudio: !0 } }), f; }, Ie = (a) => () => Ae(a); export { Ie as default };