Skip to content

Commit 3ce670a

Browse files
committed
fix: prevent partial-word gender highlights in the editor
- add Unicode-aware word-boundary detection to the gender-bias analysis utility - update EditorComponent to skip gender findings inside longer words - test that Führung is not highlighted inside Personalführung - test that lead is not highlighted inside misleading
1 parent a9bc453 commit 3ce670a

3 files changed

Lines changed: 43 additions & 3 deletions

File tree

src/main/webapp/app/shared/components/atoms/editor/editor.component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TooltipModule } from 'primeng/tooltip';
66
import { ContentChange, QuillEditorComponent } from 'ngx-quill';
77
import { FormsModule } from '@angular/forms';
88
import { extractTextFromHtml } from 'app/shared/util/text.util';
9-
import { getUniqueNonInclusiveWords } from 'app/shared/gender-bias-analysis/gender-bias-analysis.utils';
9+
import { getUniqueNonInclusiveWords, isWordChar } from 'app/shared/gender-bias-analysis/gender-bias-analysis.utils';
1010
import { GenderBiasAnalysisService } from 'app/shared/gender-bias-analysis/gender-bias-analysis';
1111
import { GenderBiasAnalysisResponse } from 'app/generated/model/gender-bias-analysis-response';
1212
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
@@ -466,13 +466,16 @@ export class EditorComponent extends BaseInputDirective<string> {
466466

467467
for (const { text } of genderBiasHighlights) {
468468
const searchText = text.toLowerCase();
469+
if (!searchText) continue;
469470
let startIndex = 0;
470471

471472
while (startIndex < fullText.length) {
472473
const index = fullText.indexOf(searchText, startIndex);
473474
if (index === -1) break;
474-
editor.formatText(index, text.length, 'genderBiasHighlight', true);
475-
startIndex = index + text.length;
475+
if (!isWordChar(fullText[index - 1]) && !isWordChar(fullText[index + searchText.length])) {
476+
editor.formatText(index, searchText.length, 'genderBiasHighlight', true);
477+
}
478+
startIndex = index + searchText.length;
476479
}
477480
}
478481
}

src/main/webapp/app/shared/gender-bias-analysis/gender-bias-analysis.utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ export function getUniqueNonInclusiveWords(biasedWords: BiasedWordDTO[] | undefi
1414
const words = biasedWords?.filter(word => word.type === 'non-inclusive').map(word => word.word?.trim()) ?? [];
1515
return words.filter((word): word is string => Boolean(word)).filter((word, index, values) => values.indexOf(word) === index);
1616
}
17+
18+
/**
19+
* Whether the character is part of a word for highlight-boundary purposes.
20+
* Uses \p{L} rather than \w so umlauts and ß count; the hyphen is excluded to
21+
* mirror deHyphenNonCodedWords on the server.
22+
*
23+
* @param char the character to test, or undefined at the text boundary
24+
* @returns true if the character continues a word
25+
*/
26+
export function isWordChar(char: string | undefined): boolean {
27+
return char !== undefined && /[\p{L}\p{N}]/u.test(char);
28+
}

src/test/webapp/app/shared/components/atoms/editor/editor.component.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,31 @@ describe('EditorComponent', () => {
313313
fixture.detectChanges();
314314
expect(comp.genderBiasHighlights()).toHaveLength(0);
315315
});
316+
317+
it.each([
318+
['führung', 'Führung Personalführung\n'],
319+
['lead', 'lead misleading\n'],
320+
])('should highlight %s only as a complete word', (word, text) => {
321+
const fixture = createFixture();
322+
const comp = fixture.componentInstance;
323+
const formatText = vi.fn();
324+
Object.defineProperty(comp.quillEditorComponent()!, 'quillEditor', {
325+
configurable: true,
326+
value: { formatText, getLength: () => text.length, getText: () => text },
327+
});
328+
329+
fixture.componentRef.setInput('showGenderDecoderButton', true);
330+
analysisSubject.next({ biasedWords: [{ word, type: 'non-inclusive' }] });
331+
fixture.detectChanges();
332+
formatText.mockClear();
333+
334+
comp.applyPendingHighlights();
335+
336+
const appliedGenderHighlights = formatText.mock.calls.filter(
337+
([, , format, value]) => format === 'genderBiasHighlight' && value === true,
338+
);
339+
expect(appliedGenderHighlights).toEqual([[0, word.length, 'genderBiasHighlight', true]]);
340+
});
316341
});
317342

318343
describe('mapToLanguageCode', () => {

0 commit comments

Comments
 (0)