@rcb-plugins/html-renderer
Version:
A simple plugin for rendering html messages in React ChatBotify.
728 lines (725 loc) • 20.7 kB
JavaScript
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 = """;
break;
case 38:
s = "&";
break;
case 39:
s = "'";
break;
case 60:
s = "<";
break;
case 62:
s = ">";
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|�([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
};