Skip to content

Commit 366c5f4

Browse files
committed
Handle edge node edge-case
1 parent b95456e commit 366c5f4

2 files changed

Lines changed: 104 additions & 11 deletions

File tree

packages/eui/src/components/markdown_editor/plugins/remark/remark_intraword_underscore.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,54 @@ describe('remarkIntrawordUnderscore', () => {
6363
'Fields ABDC__AppleBanana__c and ABDC__MangoKiwi__c are required'
6464
);
6565
});
66+
67+
it('preserves trailing double underscores as plain text', () => {
68+
const { container } = render(
69+
<EuiMarkdownFormat>{'Mango__Kiwi__'}</EuiMarkdownFormat>
70+
);
71+
72+
expect(container.querySelector('strong')).not.toBeInTheDocument();
73+
expect(container).toHaveTextContent('Mango__Kiwi__');
74+
});
75+
76+
it('preserves leading double underscores as plain text', () => {
77+
const { container } = render(
78+
<EuiMarkdownFormat>{'__Mango__Kiwi'}</EuiMarkdownFormat>
79+
);
80+
81+
expect(container.querySelector('strong')).not.toBeInTheDocument();
82+
expect(container).toHaveTextContent('__Mango__Kiwi');
83+
});
84+
85+
it('preserves trailing single underscores as plain text', () => {
86+
const { container } = render(
87+
<EuiMarkdownFormat>{'Mango_Kiwi_'}</EuiMarkdownFormat>
88+
);
89+
90+
expect(container.querySelector('em')).not.toBeInTheDocument();
91+
expect(container).toHaveTextContent('Mango_Kiwi_');
92+
});
93+
94+
it('preserves mixed double/single trailing underscores as plain text', () => {
95+
const { container } = render(
96+
<EuiMarkdownFormat>{'Mango__Kiwi_'}</EuiMarkdownFormat>
97+
);
98+
99+
expect(container.querySelector('em')).not.toBeInTheDocument();
100+
expect(container.querySelector('strong')).not.toBeInTheDocument();
101+
expect(container).toHaveTextContent('Mango__Kiwi_');
102+
});
103+
104+
it('handles edge-case identifiers mixed in a sentence', () => {
105+
const { container } = render(
106+
<EuiMarkdownFormat>
107+
{'Check __Mango__Kiwi and Mango__Kiwi__ fields'}
108+
</EuiMarkdownFormat>
109+
);
110+
111+
expect(container.querySelector('strong')).not.toBeInTheDocument();
112+
expect(container).toHaveTextContent(
113+
'Check __Mango__Kiwi and Mango__Kiwi__ fields'
114+
);
115+
});
66116
});

packages/eui/src/components/markdown_editor/plugins/remark/remark_intraword_underscore.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,33 @@ function processParent(parent: Parent, source: string) {
9595
}
9696
}
9797

98+
function getInnerText(node: Node): string {
99+
if (isTextNode(node)) return node.value;
100+
if ('children' in node) {
101+
const children = (node as Parent).children;
102+
if (children.length > 0) return getInnerText(children[0]);
103+
}
104+
return '';
105+
}
106+
107+
function getInnerTextEnd(node: Node): string {
108+
if (isTextNode(node)) return node.value;
109+
if ('children' in node) {
110+
const children = (node as Parent).children;
111+
if (children.length > 0)
112+
return getInnerTextEnd(children[children.length - 1]);
113+
}
114+
return '';
115+
}
116+
98117
/**
99118
* Checks whether the emphasis/strong node at `index` within `parent`
100119
* is an intraword usage of underscore delimiters.
120+
*
121+
* A node is intraword when at least one side has an alphanumeric text
122+
* neighbor AND the inner content on the corresponding delimiter side
123+
* also touches word characters — proving the underscores are embedded
124+
* in a word rather than used as formatting.
101125
*/
102126
function isIntraword(parent: Parent, index: number, source: string): boolean {
103127
const node = parent.children[index];
@@ -112,19 +136,38 @@ function isIntraword(parent: Parent, index: number, source: string): boolean {
112136
const next =
113137
index < parent.children.length - 1 ? parent.children[index + 1] : null;
114138

115-
const prevEndsWithAlpha =
116-
prev != null &&
117-
isTextNode(prev) &&
118-
prev.value.length > 0 &&
119-
isAlphanumeric(prev.value[prev.value.length - 1]);
139+
const prevChar =
140+
prev != null && isTextNode(prev) && prev.value.length > 0
141+
? prev.value[prev.value.length - 1]
142+
: null;
143+
144+
const nextChar =
145+
next != null && isTextNode(next) && next.value.length > 0
146+
? next.value[0]
147+
: null;
148+
149+
const prevIsAlpha = prevChar != null && isAlphanumeric(prevChar);
150+
const nextIsAlpha = nextChar != null && isAlphanumeric(nextChar);
120151

121-
const nextStartsWithAlpha =
122-
next != null &&
123-
isTextNode(next) &&
124-
next.value.length > 0 &&
125-
isAlphanumeric(next.value[0]);
152+
// Both sides flanked by alphanumeric — classic intraword (e.g. `foo__bar__baz`)
153+
if (prevIsAlpha && nextIsAlpha) return true;
154+
155+
// One-sided: prev is alpha or underscore, no alpha next — check inner text
156+
// starts with alpha (e.g. `Lorem__ipsum__` or `Lorem__ipsum_`)
157+
if (prevIsAlpha || prevChar === '_') {
158+
const inner = getInnerText(node);
159+
if (inner.length > 0 && isAlphanumeric(inner[0])) return true;
160+
}
161+
162+
// One-sided: next is alpha or underscore, no alpha prev — check inner text
163+
// ends with alpha (e.g. `__Lorem__ipsum` or `_Lorem__ipsum`)
164+
if (nextIsAlpha || nextChar === '_') {
165+
const inner = getInnerTextEnd(node);
166+
if (inner.length > 0 && isAlphanumeric(inner[inner.length - 1]))
167+
return true;
168+
}
126169

127-
return prevEndsWithAlpha && nextStartsWithAlpha;
170+
return false;
128171
}
129172

130173
function mergeAdjacentText(parent: Parent) {

0 commit comments

Comments
 (0)