@newfold/wp-module-ai-chat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/package.json +51 -0
- package/src/components/chat/ChatHeader.jsx +63 -0
- package/src/components/chat/ChatHistoryDropdown.jsx +182 -0
- package/src/components/chat/ChatHistoryList.jsx +257 -0
- package/src/components/chat/ChatInput.jsx +157 -0
- package/src/components/chat/ChatMessage.jsx +157 -0
- package/src/components/chat/ChatMessages.jsx +137 -0
- package/src/components/chat/WelcomeScreen.jsx +115 -0
- package/src/components/icons/CloseIcon.jsx +27 -0
- package/src/components/icons/SparklesOutlineIcon.jsx +30 -0
- package/src/components/icons/index.js +5 -0
- package/src/components/ui/AILogo.jsx +47 -0
- package/src/components/ui/BluBetaHeading.jsx +18 -0
- package/src/components/ui/ErrorAlert.jsx +30 -0
- package/src/components/ui/HeaderBar.jsx +34 -0
- package/src/components/ui/SuggestionButton.jsx +28 -0
- package/src/components/ui/ToolExecutionList.jsx +264 -0
- package/src/components/ui/TypingIndicator.jsx +268 -0
- package/src/constants/nfdAgents/input.js +13 -0
- package/src/constants/nfdAgents/storageKeys.js +102 -0
- package/src/constants/nfdAgents/typingStatus.js +40 -0
- package/src/constants/nfdAgents/websocket.js +44 -0
- package/src/hooks/useAIChat.js +432 -0
- package/src/hooks/useNfdAgentsWebSocket.js +964 -0
- package/src/index.js +66 -0
- package/src/services/mcpClient.js +433 -0
- package/src/services/openaiClient.js +416 -0
- package/src/styles/_branding.scss +151 -0
- package/src/styles/_history.scss +180 -0
- package/src/styles/_input.scss +170 -0
- package/src/styles/_messages.scss +272 -0
- package/src/styles/_mixins.scss +21 -0
- package/src/styles/_typing-indicator.scss +162 -0
- package/src/styles/_ui.scss +173 -0
- package/src/styles/_vars.scss +103 -0
- package/src/styles/_welcome.scss +81 -0
- package/src/styles/app.scss +10 -0
- package/src/utils/helpers.js +75 -0
- package/src/utils/markdownParser.js +319 -0
- package/src/utils/nfdAgents/archiveConversation.js +82 -0
- package/src/utils/nfdAgents/chatHistoryList.js +130 -0
- package/src/utils/nfdAgents/configFetcher.js +137 -0
- package/src/utils/nfdAgents/greeting.js +55 -0
- package/src/utils/nfdAgents/jwtUtils.js +59 -0
- package/src/utils/nfdAgents/messageHandler.js +328 -0
- package/src/utils/nfdAgents/storage.js +112 -0
- package/src/utils/nfdAgents/typingIndicatorToolDisplay.js +180 -0
- package/src/utils/nfdAgents/url.js +101 -0
- package/src/utils/restApi.js +87 -0
- package/src/utils/sanitizeHtml.js +94 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool/ability display helpers for the TypingIndicator tool execution list.
|
|
3
|
+
* Maps tool and ability names to user-facing title and description.
|
|
4
|
+
*
|
|
5
|
+
* @package
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { __ } from "@wordpress/i18n";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get ability details for display in the typing indicator.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} abilityName The ability name (e.g. from tool arguments).
|
|
14
|
+
* @return {Object} { title, description }
|
|
15
|
+
*/
|
|
16
|
+
export function getAbilityDetails(abilityName) {
|
|
17
|
+
const abilityMap = {
|
|
18
|
+
"nfd-agents/get-global-styles": {
|
|
19
|
+
title: __("Reading Site Colors", "wp-module-ai-chat"),
|
|
20
|
+
description: __(
|
|
21
|
+
"Fetching current color palette and typography settings",
|
|
22
|
+
"wp-module-ai-chat"
|
|
23
|
+
),
|
|
24
|
+
},
|
|
25
|
+
"nfd-agents/update-global-palette": {
|
|
26
|
+
title: __("Updating Site Colors", "wp-module-ai-chat"),
|
|
27
|
+
description: __("Applying new colors to global styles", "wp-module-ai-chat"),
|
|
28
|
+
},
|
|
29
|
+
"newfold-agents/get-global-styles": {
|
|
30
|
+
title: __("Reading Site Colors", "wp-module-ai-chat"),
|
|
31
|
+
description: __(
|
|
32
|
+
"Fetching current color palette and typography settings",
|
|
33
|
+
"wp-module-ai-chat"
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
"newfold-agents/update-global-palette": {
|
|
37
|
+
title: __("Updating Site Colors", "wp-module-ai-chat"),
|
|
38
|
+
description: __("Applying new colors to global styles", "wp-module-ai-chat"),
|
|
39
|
+
},
|
|
40
|
+
"blu/get-global-styles": {
|
|
41
|
+
title: __("Reading Site Colors", "wp-module-ai-chat"),
|
|
42
|
+
description: __(
|
|
43
|
+
"Fetching current color palette and typography settings",
|
|
44
|
+
"wp-module-ai-chat"
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
"blu/update-global-palette": {
|
|
48
|
+
title: __("Updating Site Colors", "wp-module-ai-chat"),
|
|
49
|
+
description: __("Applying new colors to global styles", "wp-module-ai-chat"),
|
|
50
|
+
},
|
|
51
|
+
"blu-get-global-styles": {
|
|
52
|
+
title: __("Reading Site Colors", "wp-module-ai-chat"),
|
|
53
|
+
description: __(
|
|
54
|
+
"Fetching current color palette and typography settings",
|
|
55
|
+
"wp-module-ai-chat"
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
"blu/get-active-global-styles": {
|
|
59
|
+
title: __("Reading Active Styles", "wp-module-ai-chat"),
|
|
60
|
+
description: __("Fetching active global styles", "wp-module-ai-chat"),
|
|
61
|
+
},
|
|
62
|
+
"blu-get-active-global-styles": {
|
|
63
|
+
title: __("Reading Active Styles", "wp-module-ai-chat"),
|
|
64
|
+
description: __("Fetching active global styles", "wp-module-ai-chat"),
|
|
65
|
+
},
|
|
66
|
+
"blu/get-active-global-styles-id": {
|
|
67
|
+
title: __("Getting Styles ID", "wp-module-ai-chat"),
|
|
68
|
+
description: __("Fetching active styles ID", "wp-module-ai-chat"),
|
|
69
|
+
},
|
|
70
|
+
"blu-get-active-global-styles-id": {
|
|
71
|
+
title: __("Getting Styles ID", "wp-module-ai-chat"),
|
|
72
|
+
description: __("Fetching active styles ID", "wp-module-ai-chat"),
|
|
73
|
+
},
|
|
74
|
+
"blu/update-global-styles": {
|
|
75
|
+
title: __("Updating Site Styles", "wp-module-ai-chat"),
|
|
76
|
+
description: __("Applying new styles to global styles", "wp-module-ai-chat"),
|
|
77
|
+
},
|
|
78
|
+
"blu-update-global-styles": {
|
|
79
|
+
title: __("Updating Site Styles", "wp-module-ai-chat"),
|
|
80
|
+
description: __("Applying new styles to global styles", "wp-module-ai-chat"),
|
|
81
|
+
},
|
|
82
|
+
"blu-update-global-palette": {
|
|
83
|
+
title: __("Updating Site Colors", "wp-module-ai-chat"),
|
|
84
|
+
description: __("Applying new colors to global styles", "wp-module-ai-chat"),
|
|
85
|
+
},
|
|
86
|
+
"blu/edit-block": {
|
|
87
|
+
title: __("Editing Block Content", "wp-module-ai-chat"),
|
|
88
|
+
description: __("Editing block content", "wp-module-ai-chat"),
|
|
89
|
+
},
|
|
90
|
+
"blu-edit-block": {
|
|
91
|
+
title: __("Editing Block Content", "wp-module-ai-chat"),
|
|
92
|
+
description: __("Editing block content", "wp-module-ai-chat"),
|
|
93
|
+
},
|
|
94
|
+
"blu/add-section": {
|
|
95
|
+
title: __("Adding New Section", "wp-module-ai-chat"),
|
|
96
|
+
description: __("Adding new section", "wp-module-ai-chat"),
|
|
97
|
+
},
|
|
98
|
+
"blu-add-section": {
|
|
99
|
+
title: __("Adding New Section", "wp-module-ai-chat"),
|
|
100
|
+
description: __("Adding new section", "wp-module-ai-chat"),
|
|
101
|
+
},
|
|
102
|
+
"blu/delete-block": {
|
|
103
|
+
title: __("Removing Block", "wp-module-ai-chat"),
|
|
104
|
+
description: __("Removing block", "wp-module-ai-chat"),
|
|
105
|
+
},
|
|
106
|
+
"blu-delete-block": {
|
|
107
|
+
title: __("Removing Block", "wp-module-ai-chat"),
|
|
108
|
+
description: __("Removing block", "wp-module-ai-chat"),
|
|
109
|
+
},
|
|
110
|
+
"blu/move-block": {
|
|
111
|
+
title: __("Moving Block", "wp-module-ai-chat"),
|
|
112
|
+
description: __("Moving block", "wp-module-ai-chat"),
|
|
113
|
+
},
|
|
114
|
+
"blu-move-block": {
|
|
115
|
+
title: __("Moving Block", "wp-module-ai-chat"),
|
|
116
|
+
description: __("Moving block", "wp-module-ai-chat"),
|
|
117
|
+
},
|
|
118
|
+
"mcp-adapter-discover-abilities": {
|
|
119
|
+
title: __("Discovering Actions", "wp-module-ai-chat"),
|
|
120
|
+
description: __("Finding available WordPress abilities", "wp-module-ai-chat"),
|
|
121
|
+
},
|
|
122
|
+
"mcp-adapter-get-ability-info": {
|
|
123
|
+
title: __("Getting Ability Info", "wp-module-ai-chat"),
|
|
124
|
+
description: __("Fetching ability details", "wp-module-ai-chat"),
|
|
125
|
+
},
|
|
126
|
+
"mcp-adapter-execute-ability": {
|
|
127
|
+
title: __("Executing Action", "wp-module-ai-chat"),
|
|
128
|
+
description: __("Running WordPress ability", "wp-module-ai-chat"),
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (abilityMap[abilityName]) {
|
|
133
|
+
return abilityMap[abilityName];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (abilityName === "preparing-changes") {
|
|
137
|
+
return {
|
|
138
|
+
title: __("Preparing changes", "wp-module-ai-chat"),
|
|
139
|
+
description: __("Building block markup", "wp-module-ai-chat"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
title:
|
|
145
|
+
abilityName?.replace(/[-_\/]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) ||
|
|
146
|
+
__("Executing", "wp-module-ai-chat"),
|
|
147
|
+
description: __("Running action", "wp-module-ai-chat"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get tool details for display (title, description, optional params string).
|
|
153
|
+
*
|
|
154
|
+
* @param {string} toolName The tool name.
|
|
155
|
+
* @param {Object} args The tool arguments (e.g. ability_name, parameters).
|
|
156
|
+
* @return {Object} { title, description, params }
|
|
157
|
+
*/
|
|
158
|
+
export function getToolDetails(toolName, args = {}) {
|
|
159
|
+
if (toolName === "mcp-adapter-execute-ability") {
|
|
160
|
+
const abilityName = args?.ability_name || "unknown";
|
|
161
|
+
const details = getAbilityDetails(abilityName);
|
|
162
|
+
|
|
163
|
+
let params = null;
|
|
164
|
+
const paletteAbility =
|
|
165
|
+
abilityName === "nfd-agents/update-global-palette" ||
|
|
166
|
+
abilityName === "newfold-agents/update-global-palette" ||
|
|
167
|
+
abilityName === "blu/update-global-palette" ||
|
|
168
|
+
abilityName === "blu-update-global-palette" ||
|
|
169
|
+
abilityName === "blu/update-global-styles" ||
|
|
170
|
+
abilityName === "blu-update-global-styles";
|
|
171
|
+
if (paletteAbility && args?.parameters?.colors) {
|
|
172
|
+
const colorCount = args.parameters.colors.length;
|
|
173
|
+
params = `${colorCount} color${colorCount !== 1 ? "s" : ""}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { ...details, params };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return getAbilityDetails(toolName);
|
|
180
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFD Agents URL Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for URL normalization and protocol conversion for WebSocket connections.
|
|
5
|
+
* Used for converting HTTP/HTTPS URLs to WebSocket protocols and normalizing site URLs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert HTTP/HTTPS URL to WebSocket protocol
|
|
10
|
+
*
|
|
11
|
+
* @param {string} url HTTP or HTTPS URL
|
|
12
|
+
* @return {string} WebSocket URL (ws:// or wss://)
|
|
13
|
+
*/
|
|
14
|
+
export const convertToWebSocketUrl = (url) => {
|
|
15
|
+
if (!url || typeof url !== "string") {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (url.startsWith("http://")) {
|
|
20
|
+
return url.replace("http://", "ws://");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (url.startsWith("https://")) {
|
|
24
|
+
return url.replace("https://", "wss://");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If no protocol specified, determine based on localhost
|
|
28
|
+
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
|
|
29
|
+
return isLocalhost(url) ? `ws://${url}` : `wss://${url}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return url;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if URL is localhost
|
|
37
|
+
*
|
|
38
|
+
* @param {string} url URL to check
|
|
39
|
+
* @return {boolean} True if URL is localhost
|
|
40
|
+
*/
|
|
41
|
+
export const isLocalhost = (url) => {
|
|
42
|
+
if (!url || typeof url !== "string") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalized = url.toLowerCase();
|
|
47
|
+
return normalized.includes("localhost") || normalized.includes("127.0.0.1");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the full WebSocket URL from config, session ID, and consumer type.
|
|
52
|
+
* Normalizes the 'nfd-agents' agent type alias to 'blu'.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} config Config object from fetchAgentConfig
|
|
55
|
+
* @param {string} config.gateway_url Gateway HTTP(S) URL
|
|
56
|
+
* @param {string} config.brand_id Brand identifier
|
|
57
|
+
* @param {string} config.agent_type Agent type (may be 'nfd-agents' alias)
|
|
58
|
+
* @param {string} config.jarvis_jwt Jarvis JWT for agents backend auth (backend returns this key)
|
|
59
|
+
* @param {string} config.huapi_token Optional alias for token (e.g. NFD_AGENTS_CHAT_DEBUG_TOKEN)
|
|
60
|
+
* @param {string} sessionId Session ID for the connection
|
|
61
|
+
* @param {string} consumerType Consumer type; passed to gateway as wordpress_${consumerType}
|
|
62
|
+
* @return {string} Full WebSocket URL with query parameters
|
|
63
|
+
*/
|
|
64
|
+
export const buildWebSocketUrl = (config, sessionId, consumerType) => {
|
|
65
|
+
const wsBaseUrl = convertToWebSocketUrl(config.gateway_url);
|
|
66
|
+
const agentType = (config.agent_type === "nfd-agents" ? "blu" : config.agent_type) || "blu";
|
|
67
|
+
const consumer = `wordpress_${consumerType}`;
|
|
68
|
+
const token = config.huapi_token ?? config.jarvis_jwt ?? "";
|
|
69
|
+
|
|
70
|
+
let url = `${wsBaseUrl}/${config.brand_id}/agents/${agentType}/v1/ws?session_id=${sessionId}&token=${encodeURIComponent(token)}&consumer=${encodeURIComponent(consumer)}`;
|
|
71
|
+
if (config.site_url && typeof config.site_url === "string" && config.site_url.trim()) {
|
|
72
|
+
url += `&site_url=${encodeURIComponent(config.site_url.trim())}`;
|
|
73
|
+
}
|
|
74
|
+
return url;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalize site URL to ensure it has a protocol.
|
|
79
|
+
* Adds http:// for localhost, https:// for other URLs.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} siteUrl Site URL to normalize
|
|
82
|
+
* @return {string} Normalized URL with protocol
|
|
83
|
+
*/
|
|
84
|
+
export const normalizeUrl = (siteUrl) => {
|
|
85
|
+
if (!siteUrl || typeof siteUrl !== "string") {
|
|
86
|
+
return siteUrl;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If already has protocol, return as-is
|
|
90
|
+
if (siteUrl.match(/^https?:\/\//)) {
|
|
91
|
+
return siteUrl;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if it's localhost and default to http://
|
|
95
|
+
if (isLocalhost(siteUrl)) {
|
|
96
|
+
return `http://${siteUrl}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For other URLs, default to https://
|
|
100
|
+
return `https://${siteUrl}`;
|
|
101
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for constructing WordPress REST API URLs that work
|
|
5
|
+
* regardless of permalink settings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the REST API base URL using rest_route parameter
|
|
10
|
+
* This works even when permalinks are not configured properly
|
|
11
|
+
*
|
|
12
|
+
* @param {string} [baseUrl] Base URL (defaults to current site URL)
|
|
13
|
+
* @return {string} REST API base URL
|
|
14
|
+
*/
|
|
15
|
+
export const getRestApiBaseUrl = (baseUrl = "") => {
|
|
16
|
+
// If baseUrl is provided and already contains rest_route, return as-is
|
|
17
|
+
if (baseUrl && baseUrl.includes("rest_route=")) {
|
|
18
|
+
return baseUrl;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get site URL from window if not provided
|
|
22
|
+
if (!baseUrl && typeof window !== "undefined") {
|
|
23
|
+
baseUrl = window.location.origin;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Use rest_route parameter instead of /wp-json/ path
|
|
27
|
+
// This works regardless of permalink settings
|
|
28
|
+
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
29
|
+
return `${baseUrl}${separator}rest_route=/`;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build a REST API endpoint URL
|
|
34
|
+
*
|
|
35
|
+
* @param {string} namespace REST API namespace (e.g., 'nfd-agents/chat/v1')
|
|
36
|
+
* @param {string} route REST API route (e.g., 'config')
|
|
37
|
+
* @param {string} [baseUrl] Base URL (defaults to current site URL)
|
|
38
|
+
* @return {string} Full REST API endpoint URL
|
|
39
|
+
*/
|
|
40
|
+
export const buildRestApiUrl = (namespace, route, baseUrl = "") => {
|
|
41
|
+
const restBase = getRestApiBaseUrl(baseUrl);
|
|
42
|
+
const endpoint = `${namespace}/${route}`;
|
|
43
|
+
|
|
44
|
+
// If restBase already has rest_route, append to it
|
|
45
|
+
if (restBase.includes("rest_route=")) {
|
|
46
|
+
return restBase.replace("rest_route=/", `rest_route=/${endpoint}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Otherwise, construct new rest_route parameter
|
|
50
|
+
const separator = restBase.includes("?") ? "&" : "?";
|
|
51
|
+
return `${restBase}${separator}rest_route=/${endpoint}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert a wp-json style URL to rest_route format
|
|
56
|
+
*
|
|
57
|
+
* @param {string} wpJsonUrl URL in format /wp-json/namespace/route
|
|
58
|
+
* @param {string} [baseUrl] Base URL (defaults to current site URL)
|
|
59
|
+
* @return {string} URL with rest_route parameter
|
|
60
|
+
*/
|
|
61
|
+
export const convertWpJsonToRestRoute = (wpJsonUrl, baseUrl = "") => {
|
|
62
|
+
// If already using rest_route, return as-is
|
|
63
|
+
if (wpJsonUrl.includes("rest_route=")) {
|
|
64
|
+
return wpJsonUrl;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract the path after /wp-json/
|
|
68
|
+
const wpJsonMatch = wpJsonUrl.match(/\/wp-json\/(.+)$/);
|
|
69
|
+
if (!wpJsonMatch) {
|
|
70
|
+
// Not a wp-json URL, return as-is
|
|
71
|
+
return wpJsonUrl;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const endpoint = wpJsonMatch[1];
|
|
75
|
+
|
|
76
|
+
// Get base URL
|
|
77
|
+
if (!baseUrl && typeof window !== "undefined") {
|
|
78
|
+
baseUrl = window.location.origin;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Remove /wp-json/ from baseUrl if present
|
|
82
|
+
const cleanBaseUrl = baseUrl.replace(/\/wp-json\/?$/, "");
|
|
83
|
+
|
|
84
|
+
// Build rest_route URL
|
|
85
|
+
const separator = cleanBaseUrl.includes("?") ? "&" : "?";
|
|
86
|
+
return `${cleanBaseUrl}${separator}rest_route=/${endpoint}`;
|
|
87
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import DOMPurify from "dompurify";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sanitize HTML content using DOMPurify to prevent XSS attacks
|
|
8
|
+
*
|
|
9
|
+
* @param {string} html - The HTML string to sanitize.
|
|
10
|
+
* @return {string} The sanitized HTML string.
|
|
11
|
+
*/
|
|
12
|
+
export const sanitizeHtml = (html) => {
|
|
13
|
+
return DOMPurify.sanitize(html, {
|
|
14
|
+
ALLOWED_TAGS: [
|
|
15
|
+
"section",
|
|
16
|
+
"div",
|
|
17
|
+
"h1",
|
|
18
|
+
"h2",
|
|
19
|
+
"h3",
|
|
20
|
+
"h4",
|
|
21
|
+
"h5",
|
|
22
|
+
"h6",
|
|
23
|
+
"p",
|
|
24
|
+
"span",
|
|
25
|
+
"strong",
|
|
26
|
+
"em",
|
|
27
|
+
"b",
|
|
28
|
+
"i",
|
|
29
|
+
"u",
|
|
30
|
+
"s",
|
|
31
|
+
"mark",
|
|
32
|
+
"small",
|
|
33
|
+
"sub",
|
|
34
|
+
"sup",
|
|
35
|
+
"a",
|
|
36
|
+
"br",
|
|
37
|
+
"ul",
|
|
38
|
+
"ol",
|
|
39
|
+
"li",
|
|
40
|
+
"dl",
|
|
41
|
+
"dt",
|
|
42
|
+
"dd",
|
|
43
|
+
"blockquote",
|
|
44
|
+
"code",
|
|
45
|
+
"pre",
|
|
46
|
+
"table",
|
|
47
|
+
"thead",
|
|
48
|
+
"tbody",
|
|
49
|
+
"tr",
|
|
50
|
+
"th",
|
|
51
|
+
"td",
|
|
52
|
+
"hr",
|
|
53
|
+
"address",
|
|
54
|
+
"time",
|
|
55
|
+
],
|
|
56
|
+
ALLOWED_ATTR: ["style", "class", "id", "href", "datetime", "target", "rel"],
|
|
57
|
+
ALLOW_DATA_ATTR: false,
|
|
58
|
+
FORBID_TAGS: [
|
|
59
|
+
"script",
|
|
60
|
+
"object",
|
|
61
|
+
"embed",
|
|
62
|
+
"iframe",
|
|
63
|
+
"form",
|
|
64
|
+
"fieldset",
|
|
65
|
+
"legend",
|
|
66
|
+
"label",
|
|
67
|
+
"input",
|
|
68
|
+
"textarea",
|
|
69
|
+
"select",
|
|
70
|
+
"option",
|
|
71
|
+
"button",
|
|
72
|
+
"details",
|
|
73
|
+
"summary",
|
|
74
|
+
"progress",
|
|
75
|
+
"meter",
|
|
76
|
+
],
|
|
77
|
+
FORBID_ATTR: ["onerror", "onload", "onclick", "onmouseover", "onfocus", "onblur"],
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if content contains HTML tags
|
|
83
|
+
*
|
|
84
|
+
* @param {string} content - The content to check.
|
|
85
|
+
* @return {boolean} True if content contains HTML tags.
|
|
86
|
+
*/
|
|
87
|
+
export const containsHtml = (content) => {
|
|
88
|
+
return /<[a-z][\s\S]*>/i.test(content);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
sanitizeHtml,
|
|
93
|
+
containsHtml,
|
|
94
|
+
};
|