Skip to content

Commit 38a1207

Browse files
Fix CodeBoxHTML's issues by using the CodeBox component with SyntaxHi… (#1653)
* Fix CodeBoxHTML's issues by using the CodeBox component with SyntaxHighlighter * Strip and decode the HTML into a string, then pass it to the highlighter codebox. * Good performance, copying and styling now work well * Update apps/cyberstorm-remix/app/commonComponents/CodeBoxHTML/CodeBoxHTML.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d3c5101 commit 38a1207

File tree

4 files changed

+90
-119
lines changed

4 files changed

+90
-119
lines changed

apps/cyberstorm-remix/app/commonComponents/CodeBoxHTML/CodeBoxHTML.css

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,5 @@
22
position: relative;
33
align-self: stretch;
44
width: 100%;
5-
padding: var(--space-16);
6-
border: 1px solid var(--color-surface-7);
7-
8-
border-radius: var(--radius-md);
95
overflow: auto;
10-
background-color: var(--color-surface-1);
11-
12-
--border-color: transparent;
13-
}
14-
15-
.code-box-html__content {
16-
position: relative;
17-
width: 100%;
18-
}
19-
20-
.code-box-html__line {
21-
position: absolute;
22-
width: 100%;
23-
font-size: var(--font-size-body-md);
24-
font-family: var(--font-family-monospace);
25-
font-style: normal;
26-
line-height: var(--line-height-md);
276
}
Lines changed: 28 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,50 @@
1-
import { useState, useMemo, useRef, useEffect } from "react";
1+
import { CodeBox, NewAlert } from "@thunderstore/cyberstorm";
2+
import { memo, useMemo } from "react";
3+
import { stripHtmlTags } from "cyberstorm/utils/HTMLParsing";
24
import "./CodeBoxHTML.css";
3-
import "./Highlight.css";
45

56
export interface CodeBoxHTMLProps {
67
value?: string;
78
maxHeight?: number;
9+
language?: string;
810
}
911

1012
/**
11-
* CodeBox component which renders HTML and
12-
* uses virtual scrolling to render the content
13-
* in parts to improve performance
13+
* CodeBox component which renders HTML content by stripping HTML tags
14+
* and passing the plain text to the CodeBox component for syntax highlighting
1415
*/
15-
export function CodeBoxHTML({ value = "", maxHeight = 600 }: CodeBoxHTMLProps) {
16-
const [scrollTop, setScrollTop] = useState(0);
17-
const [containerHeight, setContainerHeight] = useState(maxHeight);
18-
const [containerWidth, setContainerWidth] = useState<number | undefined>(
19-
undefined
20-
);
21-
const [lineHeight, setLineHeight] = useState(21); // We'll measure this based on the actual styles later
22-
const containerRef = useRef<HTMLDivElement>(null);
23-
24-
// Extra lines to render above/below viewport, so that there's content
25-
// to render immediately when scrolling, making it smoother
26-
const BUFFER_LINES = 25;
27-
const lines = useMemo(() => value.split("\n"), [value]);
28-
29-
useEffect(() => {
30-
const { width, lineHeight: measuredLineHeight } =
31-
calculateDimensions(lines);
32-
setContainerWidth(width);
33-
setLineHeight(measuredLineHeight);
34-
}, [lines]);
35-
36-
useEffect(() => {
37-
const updateHeight = () => {
38-
if (containerRef.current) {
39-
setContainerHeight(containerRef.current.clientHeight);
40-
}
41-
};
42-
updateHeight();
43-
window.addEventListener("resize", updateHeight);
44-
return () => window.removeEventListener("resize", updateHeight);
45-
}, []);
46-
47-
const visibleStart = Math.max(
48-
0,
49-
Math.floor(scrollTop / lineHeight) - BUFFER_LINES
50-
);
51-
52-
const visibleEnd = Math.min(
53-
visibleStart + Math.ceil(containerHeight / lineHeight) + BUFFER_LINES * 2,
54-
lines.length
55-
);
56-
57-
const totalHeight = lines.length * lineHeight;
16+
export const CodeBoxHTML = memo(function CodeBoxHTML({
17+
value = "",
18+
maxHeight = 600,
19+
language = "text",
20+
}: CodeBoxHTMLProps) {
21+
const result = useMemo(() => {
22+
try {
23+
const text = stripHtmlTags(value);
24+
return { text, error: null };
25+
} catch (err) {
26+
const errorMessage =
27+
err instanceof Error ? err.message : "Failed to process HTML content";
28+
return { text: "", error: errorMessage };
29+
}
30+
}, [value]);
31+
32+
if (result.error) {
33+
return <NewAlert csVariant="danger">{result.error}</NewAlert>;
34+
}
5835

5936
return (
6037
<div
61-
ref={containerRef}
6238
className="code-box-html"
6339
style={{
64-
height: maxHeight,
40+
maxHeight,
6541
width: "100%",
6642
overflow: "auto",
6743
}}
68-
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
6944
>
70-
<pre
71-
className="code-box-html__content"
72-
style={{
73-
height: totalHeight,
74-
width: containerWidth ? `${containerWidth}px` : "auto",
75-
minWidth: "100%",
76-
}}
77-
>
78-
{lines.slice(visibleStart, visibleEnd).map((line, index) => (
79-
<code
80-
key={visibleStart + index}
81-
className="code-box-html__line highlight"
82-
style={{
83-
top: (visibleStart + index) * lineHeight,
84-
height: lineHeight,
85-
}}
86-
dangerouslySetInnerHTML={{ __html: line }}
87-
/>
88-
))}
89-
</pre>
45+
<CodeBox value={result.text} language={language} />
9046
</div>
9147
);
92-
}
93-
94-
/*
95-
* Measure the longest line width and line height using the same styles as the component
96-
*/
97-
const calculateDimensions = (lines: string[]) => {
98-
if (lines.length === 0 || (lines.length === 1 && lines[0] === ""))
99-
return { width: 0, lineHeight: 21 };
100-
const longestLine = lines.reduce((longest, current) =>
101-
current.length > longest.length ? current : longest
102-
);
103-
const measureElement = document.createElement("div");
104-
measureElement.className = "code-box-html__line";
105-
measureElement.style.width = "auto";
106-
measureElement.style.padding = "0";
107-
measureElement.style.border = "none";
108-
document.body.appendChild(measureElement);
109-
try {
110-
measureElement.innerHTML = longestLine;
111-
const width = measureElement.offsetWidth;
112-
const measuredLineHeight = measureElement.offsetHeight;
113-
return { width, lineHeight: measuredLineHeight };
114-
} finally {
115-
document.body.removeChild(measureElement);
116-
}
117-
};
48+
});
11849

