Compare commits

...

13 Commits

Author SHA1 Message Date
Richard Wong a279ff0cf3 Fix: fixed VerseValidator border-top dark-mode color 2025-05-13 17:24:42 +08:00
Richard Wong f34d9fc7ef Feat: added ordering display, and reset button 2025-05-13 17:18:39 +08:00
Richard Wong 8061ba5b11 Fix: fixed dep6-part4 errata 2025-05-13 15:42:30 +08:00
Richard Wong c0675ead58
Feat: added a README.md 2025-04-15 19:21:33 +08:00
Richard Wong 8f45d5d0c9
Fix: errata in verse.json 2025-04-15 19:11:00 +08:00
Richard Wong c75ff3128c
Fix: removed darkmode function
Fix: slightly darken diff color for visibility
2024-08-31 00:14:44 +09:00
Richard Wong 033b447eeb
Feat: added field-sizing: content attribute for dynamic size textarea
Feat: changed color for diffs
Content: added up to DEP8 for english
2024-08-31 00:05:34 +09:00
Richard Wong 5ad4720e47
Content: added up to DEP4 for korean verses 2024-08-29 16:20:26 +09:00
Richard Wong 4513592483
Fix: added hangul jamo decomposition to handle ios broken IME 2024-08-29 15:25:39 +09:00
Richard Wong faf83b4827
Feat: use composition event to stabilize korean input 2024-08-27 17:54:15 +09:00
Richard Wong ab75ebbef7
Content: added up to DEP2 for korean verses 2024-08-27 16:25:00 +09:00
Richard Wong a5acd09642
Feat: add partial testing 2024-08-27 14:44:18 +09:00
Richard Wong 965d985251
Fix: reset checklist when changing language 2024-08-20 13:35:13 +09:00
9 changed files with 3312 additions and 190 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
data_files/
# Editor directories and files
.vscode/*

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# Verse Checker
This is a simple web-application for checking verses.
You can try the application at: https://scripturememory.richardwong.io/
Features include:
- Languages supported: English, Korean
- Shuffling with random selection from selected packs
- Hide References
- Showing mistakes as deltas with respect to the answer
## To Use
This application uses `pnpm` as the node.js package manager.
```bash
git clone https://github.com/RichFree/VerseChecker.git
cd VerseChecker
pnpm run dev
```

View File

@ -37,11 +37,11 @@
},
{
"value": "dep-1",
"label": "DEP 1"
"label": "DEP 1: Assurance of Salvation"
},
{
"value": "dep-2",
"label": "DEP 2",
"label": "DEP 2: Quiet Time",
"children": [
{ "value": "dep-2-part-a", "label": "Why do we have Quiet Time?" },
{ "value": "dep-2-part-b", "label": "What is Quiet Time?" },
@ -50,7 +50,7 @@
},
{
"value": "dep-3",
"label": "DEP 3",
"label": "DEP 3: The Word",
"children": [
{ "value": "dep-3-part-a", "label": "Authority of the Word" },
{ "value": "dep-3-part-b", "label": "value of the Word" },
@ -60,7 +60,7 @@
},
{
"value": "dep-4",
"label": "DEP 4",
"label": "DEP 4: Prayer",
"children": [
{ "value": "dep-4-part-a", "label": "Command of Prayer" },
{ "value": "dep-4-part-b", "label": "Promises and Blessings of Prayer" },
@ -71,7 +71,7 @@
},
{
"value": "dep-5",
"label": "DEP 5",
"label": "DEP 5: Fellowship",
"children": [
{ "value": "dep-5-part-a", "label": "Foundation of Christian Fellowship" },
{ "value": "dep-5-part-b", "label": "Importance of fellowship" },
@ -82,12 +82,28 @@
},
{
"value":"dep-6",
"label":"DEP 6",
"label":"DEP 6: Witnessing",
"children": [
{ "value": "dep-6-part-a", "label": "Who is respondible for witnessing?" },
{ "value": "dep-6-part-a", "label": "Who is responsible for witnessing?" },
{ "value": "dep-6-part-b", "label": "Why should we witness?" },
{ "value": "dep-6-part-c", "label": "How do we witness?" }
{ "value": "dep-6-part-c", "label": "How do we witness?" },
{ "value": "dep-6-part-d", "label": "Examples of witness" },
{ "value": "dep-6-part-e", "label": "Bridge Illustration" }
]
},
{
"value":"dep-7",
"label":"DEP 7: The Lordship of Christ",
"children": [
{ "value": "dep-7-part-a", "label": "We must believe in the Lordship of Christ" },
{ "value": "dep-7-part-b", "label": "Blessings when surrendering to the Lordship" },
{ "value": "dep-7-part-c", "label": "What to surrender in the Lordship" },
{ "value": "dep-7-part-d", "label": "Paragons of surrendering to the Lordship" }
]
},
{
"value": "dep-8",
"label": "DEP 8: World Vision"
}
]
}

View File

@ -14,15 +14,100 @@
"verses": "Verses:"
},
"verse_validator": {
"input_reference": "Input Verse Reference: (KR)",
"input_chapter_title": "Input Chapter Title: (KR)",
"input_title": "Input Title: (KR)",
"input_verse": "Input Verse: (KR)"
"input_reference": "Input Verse Reference: ",
"input_chapter_title": "Input Chapter Title: ",
"input_title": "Input Title: ",
"input_verse": "Input Verse: "
},
"nodes": [
{
"value": "loa",
"label": "Lessons on Assurance"
"label": "5확신"
},
{
"value": "8구절",
"label": "8구절"
},
{
"value": "tms60",
"label": "60구절",
"children": [
{ "value": "tms-60-pack-a", "label": "새로운 삶" },
{ "value": "tms-60-pack-b", "label": "그리스도를 전파함" },
{ "value": "tms-60-pack-c", "label": "하나님을 의뢰함" },
{ "value": "tms-60-pack-d", "label": "그리스도 제자의 자격" },
{ "value": "tms-60-pack-e", "label": "그리스도를 닮아감" }
]
},
{
"value": "dep-1",
"label": "DEP 1"
},
{
"value": "dep-2",
"label": "DEP 2",
"children": [
{ "value": "dep-2-part-a", "label": "왜 QT를 가져야 하는가?" },
{ "value": "dep-2-part-b", "label": "QT란 무엇인가?" },
{ "value": "dep-2-part-c", "label": "QT의 본" }
]
},
{
"value": "dep-3",
"label": "DEP 3",
"children": [
{ "value": "dep-3-part-a", "label": "말씀의 권위" },
{ "value": "dep-3-part-b", "label": "말씀의 가치" },
{ "value": "dep-3-part-c", "label": "말씀에 대한 태도" },
{ "value": "dep-3-part-d", "label": "말씀의 섭취 방법 말씀의 손 예화" }
]
},
{
"value": "dep-4",
"label": "DEP 4",
"children": [
{ "value": "dep-4-part-a", "label": "기도의 명령" },
{ "value": "dep-4-part-b", "label": "기도의 약속과 축복" },
{ "value": "dep-4-part-c", "label": "응답받는 기도의 조건" },
{ "value": "dep-4-part-d", "label": "기도의 본" },
{ "value": "dep-4-part-e", "label": "기도의 손 예화" }
]
},
{
"value": "dep-5",
"label": "DEP 5",
"children": [
{ "value": "dep-5-part-a", "label": "교제의 기초" },
{ "value": "dep-5-part-b", "label": "교제의 중요성" },
{ "value": "dep-5-part-c", "label": "교제의 요소" },
{ "value": "dep-5-part-d", "label": "교제의 태도" },
{ "value": "dep-5-part-e", "label": "교제에서의 문제 해결" }
]
},
{
"value":"dep-6",
"label":"DEP 6",
"children": [
{ "value": "dep-6-part-a", "label": "전도는 누구의 책임인가?" },
{ "value": "dep-6-part-b", "label": "왜 전도를 해야 하나?" },
{ "value": "dep-6-part-c", "label": "어떻게 전도하나?" },
{ "value": "dep-6-part-d", "label": "전도의 모범" },
{ "value": "dep-6-part-e", "label": "Bridge Illustration" }
]
},
{
"value":"dep-7",
"label":"DEP 7",
"children": [
{ "value": "dep-7-part-a", "label": "주재권을 인정해야 함" },
{ "value": "dep-7-part-b", "label": "주재권을 인정할 때의 축복" },
{ "value": "dep-7-part-c", "label": "주재권을 인정할 영역" },
{ "value": "dep-7-part-d", "label": "주재권을 인정한 삶의 모범" }
]
},
{
"value": "dep-8",
"label": "DEP 8"
}
]
}

View File

@ -31,13 +31,14 @@ const GenerateTestList = ({ VerseData, packs, testCount, toShuffle, toHideRefere
}
const ArrayTester = ({ array, toHideReference, translate}) => {
const list = array.map((element) => (
const list = array.map((element, index) => (
// key needs to be unique; chose 3 elements that will separate all elements
<VerseValidator
key={element.pack + element.title + element.reference}
element={element}
toHideReference={toHideReference}
t={translate} // this passes the t i18 object to the function
index={index + 1}
/>
))
return list
@ -104,6 +105,10 @@ function Page() {
// function hook to change language
// updates both i18n language and also the VerseData state variable
const changeLanguage = (lng) => {
// reset selection list
setChecked([]);
setExpanded([]);
// i18n.changeLanguage is async, so we should wait until its done to avoid
// race conditions
// console.log("change language");
@ -113,25 +118,9 @@ function Page() {
};
// // create checklist array for pack selection
// const packList = Object.keys(VerseData);
// // return a list of packObj's
// // 1. packObj.pack for the pack name
// // 2. packObj.include for whether to include the pack
// const packObjList = packList.map((element) => {
// // create object for each element in VerseData key list
// const packObj = new Object();
// packObj.pack = element;
// packObj.include = false;
// return packObj
// }
// )
// const [packs, setPacks] = useState(packObjList)
// initialize state variable testCount
// purpose: to set number of verses to test
const [testCount, setTestCount] = useState(20)
const [testCount, setTestCount] = useState(30)
const testCountChange = (e) => {
const value = e.target.value
setTestCount(value)
@ -252,7 +241,12 @@ function Page() {
// loading component for suspense fallback
const Loader = () => (
<div className="App">
<img src={logo} className="App-logo" alt="logo" />
<img
src={logo}
className="App-logo"
alt="logo"
style={{ width: '20vw', height: 'auto' }}
/>
<div>loading...</div>
</div>
);

View File

@ -12,6 +12,7 @@
.reference-box {
max-width:400px;
height: 5vh;
field-sizing: content; /* not yet implemented in firefox and safari */
display: block;
width: 99%;
border: 1px solid grey;
@ -22,6 +23,7 @@
.chapter-title-box {
max-width:400px;
height: 5vh;
field-sizing: content; /* not yet implemented in firefox and safari */
display: block;
width: 99%;
border: 1px solid grey;
@ -32,6 +34,7 @@
.title-box {
max-width: 400px;
height: 5vh;
field-sizing: content; /* not yet implemented in firefox and safari */
display: block;
width: 99%;
border: 1px solid grey;
@ -46,7 +49,8 @@
.verse-box {
max-width:400px;
height: 10vh;
min-height: 12vh;
field-sizing: content; /* not yet implemented in firefox and safari */
display: block;
width: 99%;
border: 1px solid grey;
@ -73,11 +77,43 @@
@media (prefers-color-scheme: light) {
.correct {
background-color: #e6ffe6; /* Change the background color as needed */
}
.partial {
background-color: #dafcff; /* Change the background color as needed */
}
.incorrect {
background-color: transparent; /* Change the background color as needed */
}
:root {
--background-color-removed: #f1ebb3;
--background-color-added: #ffd7b6;
}
}
@media (prefers-color-scheme: dark) {
.correct {
background-color: #495749; /* Change the background color as needed */
background-color: #2e5e2e; /* Change the background color as needed */
}
.partial {
background-color: #004d5c; /* Change the background color as needed */
}
.incorrect {
background-color: transparent; /* Change the background color as needed */
}
:root {
--background-color-removed: #877e2a;
--background-color-added: #7b4418;
}
.VerseValidator {
border-top: 1px solid white; /* override for dark mode */
}
}

View File

@ -1,25 +1,76 @@
import { useState } from "react";
import "./VerseValidator.css";
import { StringDiff } from "react-string-diff";
import { containsKorean, jamoSubstringMatch } from './utils';
const STATE = {
INCORRECT: 0,
PARTIAL: 1,
CORRECT: 2,
};
// function to render and handle logic of each of the cells
const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse } , toHideReference, t}) => { // useful use of destructuring here
const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse } , toHideReference, t, index}) => { // useful use of destructuring here
const [inputReference, setReference] = useState('')
const [referenceBool, setReferenceBool] = useState(false)
const [referenceBool, setReferenceBool] = useState(STATE.INCORRECT)
const [inputChapterTitle, setChapterTitle] = useState('')
const [chapterTitleBool, setChapterTitleBool] = useState(false)
const [chapterTitleBool, setChapterTitleBool] = useState(STATE.INCORRECT)
const [inputTitle, setTitle] = useState('')
const [titleBool, setTitleBool] = useState(false)
const [titleBool, setTitleBool] = useState(STATE.INCORRECT)
const [inputVerse, setVerse] = useState('')
const [verseBool, setVerseBool] = useState(false)
const [verseBool, setVerseBool] = useState(STATE.INCORRECT)
const[hintBool, setHintBool] = useState(false)
const[diffBool, setDiffBool] = useState(false)
const [isComposing, setIsComposing] = useState(false);
// handle reset
const handleReset = () => {
setReference('');
setReferenceBool(STATE.INCORRECT);
setChapterTitle('');
setChapterTitleBool(STATE.INCORRECT);
setTitle('');
setTitleBool(STATE.INCORRECT);
setVerse('');
setVerseBool(STATE.INCORRECT);
setDiffBool(false); // optionally hide answer again
};
// function to check correctness of verse input
// so far only perform checking on full spelling of reference names
// Handle the start of composition
const handleCompositionStart = () => {
setIsComposing(true);
};
function resultChecker(string1, string2) {
var result = STATE.INCORRECT; // init
// contains korean
if (containsKorean(string1)) {
if (string1 === string2) {
result = STATE.CORRECT;
} else if (jamoSubstringMatch(string2, string1) & string1 !== "") {
result = STATE.PARTIAL;
} else {
result = STATE.INCORRECT;
}
} else { // does not contain korean
if (string1 === string2) {
result = STATE.CORRECT;
} else if (string2.startsWith(string1) & string1 !== "") {
result = STATE.PARTIAL;
} else {
result = STATE.INCORRECT;
}
}
return result;
}
// function to check correctness of reference input
const referenceChange = (e) => {
const value = e.target.value;
const string1 = String(value)
@ -30,33 +81,21 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
.replace(/\s+/g, "")
.toLowerCase()
.normalize("NFC");
const bool = (string1 === string2);
const result = resultChecker(string1, string2);
setReference(value);
setReferenceBool(bool);
setReferenceBool(result);
};
{/* function to check correctness of verse input */}
const verseChange = (e) => {
const value = e.target.value;
let string1 = value;
let string2 = verse;
string1 = String(string1)
.replace(/[\p{P}\p{S}]/gu, "")
.replace(/\s+/g, "")
.toLowerCase()
.normalize("NFC");
string2 = String(string2)
.replace(/[\p{P}\p{S}]/gu, "")
.replace(/\s+/g, "")
.toLowerCase()
.normalize("NFC");
const referenceClassName = `reference-box${
referenceBool === STATE.CORRECT ? " correct" :
referenceBool === STATE.PARTIAL ? " partial" :
" incorrect"
}`;
const bool = string1 === string2;
setVerse(value);
setVerseBool(bool);
};
{/* function to check correctness of title input */}
const titleChange = (e) => {
@ -75,11 +114,19 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
.toLowerCase()
.normalize("NFC");
const bool = string1 === string2;
const result = resultChecker(string1, string2);
setTitle(value);
setTitleBool(bool);
setTitleBool(result);
};
const titleClassName = `chapter-title-box${
titleBool=== STATE.CORRECT ? " correct" :
titleBool === STATE.PARTIAL ? " partial" :
" incorrect"
}`;
{/* function to check correctness of chapter title input */}
@ -101,26 +148,61 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
.toLowerCase()
.normalize("NFC");
const bool = string1 === string2;
const result = resultChecker(string1, string2);
setChapterTitle(value);
setChapterTitleBool(bool);
setChapterTitleBool(result);
};
const chapterTitleClassName = `title-box${
chapterTitleBool=== STATE.CORRECT ? " correct" :
chapterTitleBool === STATE.PARTIAL ? " partial" :
" incorrect"
}`;
// check verse input
const verseChange = (e) => {
const value = e.target.value;
let string1 = value;
let string2 = verse;
string1 = String(string1)
.replace(/[\p{P}\p{S}]/gu, "")
.replace(/\s+/g, "")
.toLowerCase()
.normalize("NFC");
string2 = String(string2)
.replace(/[\p{P}\p{S}]/gu, "")
.replace(/\s+/g, "")
.toLowerCase()
.normalize("NFC");
const result = resultChecker(string1, string2);
setVerse(value);
setVerseBool(result);
};
const DiffViewer = ({oldValue, newValue}) => {
const string1 = String(oldValue)
.replace(/[\p{P}\p{S}]/gu, "")
.toLowerCase()
.normalize("NFC");
const verseClassName = `verse-box${
verseBool === STATE.CORRECT ? " correct" :
verseBool === STATE.PARTIAL ? " partial" :
" incorrect"
}`;
const string2 = String(newValue)
.replace(/[\p{P}\p{S}]/gu, "")
.toLowerCase()
.normalize("NFC");
// const DiffViewer = ({oldValue, newValue}) => {
// const string1 = String(oldValue)
// .replace(/[\p{P}\p{S}]/gu, "")
// .toLowerCase()
// .normalize("NFC");
return (<StringDiff oldValue={string1} newValue={string2} diffMethod="diffWords" />)
}
// const string2 = String(newValue)
// .replace(/[\p{P}\p{S}]/gu, "")
// .toLowerCase()
// .normalize("NFC");
// return (<StringDiff oldValue={string1} newValue={string2} diffMethod="diffWords" />)
// }
const DiffViewerStrict = ({oldValue, newValue}) => {
const string1 = String(oldValue)
@ -131,12 +213,33 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
.toLowerCase()
.normalize("NFC");
return (<StringDiff oldValue={string1} newValue={string2} diffMethod="diffWords" />)
let diffStyle = {
added: {
backgroundColor: 'var(--background-color-added)'
},
removed: {
backgroundColor: 'var(--background-color-removed)'
},
default: {}
};
return (<StringDiff
oldValue={string1}
newValue={string2}
diffMethod="diffWords"
styles={diffStyle}
/>)
}
return (
<div className="VerseValidator">
<div className="verse-number">
<h3>Verse {index}</h3>
</div>
{/* toggle hiding reference */}
{toHideReference ? (
<div>
@ -144,11 +247,17 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{t('verse_validator.input_reference')}
</label>
<textarea
className={`reference-box${referenceBool ? " correct" : ""}`}
className={referenceClassName}
type="text"
id="referenceBox"
name="referenceBox"
onChange={referenceChange}
value={inputReference}
onChange={(event) => {
if (!isComposing) {
referenceChange(event);
}
}}
/>
</div>
) : (
@ -164,11 +273,22 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{t('verse_validator.input_chapter_title')}
</label>
<textarea
className={`chapter-title-box${chapterTitleBool ? " correct" : ""}`}
className={chapterTitleClassName}
type="text"
id="chapterTitleBox"
name="chapterTitleBox"
onChange={chapterTitleChange}
value={inputChapterTitle}
onChange={(event) => {
if (!isComposing) {
chapterTitleChange(event);
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={(event) => {
setIsComposing(false);
chapterTitleChange(event);
}}
/>
</div>
)}
@ -178,11 +298,22 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{t('verse_validator.input_title')}
</label>
<textarea
className={`title-box${titleBool ? " correct" : ""}`}
className={titleClassName}
type="text"
id="titleBox"
name="titleBox"
onChange={titleChange}
value={inputTitle}
onChange={(event) => {
if (!isComposing) {
titleChange(event);
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={(event) => {
setIsComposing(false);
titleChange(event);
}}
/>
{/* input box for verse */}
@ -190,17 +321,30 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{t('verse_validator.input_verse')}
</label>
<textarea
className={`verse-box${verseBool ? " correct" : ""}`}
className={verseClassName}
type="text"
id="verseBox"
name="verseBox"
onChange={verseChange}
value={inputVerse}
onChange={(event) => {
if (!isComposing) {
verseChange(event);
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={(event) => {
setIsComposing(false);
verseChange(event);
}}
/>
{/* button to toggle show answer*/}
<div className="answer-button-box">
{/* <button onClick={() => setHintBool(!hintBool)}>Show Answer:</button> */}
<button onClick={() => setDiffBool(!diffBool)}>Show Answer:</button>
<button onClick={handleReset}>Reset</button>
</div>
{/* This shows the difference between given and input answers*/}
@ -221,7 +365,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
{chapterTitle && (
<div>
ChapterTitle:
<DiffViewer
<DiffViewerStrict
oldValue={chapterTitle}
newValue={inputChapterTitle}
/>
@ -231,7 +375,7 @@ const VerseValidator = ({ element: { pack, title, chapterTitle, reference, verse
<p></p>
<div>
Title:
<DiffViewer
<DiffViewerStrict
oldValue={title}
newValue={inputTitle}
/>

File diff suppressed because it is too large Load Diff

86
src/utils.js Normal file
View File

@ -0,0 +1,86 @@
// jamo utils
const BASE_CODE = 44032;
const CHO = 588;
const JUNG = 28;
const cho = ["ㄱ", "ㄲ", "ㄴ", "ㄷ", "ㄸ", "ㄹ", "ㅁ", "ㅂ", "ㅃ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅉ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
const jung = ["ㅏ", "ㅐ", "ㅑ", "ㅒ", "ㅓ", "ㅔ", "ㅕ", "ㅖ", "ㅗ", "ㅘ", "ㅙ", "ㅚ", "ㅛ", "ㅜ", "ㅝ", "ㅞ", "ㅟ", "ㅠ", "ㅡ", "ㅢ", "ㅣ"];
const jong = ["", "ㄱ", "ㄲ", "ㄳ", "ㄴ", "ㄵ", "ㄶ", "ㄷ", "ㄹ", "ㄺ", "ㄻ", "ㄼ", "ㄽ", "ㄾ", "ㄿ", "ㅀ", "ㅁ", "ㅂ", "ㅄ", "ㅅ", "ㅆ", "ㅇ", "ㅈ", "ㅊ", "ㅋ", "ㅌ", "ㅍ", "ㅎ"];
// Decompose the medial vowel into individual components, if necessary
const jungSplit = {
"ㅘ": ["ㅗ", "ㅏ"],
"ㅙ": ["ㅗ", "ㅐ"],
"ㅚ": ["ㅗ", "ㅣ"],
"ㅝ": ["ㅜ", "ㅓ"],
"ㅞ": ["ㅜ", "ㅔ"],
"ㅟ": ["ㅜ", "ㅣ"],
"ㅢ": ["ㅡ", "ㅣ"],
};
// decompose ending consonents
const jongSplit = {
"ㄲ": ["ㄱ", "ㄱ"],
"ㄳ": ["ㄱ", "ㅅ"],
"ㄵ": ["ㄴ", "ㅈ"],
"ㄶ": ["ㄴ", "ㅎ"],
"ㄺ": ["ㄹ", "ㄱ"],
"ㄻ": ["ㄹ", "ㅁ"],
"ㄼ": ["ㄹ", "ㅂ"],
"ㄽ": ["ㄹ", "ㅅ"],
"ㄾ": ["ㄹ", "ㅌ"],
"ㄿ": ["ㄹ", "ㅍ"],
"ㅀ": ["ㄹ", "ㅎ"],
"ㅄ": ["ㅂ", "ㅅ"],
"ㅆ": ["ㅅ", "ㅅ"],
}
export function decomposeHangul(character) {
const code = character.charCodeAt(0) - BASE_CODE;
if (code < 0 || code > 11171) {
// Return character as is if it's not a Hangul syllable
return character;
}
const choIndex = Math.floor(code / CHO);
const jungIndex = Math.floor((code - (choIndex * CHO)) / JUNG);
const jongIndex = code % JUNG;
// Decompose cho, jung, and jong
const choJamo = cho[choIndex];
const jungJamo = jung[jungIndex];
const jongJamo = jong[jongIndex];
// Handle double jamo in jung
const jungComponents = jungSplit[jungJamo] || [jungJamo];
// handle double jamo in jong
const jongComponents = jongSplit[jongJamo] || [jongJamo];
return [choJamo, ...jungComponents, ...jongComponents].join('');
}
export function decomposeStringToJamo(inputString) {
return inputString.split('').map(decomposeHangul).join('');
}
export function jamoSubstringMatch(mainString, substring) {
const decomposedMain = decomposeStringToJamo(mainString)
.replace(/\s+/g, "");
const decomposedSub = decomposeStringToJamo(substring)
.replace(/\s+/g, "");
return decomposedMain.startsWith(decomposedSub);
}
// Unicode Ranges for Korean Characters:
// Hangul Syllables: \uAC00-\uD7A3
// Hangul Jamo: \u1100-\u11FF (for initial consonants, vowels, and final consonants)
// Hangul Compatibility Jamo: \u3130-\u318F
export function containsKorean(text) {
const koreanRegex = /[\u1100-\u11FF\u3130-\u318F\uAC00-\uD7A3]/;
return koreanRegex.test(text);
}