diff --git a/.changeset/shaggy-toes-speak.md b/.changeset/shaggy-toes-speak.md new file mode 100644 index 00000000..c7c62ac0 --- /dev/null +++ b/.changeset/shaggy-toes-speak.md @@ -0,0 +1,5 @@ +--- +"react-mentions": patch +--- + +Fixed substring matching bug when typeing korean. diff --git a/src/utils/createMatcher.js b/src/utils/createMatcher.js new file mode 100644 index 00000000..6ec1c0ab --- /dev/null +++ b/src/utils/createMatcher.js @@ -0,0 +1,51 @@ +import escapeRegex from './escapeRegex' + +function ch2pattern(ch) { + const offset = 44032 /* '가'의 코드 */ + // 한국어 음절 (Korean syllables) + if (/[가-힣]/.test(ch)) { + const chCode = ch.charCodeAt(0) - offset + // 종성이 있으면 문자 그대로를 찾는다. + if (chCode % 28 > 0) { + return ch + } + + const begin = Math.floor(chCode / 28) * 28 + offset + const end = begin + 27 + return `[\\u${begin.toString(16)}-\\u${end.toString(16)}]` + } + + // 한글 자음 (Korean consonants) + if (/[ㄱ-ㅎ]/.test(ch)) { + const con2syl = { + ㄱ: '가'.charCodeAt(0), + ㄲ: '까'.charCodeAt(0), + ㄴ: '나'.charCodeAt(0), + ㄷ: '다'.charCodeAt(0), + ㄸ: '따'.charCodeAt(0), + ㄹ: '라'.charCodeAt(0), + ㅁ: '마'.charCodeAt(0), + ㅂ: '바'.charCodeAt(0), + ㅃ: '빠'.charCodeAt(0), + ㅅ: '사'.charCodeAt(0), + } + const begin = + con2syl[ch] || + (ch.charCodeAt(0) - 12613) /* 'ㅅ'의 코드 */ * 588 + con2syl['ㅅ'] + const end = begin + 587 + return `[${ch}\\u${begin.toString(16)}-\\u${end.toString(16)}]` + } + + // 그 외엔 그대로 내보냄 (other than korean) + return escapeRegex(ch) +} + +export default function createMatcher(input) { + const pattern = input + .split('') + .map(ch2pattern) + .map(pattern => '(' + pattern + ')') + .join('') + + return new RegExp(pattern + '.*', 'i') +} diff --git a/src/utils/createMatcher.spec.js b/src/utils/createMatcher.spec.js new file mode 100644 index 00000000..5929a656 --- /dev/null +++ b/src/utils/createMatcher.spec.js @@ -0,0 +1,23 @@ +import createMatcher from './createMatcher' + +describe('#createMatcher', () => { + it('should match substring including input value', () => { + const enRegex = createMatcher('je') + expect(enRegex.test('Jesse Pinkman')).toEqual(true) + expect(enRegex.test('Pinkman')).toEqual(false) + + const enRegex2 = createMatcher('pi') + expect('Jesse Pinkman'.match(enRegex2).index).toEqual(6) + expect('Pinkman'.match(enRegex2).index).toEqual(0) + }) + + it('should match substring including Korean consonants', () => { + const koRegex = createMatcher('ㅅㄷ') + expect(koRegex.test('성덕선')).toEqual(true) + expect(koRegex.test('덕선')).toEqual(false) + + const enRegex2 = createMatcher('ㄷㅅ') + expect('성덕선'.match(enRegex2).index).toEqual(1) + expect('덕선'.match(enRegex2).index).toEqual(0) + }) +}) diff --git a/src/utils/getSubstringIndex.js b/src/utils/getSubstringIndex.js index 069d7b61..1ce4685f 100644 --- a/src/utils/getSubstringIndex.js +++ b/src/utils/getSubstringIndex.js @@ -1,3 +1,4 @@ +import createMatcher from './createMatcher' import lettersDiacritics from './diacritics' const removeAccents = str => { @@ -16,11 +17,13 @@ const removeAccents = str => { export const normalizeString = str => removeAccents(str).toLowerCase() const getSubstringIndex = (str, substr, ignoreAccents) => { - if (!ignoreAccents) { - return str.toLowerCase().indexOf(substr.toLowerCase()) - } + const display = ignoreAccents ? normalizeString(str) : str + const query = ignoreAccents ? normalizeString(substr) : substr - return normalizeString(str).indexOf(normalizeString(substr)) + const regex = createMatcher(query) + const match = display.match(regex) + + return match ? match.index : -1 } export default getSubstringIndex