11950
CodeBoxHTML.displayName = "CodeBoxHTML";

apps/cyberstorm-remix/app/p/tabs/Source/Source.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,10 @@ export default function Source() {
181181
className="package-source__decompilations-file"
182182
key={decompilation.source_file_name}
183183
>
184-
<CodeBoxHTML value={decompilation.result} />
184+
<CodeBoxHTML
185+
value={decompilation.result}
186+
language={"csharp"}
187+
/>
185188
</div>
186189
</div>
187190
);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Named HTML entities map for fast lookup
2+
const NAMED_ENTITIES: Record<string, string> = {
3+
lt: "<",
4+
gt: ">",
5+
quot: '"',
6+
apos: "'",
7+
nbsp: " ",
8+
amp: "&",
9+
};
10+
11+
/**
12+
* Strips HTML tags from a string and decodes HTML entities to extract plain text content.
13+
* Uses a loop to handle potentially nested or malformed tags.
14+
* This is safe for our use case because:
15+
* 1. Input is already server-sanitized syntax highlighting HTML
16+
* 2. The result is only used as text content, not rendered as HTML
17+
*/
18+
export function stripHtmlTags(html: string = ""): string {
19+
if (!html) return "";
20+
21+
// Strip HTML tags - loop handles nested/malformed tags like <scr<script>ipt>
22+
const tagPattern = /<[^>]*>/g;
23+
let result = html;
24+
let previous;
25+
do {
26+
previous = result;
27+
result = result.replace(tagPattern, "");
28+
} while (result !== previous);
29+
30+
return decodeHtmlEntities(result);
31+
}
32+
33+
/**
34+
* Decodes HTML entities with a single regex pass for performance.
35+
* Handles named entities, numeric (&#123;), and hex (&#x1F;) entities.
36+
* All entities including &amp; are handled in one pass to correctly decode
37+
* double-encoded entities like &amp;lt;.
38+
*/
39+
function decodeHtmlEntities(text: string): string {
40+
return text.replace(
41+
/&(?:#(\d+)|#x([0-9a-fA-F]+)|([a-z0-9]+));/gi,
42+
(match, dec, hex, named) => {
43+
if (named) {
44+
const lower = named.toLowerCase();
45+
return NAMED_ENTITIES[lower] || match;
46+
}
47+
if (dec || hex) {
48+
try {
49+
const codePoint = dec ? Number(dec) : parseInt(hex, 16);
50+
return String.fromCodePoint(codePoint);
51+
} catch {
52+
return match; // Invalid code point, return unchanged
53+
}
54+
}
55+
return match;
56+
}
57+
);
58+
}

0 commit comments

Comments
 (0)