UNPKG

fulltext-search-kit

Version:

A utility library for full-text search in TypeScript

142 lines (141 loc) 4.43 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.fullTextSearch = fullTextSearch; exports.getFullTextSearchHighlights = getFullTextSearchHighlights; function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function isKorean(char) { return /[가-힣ㄱ-ㅎㅏ-ㅣ]/.test(char); } function getInitialsListWithSpaces(text) { const INITIALS = [ 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', ]; const result = []; const positions = []; let offset = 0; for (const char of text) { const code = char.charCodeAt(0); if (code >= 0xac00 && code <= 0xd7a3) { const index = Math.floor((code - 0xac00) / 588); result.push(INITIALS[index]); positions.push(offset); } else if (/^[ㄱ-ㅎ]$/.test(char)) { result.push(char); positions.push(offset); } offset += char.length; } return { list: result, positions }; } function convertToSearchableText(text) { return text .replace(/\s/g, '') // remove spaces .normalize('NFC'); // normalize Korean } function matchKeyword(text, keyword) { const normalizedText = convertToSearchableText(text); const normalizedKeyword = convertToSearchableText(keyword); // 기본 완성형 비교 if (normalizedText.includes(normalizedKeyword)) { return true; } // 초성 fallback const initials = getInitialsListWithSpaces(normalizedText).list; const keywordChars = Array.from(normalizedKeyword); let idx = 0; for (const char of keywordChars) { if (/^[ㄱ-ㅎ]$/.test(char)) { if (initials[idx] !== char) return false; } else { if (normalizedText[idx] !== char) return false; } idx++; } return true; } function highlightText(text, keyword) { const normalizedText = convertToSearchableText(text); const normalizedKeyword = convertToSearchableText(keyword); const baseMatch = normalizedText.indexOf(normalizedKeyword); if (baseMatch !== -1) { return (text.slice(0, baseMatch) + `<mark>${text.slice(baseMatch, baseMatch + keyword.length)}</mark>` + text.slice(baseMatch + keyword.length)); } // 초성 fallback highlight const { list: initials, positions } = getInitialsListWithSpaces(normalizedText); const keywordChars = Array.from(normalizedKeyword); let matched = true; for (let i = 0; i < keywordChars.length; i++) { const k = keywordChars[i]; if (/^[ㄱ-ㅎ]$/.test(k)) { if (initials[i] !== k) { matched = false; break; } } else { if (normalizedText[i] !== k) { matched = false; break; } } } if (!matched) return text; const start = positions[0]; const end = positions[keywordChars.length - 1] + 1; return (text.slice(0, start) + `<mark>${text.slice(start, end)}</mark>` + text.slice(end)); } function fullTextSearch({ data, searchRequirement, }) { if (!data || !(searchRequirement === null || searchRequirement === void 0 ? void 0 : searchRequirement.length)) return data; return data.filter((item) => searchRequirement.every(({ value, keywords }) => { const rawValue = item[value]; if (!rawValue || !keywords.length) return true; return keywords.every((keyword) => matchKeyword(String(rawValue), keyword)); })); } function getFullTextSearchHighlights({ item, searchRequirement, }) { const highlights = {}; for (const { value, keywords } of searchRequirement) { const rawValue = item[value]; if (!rawValue || !keywords.length) continue; const originalText = String(rawValue); for (const keyword of keywords) { if (matchKeyword(originalText, keyword)) { highlights[value] = highlightText(originalText, keyword); break; } } } return highlights; }