diff --git a/src/components/code/code_block_copy.spec.tsx b/src/components/code/code_block_copy.spec.tsx
index b2817c1f5bc..2571447dcb5 100644
--- a/src/components/code/code_block_copy.spec.tsx
+++ b/src/components/code/code_block_copy.spec.tsx
@@ -105,8 +105,6 @@ describe('EuiCodeBlock copy UX', () => {
cy.realMount({questionContent});
cy.get('[data-test-subj="euiCodeBlockCopy"]').realClick();
- // TODO: Remove incorrect assertion and uncomment correct assertion once bug is fixed
- assertClipboardContentEquals('hello\n\nworld');
- // assertClipboardContentEquals(questionContent);
+ assertClipboardContentEquals(questionContent);
});
});
diff --git a/src/components/code/code_block_copy.tsx b/src/components/code/code_block_copy.tsx
index 9af8059847a..6e87d11bd0c 100644
--- a/src/components/code/code_block_copy.tsx
+++ b/src/components/code/code_block_copy.tsx
@@ -11,6 +11,7 @@ import { useInnerText } from '../inner_text';
import { EuiCopy } from '../copy';
import { useEuiI18n } from '../i18n';
import { EuiButtonIcon } from '../button';
+import { NEW_LINE_REGEX_GLOBAL } from './utils';
/**
* Hook that returns copy-related state/logic/utils
@@ -26,7 +27,14 @@ export const useCopy = ({
}) => {
const [innerTextRef, _innerText] = useInnerText('');
const innerText = useMemo(
- () => _innerText?.replace(/[\r\n?]{2}|\n\n/g, '\n') || '',
+ () =>
+ _innerText
+ // Normalize line terminations to match native JS format
+ ?.replace(NEW_LINE_REGEX_GLOBAL, '\n')
+ // Reduce two or more consecutive new line characters to a single one
+ // This is needed primarily because of how syntax highlighting
+ // generated DOM elements affect `innerText` output.
+ .replace(/\n{2,}/g, '\n') || '',
[_innerText]
);
const textToCopy = isVirtualized ? `${children}` : innerText; // Virtualized code blocks do not have inner text
diff --git a/src/components/code/utils.tsx b/src/components/code/utils.tsx
index ceb0e069576..9305299919f 100644
--- a/src/components/code/utils.tsx
+++ b/src/components/code/utils.tsx
@@ -41,6 +41,22 @@ export type EuiCodeSharedProps = CommonProps &
export const SUPPORTED_LANGUAGES = listLanguages();
export const DEFAULT_LANGUAGE = 'text';
+/**
+ * Platform-agnostic new line regex that safely matches all standard
+ * line termination conventions:
+ * - LF: Unix-based platforms and JS-native sources like text areas
+ * - CRLF: Windows
+ * - CR: Mac Classic; to support files saved a long time ago
+ */
+export const NEW_LINE_REGEX = /\r\n|\r|\n/;
+
+/**
+ * Platform-agnostic global new line regex that safely matches all standard
+ * line termination conventions.
+ * See [NEW_LINE_REGEX]{@link NEW_LINE_REGEX} for more details.
+ */
+export const NEW_LINE_REGEX_GLOBAL = new RegExp(NEW_LINE_REGEX, 'g');
+
export const checkSupportedLanguage = (language: string): string => {
return SUPPORTED_LANGUAGES.includes(language) ? language : DEFAULT_LANGUAGE;
};
@@ -143,12 +159,12 @@ const addLineData = (
return nodes.reduce((result, node) => {
const lineStart = data.lineNumber;
if (node.type === 'text') {
- if (!node.value.match(/\r\n?|\n/)) {
+ if (!node.value.match(NEW_LINE_REGEX)) {
node.lineStart = lineStart;
node.lineEnd = lineStart;
result.push(node);
} else {
- const lines = node.value.split(/\r\n?|\n/);
+ const lines = node.value.split(NEW_LINE_REGEX);
lines.forEach((line, i) => {
const num = i === 0 ? data.lineNumber : ++data.lineNumber;
result.push({
diff --git a/upcoming_changelogs/6794.md b/upcoming_changelogs/6794.md
new file mode 100644
index 00000000000..2b0002cc430
--- /dev/null
+++ b/upcoming_changelogs/6794.md
@@ -0,0 +1,3 @@
+**Bug fixes**
+
+- Fixed `EuiCodeBlock` potentially incorrectly ignoring lines ending with a question mark when using the Copy button.