verb-master

web application to practice verb conjugations for the korean language
git clone git://brookjeynes.dev/bjeynes/verb-master.git
Log | Files | Refs | README | LICENSE

hangul.js (5738B)


      1 // SPDX-License-Identifier: AGPL-3.0-or-later
      2 // Copyright (C) 2009 Dan Bravender
      3 // Modifications Copyright (c) 2026 Brook Jeynes <me@brookjeynes.dev>
      4 /* Many of the calculations used within this file can be referenced back to
      5  * http://www.kfunigraz.ac.at/~katzer/korean_hangul_unicode.html
      6  *
      7  * Unfortunately, this site is now dead. I've gone ahead and archived a version
      8  * of it at ./resources/korean_hangul_syllabary_in_unicode_archive.html for
      9  * future reference.
     10 */
     11 export const hangulUnicodeRange = {
     12     lower: 0xAC00,
     13     upper: 0xD7A3,
     14 };
     15 const hangulTailUnicodeOffset = 0x11A8;
     16 const hangulVowelUnicodeOffset = 0x314f;
     17 const hangulLeadUnicodeOffset = 0x1100;
     18 const hangulEmptyConsonant = 0x11A7;
     19 /* Used to track modifications that have been made to characters. It keeps
     20  * track of characters' original padchims (for ㄷ -> ㄹ irregulars) and if the
     21  * character has no padchim but should be treated as if it does (for ㅅ
     22  * irregulars). When substrings are extracted the Geulja class keeps these
     23  * markers for the last character only.
     24  */
     25 export class Geulja {
     26     constructor(value) {
     27         this.value = "";
     28         this.length = 0;
     29         this.hidden_padchim = false;
     30         this.original_padchim = null;
     31         this.value = value;
     32         this.length = value.length;
     33         return new Proxy(this, {
     34             get: (obj, key) => {
     35                 if (typeof (key) === "string" && (Number.isInteger(Number(key)))) {
     36                     const index = Number(key);
     37                     if (index < 0 || index >= obj.value.length) {
     38                         return undefined;
     39                     }
     40                     return obj.getItem(index);
     41                 }
     42                 else {
     43                     return obj[key];
     44                 }
     45             },
     46         });
     47     }
     48     toString() {
     49         return this.value;
     50     }
     51     slice(start, end) {
     52         return this.value.slice(start, end);
     53     }
     54     substring(start, end) {
     55         return this.value.substring(start, end);
     56     }
     57     split(separator, limit) {
     58         return this.value.split(separator, limit);
     59     }
     60     endsWith(searchString) {
     61         return this.value.endsWith(searchString);
     62     }
     63     startsWith(searchString, position) {
     64         return this.value.startsWith(searchString, position);
     65     }
     66     getItem(index) {
     67         const geulja = new Geulja(this.value.charAt(index));
     68         // Only keep the hidden padchim marker for the last item.
     69         if (index === this.value.length - 1) {
     70             geulja.hidden_padchim = this.hidden_padchim;
     71             geulja.original_padchim = this.original_padchim;
     72         }
     73         return geulja;
     74     }
     75 }
     76 export function isHangul(character) {
     77     if (character.length !== 1) {
     78         throw new Error("isHangeul only checks characters with a length of 1");
     79     }
     80     const code = character.charCodeAt(0);
     81     return code >= hangulUnicodeRange.lower && code <= hangulUnicodeRange.upper;
     82 }
     83 export function findVowelToAppend(geulja) {
     84     const reversed = [...`${geulja}`].reverse();
     85     for (const char of reversed) {
     86         if (["뜨", "쓰", "트"].includes(char)) {
     87             return "어";
     88         }
     89         if (getVowel(char) === "ㅡ" && !getPadchim(char)) {
     90             continue;
     91         }
     92         if (["ㅗ", "ㅏ", "ㅑ"].includes(getVowel(char))) {
     93             return "아";
     94         }
     95         else {
     96             return "어";
     97         }
     98     }
     99     return "어";
    100 }
    101 /**
    102  * Assembly a set of Jamo characters.
    103  */
    104 export function join(lead, vowel, padchim = null) {
    105     const lead_offset = lead.charCodeAt(0) - hangulLeadUnicodeOffset;
    106     const vowel_offset = vowel.charCodeAt(0) - hangulVowelUnicodeOffset;
    107     const padchim_offset = padchim ? padchim.charCodeAt(0) - hangulTailUnicodeOffset : -1;
    108     return String.fromCharCode(padchim_offset + vowel_offset * 28 + lead_offset * 588 + hangulUnicodeRange.lower + 1);
    109 }
    110 export function getLead(geulja) {
    111     const character_code = `${geulja}`.charCodeAt(0);
    112     const relative_lead_code = Math.floor((character_code - hangulUnicodeRange.lower) / 588);
    113     const lead_code = (relative_lead_code + hangulLeadUnicodeOffset);
    114     return String.fromCharCode(lead_code);
    115 }
    116 export function getVowel(geulja) {
    117     const padchim = getPadchim(geulja);
    118     const padchim_offset = padchim === null ? -1 : padchim.charCodeAt(0) - hangulTailUnicodeOffset;
    119     const character_code = `${geulja}`.charCodeAt(0);
    120     const relative_vowel_code = Math.floor(((character_code - hangulUnicodeRange.lower - padchim_offset) % 588) / 28);
    121     const vowel_code = (relative_vowel_code + hangulVowelUnicodeOffset);
    122     const vowel = String.fromCharCode(vowel_code);
    123     return vowel;
    124 }
    125 export function hasHiddenPadchim(geulja) {
    126     if (geulja instanceof Geulja) {
    127         if (geulja.hidden_padchim)
    128             return true;
    129     }
    130     return false;
    131 }
    132 export function getPadchim(geulja) {
    133     if (geulja instanceof Geulja) {
    134         if (geulja.original_padchim)
    135             return geulja.original_padchim;
    136     }
    137     const character_code = `${geulja}`.charCodeAt(0);
    138     const relative_tail_code = (character_code - hangulUnicodeRange.lower) % 28;
    139     const tail_code = (relative_tail_code + hangulTailUnicodeOffset) - 1;
    140     const padchim = String.fromCharCode(tail_code);
    141     return tail_code === hangulEmptyConsonant ? null : padchim;
    142 }
    143 export function matchGeulja(geulja, lead = undefined, vowel = undefined, padchim = undefined) {
    144     const matchesLead = lead !== undefined ? getLead(geulja) === lead : true;
    145     const matchesVowel = vowel !== undefined ? getVowel(geulja) === vowel : true;
    146     const matchesPadchim = padchim !== undefined ? (hasHiddenPadchim(geulja) || getPadchim(geulja)) === padchim : true;
    147     return matchesLead && matchesVowel && matchesPadchim;
    148 }