1
0
mirror of https://github.com/element-hq/element-web.git synced 2025-08-09 14:42:51 +03:00
Files
element-web/src/components/views/messages/CodeBlock.tsx
Michael Telatynski 1e42f28a69 Harden Settings using mapped types (#28775)
* Harden Settings using mapped types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix issues found during hardening

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove oidc native flow stale key

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-12-23 20:25:15 +00:00

137 lines
5.1 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { useState } from "react";
import classNames from "classnames";
import { TooltipProvider } from "@vector-im/compound-web";
import { useSettingValue } from "../../../hooks/useSettings.ts";
import { CopyTextButton } from "../elements/CopyableText.tsx";
const MAX_HIGHLIGHT_LENGTH = 4096;
const MAX_LINES_BEFORE_COLLAPSE = 5;
interface Props {
children: HTMLElement;
onHeightChanged?(): void;
}
const ExpandCollapseButton: React.FC<{
expanded: boolean;
onClick(): void;
}> = ({ expanded, onClick }) => {
return (
<span
className={classNames("mx_EventTile_button", {
mx_EventTile_expandButton: !expanded,
mx_EventTile_collapseButton: expanded,
})}
onClick={onClick}
/>
);
};
const CodeBlock: React.FC<Props> = ({ children, onHeightChanged }) => {
const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue("expandCodeByDefault");
const [expanded, setExpanded] = useState(expandCodeByDefault);
let expandCollapseButton: JSX.Element | undefined;
if (children.textContent && children.textContent.split("\n").length >= MAX_LINES_BEFORE_COLLAPSE) {
expandCollapseButton = (
<ExpandCollapseButton
expanded={expanded}
onClick={() => {
setExpanded(!expanded);
// By expanding/collapsing we changed the height, therefore we call this
onHeightChanged?.();
}}
/>
);
}
let lineNumbers: JSX.Element | undefined;
if (showCodeLineNumbers) {
// Calculate number of lines in pre
const number = children.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
// Iterate through lines starting with 1 (number of the first line is 1)
lineNumbers = (
<span className="mx_EventTile_lineNumbers">
{Array.from({ length: number }, (_, i) => i + 1).map((i) => (
<span key={i}>{i}</span>
))}
</span>
);
}
async function highlightCode(div: HTMLElement | null): Promise<void> {
const code = div?.getElementsByTagName("code")[0];
if (!code) return;
const { default: highlight } = await import("highlight.js");
if (code.textContent && code.textContent.length > MAX_HIGHLIGHT_LENGTH) {
console.log(
`Code block is bigger than highlight limit (${code.textContent.length} > ${MAX_HIGHLIGHT_LENGTH}): not highlighting`,
);
return;
}
let advertisedLang: string | undefined;
for (const cl of code.className.split(/\s+/)) {
if (cl.startsWith("language-")) {
const maybeLang = cl.split("-", 2)[1];
if (highlight.getLanguage(maybeLang)) {
advertisedLang = maybeLang;
break;
}
}
}
if (advertisedLang) {
// If the code says what language it is, highlight it in that language
// We don't use highlightElement here because we can't force language detection
// off. It should use the one we've found in the CSS class but we'd rather pass
// it in explicitly to make sure.
code.innerHTML = highlight.highlight(code.textContent ?? "", { language: advertisedLang }).value;
} else if (enableSyntaxHighlightLanguageDetection) {
// User has language detection enabled, so highlight the block with auto-highlighting enabled.
// We pass highlightjs the text to highlight rather than letting it
// work on the DOM with highlightElement because that also adds CSS
// classes to the pre/code element that we don't want (the CSS
// conflicts with our own).
code.innerHTML = highlight.highlightAuto(code.textContent ?? "").value;
}
}
return (
<TooltipProvider>
<pre
className={classNames({
mx_EventTile_collapsedCodeBlock: !expanded,
})}
>
{lineNumbers}
<div
style={{ display: "contents" }}
dangerouslySetInnerHTML={{ __html: children.innerHTML }}
ref={highlightCode}
/>
</pre>
{expandCollapseButton}
<CopyTextButton
getTextToCopy={() => children.getElementsByTagName("code")[0]?.textContent ?? null}
className={classNames("mx_EventTile_button mx_EventTile_copyButton", {
mx_EventTile_buttonBottom: !!expandCollapseButton,
})}
/>
</TooltipProvider>
);
};
export default CodeBlock;