|
|
@@ -91,22 +91,45 @@ const codeThemeStyles = {
|
|
|
},
|
|
|
};
|
|
|
|
|
|
+const escapeHtml = (str) => {
|
|
|
+ return str
|
|
|
+ .replace(/&/g, '&')
|
|
|
+ .replace(/</g, '<')
|
|
|
+ .replace(/>/g, '>')
|
|
|
+ .replace(/"/g, '"')
|
|
|
+ .replace(/'/g, ''');
|
|
|
+};
|
|
|
+
|
|
|
const highlightJson = (str) => {
|
|
|
- return str.replace(
|
|
|
- /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
|
|
|
- (match) => {
|
|
|
- let color = '#b5cea8';
|
|
|
- if (/^"/.test(match)) {
|
|
|
- color = /:$/.test(match) ? '#9cdcfe' : '#ce9178';
|
|
|
- } else if (/true|false|null/.test(match)) {
|
|
|
- color = '#569cd6';
|
|
|
- }
|
|
|
- return `<span style="color: ${color}">${match}</span>`;
|
|
|
- },
|
|
|
- );
|
|
|
+ const tokenRegex =
|
|
|
+ /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g;
|
|
|
+
|
|
|
+ let result = '';
|
|
|
+ let lastIndex = 0;
|
|
|
+ let match;
|
|
|
+
|
|
|
+ while ((match = tokenRegex.exec(str)) !== null) {
|
|
|
+ // Escape non-token text (structural chars like {, }, [, ], :, comma, whitespace)
|
|
|
+ result += escapeHtml(str.slice(lastIndex, match.index));
|
|
|
+
|
|
|
+ const token = match[0];
|
|
|
+ let color = '#b5cea8';
|
|
|
+ if (/^"/.test(token)) {
|
|
|
+ color = /:$/.test(token) ? '#9cdcfe' : '#ce9178';
|
|
|
+ } else if (/true|false|null/.test(token)) {
|
|
|
+ color = '#569cd6';
|
|
|
+ }
|
|
|
+ // Escape token content before wrapping in span
|
|
|
+ result += `<span style="color: ${color}">${escapeHtml(token)}</span>`;
|
|
|
+ lastIndex = tokenRegex.lastIndex;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Escape remaining text
|
|
|
+ result += escapeHtml(str.slice(lastIndex));
|
|
|
+ return result;
|
|
|
};
|
|
|
|
|
|
-const linkRegex = /(https?:\/\/[^\s<"'\]),;}]+)/g;
|
|
|
+const linkRegex = /(https?:\/\/(?:[^\s<"'\]),;&}]|&)+)/g;
|
|
|
|
|
|
const linkifyHtml = (html) => {
|
|
|
const parts = html.split(/(<[^>]+>)/g);
|
|
|
@@ -184,14 +207,14 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
|
|
|
|
|
|
const highlightedContent = useMemo(() => {
|
|
|
if (contentMetrics.isVeryLarge && !isExpanded) {
|
|
|
- return displayContent;
|
|
|
+ return escapeHtml(displayContent);
|
|
|
}
|
|
|
|
|
|
if (isJsonLike(displayContent, language)) {
|
|
|
return highlightJson(displayContent);
|
|
|
}
|
|
|
|
|
|
- return displayContent;
|
|
|
+ return escapeHtml(displayContent);
|
|
|
}, [displayContent, language, contentMetrics.isVeryLarge, isExpanded]);
|
|
|
|
|
|
const renderedContent = useMemo(() => {
|