@cometchat/chat-uikit-react
Version:
Ready-to-use Chat UI Components for React
524 lines (517 loc) • 18.2 kB
JavaScript
;
var chunkNOCEWAE7_cjs = require('./chunk-NOCEWAE7.cjs');
var react = require('react');
var DOMPurify = require('dompurify');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var DOMPurify__default = /*#__PURE__*/_interopDefault(DOMPurify);
require('./index.css');
var defaultValue = {
subscribe: () => () => {
},
publish: () => {
}
};
var CometChatEventsContext = react.createContext(defaultValue);
CometChatEventsContext.displayName = "CometChatEventsContext";
function useCometChatEventsContext() {
return react.useContext(CometChatEventsContext);
}
function usePublishEvent() {
const { publish } = react.useContext(CometChatEventsContext);
return publish;
}
var ALLOWED_TAGS = [
"span",
"strong",
"em",
"b",
"i",
"u",
"s",
"code",
"pre",
"blockquote",
"ul",
"ol",
"li",
"a",
"br",
"p",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6"
];
var ALLOWED_ATTR = [
"class",
"data-uid",
"data-mention-type",
"data-self",
"href",
"target",
"rel",
"contenteditable",
"style"
];
if (typeof window !== "undefined") {
DOMPurify__default.default.addHook("afterSanitizeAttributes", (node) => {
if (node.tagName === "A") {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
});
}
function sanitizeHtml(html) {
if (!html || typeof html !== "string") return "";
if (html.trim() === "") return "";
return DOMPurify__default.default.sanitize(html, {
ALLOWED_TAGS,
ALLOWED_ATTR
});
}
function escapeUserHtml(text) {
if (!text || typeof text !== "string") return "";
const mentionPlaceholders = [];
const mentionRegex = /<@(?:uid|all):[^>]*>/g;
let escaped = text.replace(mentionRegex, (match) => {
const idx = mentionPlaceholders.length;
mentionPlaceholders.push(match);
return `__COMETCHAT_MENTION_${String(idx)}__`;
});
escaped = escaped.replace(/<[^>]*>/g, (match) => {
const inner = match.slice(1, -1).trim();
if (/^\/?u$/i.test(inner)) {
return match;
}
return match.replace(/</g, "<").replace(/>/g, ">");
});
escaped = escaped.replace(/&(?!(?:#\d+|#x[\da-fA-F]+|[a-zA-Z]\w*);)/g, "&");
escaped = escaped.replace(
/__COMETCHAT_MENTION_(\d+)__/g,
(_, idx) => mentionPlaceholders[parseInt(idx, 10)] ?? ""
);
return escaped;
}
function stripInvalidMentionFormats(text) {
if (!text || typeof text !== "string") return "";
return text.replace(/<@(?!uid:|all:)[^>]+>/g, "");
}
// src/formatters/CometChatMarkdownFormatter.ts
var CometChatMarkdownFormatter = class extends chunkNOCEWAE7_cjs.CometChatTextFormatter {
constructor() {
super(...arguments);
this.id = "markdown-formatter";
this.priority = 10;
}
getRegex() {
return /(\*\*|__|~~|`|>|\[.*?\]\(.*?\)|(?:\d+|[a-z]|[ivxlcdm]+)\.\s|[•-]\s)/g;
}
format(text) {
if (!text) {
this.originalText = "";
this.formattedText = "";
return "";
}
this.originalText = text;
let result = text;
result = this.formatCodeBlocks(result);
result = this.formatOutsideCodeBlocks(result, (s) => this.formatBlockquotes(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatBold(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatUnderline(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatItalic(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatStrikethrough(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatLinks(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatOrderedLists(s));
result = this.formatOutsideCodeBlocks(result, (s) => this.formatUnorderedLists(s));
result = this.formatInlineCode(result);
this.formattedText = result;
return this.formattedText;
}
// ─── Code Blocks ───────────────────────────────────────────────────────
formatCodeBlocks(text) {
return text.replace(/```\n?([\s\S]*?)\n?```/g, "<pre><code>$1</code></pre>");
}
/**
* Apply a formatting function only to text segments outside of code blocks.
* Unlike formatOutsideCode, this does NOT split on inline <code> tags or mention placeholders.
* Used for blockquote processing which runs before inline code conversion.
*/
formatOutsideCodeBlocks(text, formatter) {
const mentionTokens = [];
const mentionPattern = /(<@uid:[^>]+>|<@all:[^>]+>)/g;
const protectedText = text.replace(mentionPattern, (match) => {
const idx = mentionTokens.length;
mentionTokens.push(match);
return `\u200B\uFFFCMTKN${String(idx)}\uFFFC\u200B`;
});
const codeBlockPattern = /(<pre><code>[\s\S]*?<\/code><\/pre>)/g;
const parts = protectedText.split(codeBlockPattern);
let result = parts.map((part) => part.startsWith("<pre><code>") ? part : formatter(part)).join("");
result = result.replace(/\u200B\uFFFCMTKN(\d+)\uFFFC\u200B/g, (_, idx) => {
return mentionTokens[parseInt(idx, 10)] ?? "";
});
return result;
}
/**
* Apply a formatting function only to text segments outside of code blocks and inline code.
* Splits the text by <pre><code>...</code></pre> and <code>...</code> segments,
* applies the formatter only to non-code parts, then reassembles.
*/
formatOutsideCode(text, formatter) {
const mentionTokens = [];
const mentionPattern = /(<@uid:[^>]+>|<@all:[^>]+>)/g;
const textWithPlaceholders = text.replace(mentionPattern, (match) => {
const idx = mentionTokens.length;
mentionTokens.push(match);
return `\u200B\uFFFCMTKN${String(idx)}\uFFFC\u200B`;
});
const codePattern = /(<pre><code>[\s\S]*?<\/code><\/pre>|<code>[\s\S]*?<\/code>)/g;
const parts = textWithPlaceholders.split(codePattern);
let result = parts.map((part) => {
if (part.startsWith("<code>") || part.startsWith("<pre><code>")) {
return part;
}
return formatter(part);
}).join("");
result = result.replace(/\u200B\uFFFCMTKN(\d+)\uFFFC\u200B/g, (_, idx) => {
return mentionTokens[parseInt(idx, 10)] ?? "";
});
return result;
}
formatInlineCode(text) {
return text.replace(/`([^`]+)`/g, "<code>$1</code>");
}
// ─── Inline Formatting ─────────────────────────────────────────────────
formatBold(text) {
return text.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
}
formatItalic(text) {
return text.replace(/_([^_]+)_/g, "<i>$1</i>");
}
formatUnderline(text) {
let result = text.replace(/__([^_]+)__/g, "<u>$1</u>");
result = result.replace(/\+\+([^+]+)\+\+/g, "<u>$1</u>");
return result;
}
formatStrikethrough(text) {
return text.replace(/~~([^~]+)~~/g, "<s>$1</s>");
}
formatLinks(text) {
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, rawUrl) => {
const href = this.normalizeLinkUrl(rawUrl);
if (!href) return label;
return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="cometchat-link">${label}</a>`;
});
}
normalizeLinkUrl(rawUrl) {
const url = rawUrl.trim();
if (!url) return "";
const compact = Array.from(url).filter((ch) => ch.charCodeAt(0) > 32).join("").toLowerCase();
if (/^(?:javascript|data|vbscript|file):/.test(compact)) return "";
let href = url;
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(href);
const isAnchorOrRelative = /^[#/?]/.test(href) || href.startsWith("./") || href.startsWith("../");
if (!hasScheme && !isAnchorOrRelative) {
href = `https://${href}`;
}
return href.replace(/"/g, """);
}
// ─── Block Formatting ──────────────────────────────────────────────────
formatBlockquotes(text) {
const lines = text.split("\n");
const result = [];
let blockquoteGroup = [];
const flushGroup = () => {
if (blockquoteGroup.length === 0) return;
let innerContent = blockquoteGroup.join("\n");
innerContent = this.formatOrderedLists(innerContent);
innerContent = this.formatUnorderedLists(innerContent);
result.push("<blockquote>" + innerContent + "</blockquote>");
blockquoteGroup = [];
};
for (const line of lines) {
if (/^>\s?/.test(line) || /^>\s?/.test(line)) {
blockquoteGroup.push(line.replace(/^(?:>|>)\s?/, ""));
} else {
flushGroup();
result.push(line);
}
}
flushGroup();
return result.join("\n");
}
formatOrderedLists(text) {
const lines = text.split("\n");
const result = [];
const depthStack = [];
const isListHtml = [];
const listStyleForDepth = (d) => {
if (d === 0) return "decimal";
if (d === 1) return "lower-alpha";
return "lower-roman";
};
const closeToDepth = (targetDepth) => {
while (depthStack.length > targetDepth) {
depthStack.pop();
result.push("</ol></li>");
isListHtml.push(true);
}
};
const orderedListRegex = /^( *)(?:(\d+)\.|([a-z])\.|([ivxlcdm]+)\.)\s+(.+)$/;
const getDepthFromMarker = (leadingSpaces, decimal, alpha, roman) => {
const indentDepth = Math.floor(leadingSpaces / 4);
if (decimal) return indentDepth;
if (roman && (indentDepth >= 2 || depthStack.length > 0 && (depthStack[depthStack.length - 1] ?? 0) >= 1)) {
return Math.max(indentDepth, 2);
}
if (alpha) return Math.max(indentDepth, 1);
return indentDepth;
};
for (const line of lines) {
const match = orderedListRegex.exec(line);
if (match) {
const leadingSpaces = match[1]?.length ?? 0;
const decimal = match[2];
const alpha = match[3];
const roman = match[4];
const content = match[5];
const currentDepth = getDepthFromMarker(leadingSpaces, decimal, alpha, roman);
if (depthStack.length === 0) {
result.push(
`<ol style="list-style-type: ${listStyleForDepth(0)}; list-style-position: inside; padding: 0; margin: 0;">`
);
isListHtml.push(true);
depthStack.push(0);
}
if (currentDepth > depthStack.length - 1) {
const lastLine = result[result.length - 1];
if (lastLine?.endsWith("</li>")) {
result[result.length - 1] = lastLine.slice(0, -5);
}
while (depthStack.length - 1 < currentDepth) {
const d = depthStack.length;
result.push(
`<ol style="list-style-type: ${listStyleForDepth(d)}; list-style-position: inside; padding: 0; margin: 0;">`
);
isListHtml.push(true);
depthStack.push(d);
}
} else if (currentDepth < depthStack.length - 1) {
closeToDepth(currentDepth + 1);
}
result.push(
`<li style="display: list-item; padding-left: 1.5em; text-indent: -1.5em; margin: 0;">${content ?? ""}</li>`
);
isListHtml.push(true);
} else {
if (depthStack.length > 0) {
closeToDepth(0);
result.push("</ol>");
isListHtml.push(true);
depthStack.length = 0;
}
result.push(line);
isListHtml.push(false);
}
}
if (depthStack.length > 0) {
closeToDepth(0);
result.push("</ol>");
isListHtml.push(true);
}
let output = "";
for (let i = 0; i < result.length; i++) {
if (i > 0 && !(isListHtml[i] && isListHtml[i - 1])) {
output += "\n";
}
output += result[i] ?? "";
}
return output;
}
formatUnorderedLists(text) {
const lines = text.split("\n");
const result = [];
const depthStack = [];
const isListHtml = [];
const listStyleForDepth = (d) => {
if (d === 0) return "disc";
if (d === 1) return "circle";
return "square";
};
const closeToDepth = (targetDepth) => {
while (depthStack.length > targetDepth) {
depthStack.pop();
result.push("</ul></li>");
isListHtml.push(true);
}
};
for (const line of lines) {
const match = /^( *)[•-]\s+(.+)$/.exec(line);
if (match) {
const leadingSpaces = match[1]?.length ?? 0;
const content = match[2];
const currentDepth = Math.floor(leadingSpaces / 4);
if (depthStack.length === 0) {
result.push(
`<ul style="list-style-type: ${listStyleForDepth(0)}; list-style-position: inside; padding: 0; margin: 0;">`
);
isListHtml.push(true);
depthStack.push(0);
}
if (currentDepth > depthStack.length - 1) {
const lastLine = result[result.length - 1];
if (lastLine?.endsWith("</li>")) {
result[result.length - 1] = lastLine.slice(0, -5);
}
while (depthStack.length - 1 < currentDepth) {
const d = depthStack.length;
result.push(
`<ul style="list-style-type: ${listStyleForDepth(d)}; list-style-position: inside; padding: 0; margin: 0;">`
);
isListHtml.push(true);
depthStack.push(d);
}
} else if (currentDepth < depthStack.length - 1) {
closeToDepth(currentDepth + 1);
}
result.push(
`<li style="display: list-item; padding-left: 1.5em; text-indent: -1.5em; margin: 0;">${content ?? ""}</li>`
);
isListHtml.push(true);
} else {
if (depthStack.length > 0) {
closeToDepth(0);
result.push("</ul>");
isListHtml.push(true);
depthStack.length = 0;
}
result.push(line);
isListHtml.push(false);
}
}
if (depthStack.length > 0) {
closeToDepth(0);
result.push("</ul>");
isListHtml.push(true);
}
let output = "";
for (let i = 0; i < result.length; i++) {
if (i > 0 && !(isListHtml[i] && isListHtml[i - 1])) {
output += "\n";
}
output += result[i] ?? "";
}
return output;
}
/**
* Strip markdown for conversation subtitle display.
* Converts markdown to simple HTML (bold, italic, etc.) without block-level elements.
*/
stripMarkdownForConversation(text) {
let result = text;
result = result.replace(/\u200B/g, "");
result = escapeUserHtml(result);
result = result.replace(/```\n?([\s\S]*?)\n?```/g, "$1");
result = result.replace(/`([\s\S]+?)`/g, "<code>$1</code>");
result = this.normalizeOrderedListMarkers(result);
const lines = result.split("\n");
const processedLines = lines.map((line) => {
let r = line;
r = r.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>");
r = r.replace(/__([^_]+)__/g, "<u>$1</u>");
r = r.replace(/\+\+([^+]+)\+\+/g, "<u>$1</u>");
r = r.replace(/_([^_]+)_/g, "<i>$1</i>");
r = r.replace(/~~([^~]+)~~/g, "<s>$1</s>");
r = r.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
r = r.replace(/^(?:>|>)\s?/, "");
return r;
});
return processedLines.join("\n");
}
/**
* Normalize ordered list markers so indented items show correct depth markers.
* Handles backward compatibility: old messages store all levels as "1. 2. 3."
* regardless of nesting. This converts them based on indentation:
* - depth 0 (no indent): keep numeric (1. 2. 3.)
* - depth 1 (4 spaces): convert to alpha (a. b. c.)
* - depth 2 (8 spaces): convert to roman (i. ii. iii.)
*/
normalizeOrderedListMarkers(text) {
const lines = text.split("\n");
const depthCounters = [0, 0, 0];
let prevDepth = -1;
const numberToAlpha = (n) => {
let result = "";
let num = n;
while (num > 0) {
num--;
result = String.fromCharCode(97 + num % 26) + result;
num = Math.floor(num / 26);
}
return result;
};
const numberToRoman = (n) => {
const values = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const symbols = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
let result = "";
let num = n;
for (let i = 0; i < values.length; i++) {
while (num >= (values[i] ?? 0)) {
result += symbols[i] ?? "";
num -= values[i] ?? 0;
}
}
return result;
};
const processedLines = lines.map((line) => {
const match = /^( *)(?:(\d+)\.|([a-z])\.|([ivxlcdm]+)\.)\s+(.+)$/.exec(line);
if (!match) {
prevDepth = -1;
depthCounters[0] = 0;
depthCounters[1] = 0;
depthCounters[2] = 0;
return line;
}
const leadingSpaces = match[1]?.length ?? 0;
const content = match[5] ?? "";
const depth = Math.min(Math.floor(leadingSpaces / 4), 2);
if (match[3] || match[4]) {
prevDepth = depth;
return line;
}
if (depth === 0) {
prevDepth = depth;
return line;
}
if (depth <= prevDepth) {
for (let d = depth + 1; d < depthCounters.length; d++) {
depthCounters[d] = 0;
}
}
if (depth > prevDepth) {
depthCounters[depth] = 0;
}
depthCounters[depth] = (depthCounters[depth] ?? 0) + 1;
prevDepth = depth;
const indent = " ".repeat(depth);
const count = depthCounters[depth] ?? 1;
if (depth === 1) {
return `${indent}${numberToAlpha(count)}. ${content}`;
}
if (depth >= 2) {
return `${indent}${numberToRoman(count)}. ${content}`;
}
return line;
});
return processedLines.join("\n");
}
};
exports.CometChatEventsContext = CometChatEventsContext;
exports.CometChatMarkdownFormatter = CometChatMarkdownFormatter;
exports.escapeUserHtml = escapeUserHtml;
exports.sanitizeHtml = sanitizeHtml;
exports.stripInvalidMentionFormats = stripInvalidMentionFormats;
exports.useCometChatEventsContext = useCometChatEventsContext;
exports.usePublishEvent = usePublishEvent;