hangul-lube 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +33 -0
- package/package.json +4 -5
- package/src/erase_letter.js +52 -0
- package/src/get_letter_range.js +9 -3
- package/src/index.js +4 -1
- package/src/jamo.js +58 -0
- package/src/letters_of.js +1 -2
- package/src/particle.js +112 -0
- package/src/searcher.js +15 -5
- package/src/type_letter.js +103 -0
- package/types/erase_letter.d.ts +20 -0
- package/types/erase_letter.d.ts.map +1 -0
- package/types/get_letter_range.d.ts.map +1 -1
- package/types/index.d.ts +3 -0
- package/types/index.d.ts.map +1 -1
- package/types/jamo.d.ts +26 -0
- package/types/jamo.d.ts.map +1 -0
- package/types/letters_of.d.ts.map +1 -1
- package/types/particle.d.ts +22 -0
- package/types/particle.d.ts.map +1 -0
- package/types/searcher.d.ts.map +1 -1
- package/types/type_letter.d.ts +25 -0
- package/types/type_letter.d.ts.map +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
- **`typeLetter(text, letter)` and `eraseLetter(text)` type and erase one key of a two-set keyboard.**
|
|
6
|
+
`typeLetter` composes a compatibility jamo into the last character of `text`: a consonant and a
|
|
7
|
+
vowel make a syllable, two vowels join (`ㅗ` and `ㅏ` into `ㅘ`), a consonant becomes the final
|
|
8
|
+
and may join the final before it (`ㄹ` and `ㄱ` into `ㄺ`), and a vowel after a final moves it
|
|
9
|
+
to a new syllable (`값` and `ㅏ` give `갑사`). Two lone consonants join too (`ㄱ` and `ㅅ` into
|
|
10
|
+
`ㄳ`), and the same key twice never makes a double consonant. `eraseLetter` is one backspace
|
|
11
|
+
that undoes the last key (`갉` into `갈`, `과` into `고`, `가` into `ㄱ`).
|
|
12
|
+
- **`particle(word, notation)` gives the Korean particle that follows a word.** The notation is
|
|
13
|
+
`"은(는)"`, `"이(가)"`, `"을(를)"`, `"과(와)"`, `"아(야)"`, `"(이)"` or `"(으)"`, and any other
|
|
14
|
+
throws a `TypeError`. The form follows the final consonant of the last Hangul syllable, the
|
|
15
|
+
Korean reading of a number (`3` is 삼), the Korean names of the letters of a Latin capital word
|
|
16
|
+
(`URL` ends in ㄹ) or the spelling of a lower-case one, and marks at the end are passed over.
|
|
17
|
+
`"(으)"` gives `""` after ㄹ too, as `(으)로` needs: `"서울" + particle("서울", "(으)") + "로"` is `서울로`.
|
|
18
|
+
|
|
3
19
|
## 2.0.0
|
|
4
20
|
|
|
5
21
|
### Breaking
|
package/README.md
CHANGED
|
@@ -65,6 +65,39 @@ pattern("1+1") //=> "1\\+1"
|
|
|
65
65
|
|
|
66
66
|
`searcher(query)?.source` is the source a database can run case-insensitively: `~*` in PostgreSQL, `REGEXP_LIKE(column, searcher_source, 'i')` in MySQL or `$regex` with `$options: "i"` in MongoDB. Use it, not `pattern(query)`, wherever the search box is what the row must match: `pattern` keeps the white space and the invisible characters of the query and lets nothing stand between its letters, so `"도쿄하네다"` misses `"도쿄 하네다"` and a pasted byte order mark makes it match nothing. Such a query reads every row, since a regular expression cannot use an index.
|
|
67
67
|
|
|
68
|
+
### Particle
|
|
69
|
+
```ts
|
|
70
|
+
import { particle } from "hangul-lube"
|
|
71
|
+
|
|
72
|
+
"사과" + particle("사과", "이(가)") //=> "사과가"
|
|
73
|
+
"밤" + particle("밤", "은(는)") //=> "밤은"
|
|
74
|
+
"HP" + particle("HP", "을(를)") //=> "HP를"
|
|
75
|
+
"서울" + particle("서울", "(으)") + "로" //=> "서울로"
|
|
76
|
+
"3" + particle("3", "과(와)") //=> "3과"
|
|
77
|
+
particle("밤", "(으)") //=> "으"
|
|
78
|
+
```
|
|
79
|
+
- The notation is one of `"은(는)"`, `"이(가)"`, `"을(를)"`, `"과(와)"`, `"아(야)"`, `"(이)"` and `"(으)"`; any other throws a `TypeError`. `"(이)"` and `"(으)"` give the syllable that goes before the next word (`이에요`, `으로`) and `""` after a vowel; `"(으)"` gives `""` after ㄹ too.
|
|
80
|
+
- The final consonant comes from the last Hangul syllable. A number is read in Korean (`1` is 일, `3` is 삼, `10` is 십, `1,000` is 천), a Latin capital word by the Korean names of its letters (`URL` ends in ㄹ, `HP` in a vowel), and a lower-case word by its spelling (`l` is ㄹ, `m`, `n` and `ng` a consonant).
|
|
81
|
+
- Marks at the end, such as quotes and `!`, are passed over, and anything else reads as ending in a vowel.
|
|
82
|
+
|
|
83
|
+
### Typing automaton
|
|
84
|
+
```ts
|
|
85
|
+
import { eraseLetter, typeLetter } from "hangul-lube"
|
|
86
|
+
|
|
87
|
+
typeLetter("ㄱ", "ㅏ") //=> "가"
|
|
88
|
+
typeLetter("가", "ㄱ") //=> "각"
|
|
89
|
+
typeLetter("각", "ㅏ") //=> "가가"
|
|
90
|
+
typeLetter("갑", "ㅅ") //=> "값"
|
|
91
|
+
typeLetter("값", "ㅏ") //=> "갑사"
|
|
92
|
+
eraseLetter("값") //=> "갑"
|
|
93
|
+
eraseLetter("각") //=> "가"
|
|
94
|
+
eraseLetter("가") //=> "ㄱ"
|
|
95
|
+
```
|
|
96
|
+
- `typeLetter(text, letter)` types one key of a two-set (dubeolsik) keyboard into the last character of `text`. `letter` is a compatibility jamo (`ㄱ`, `ㅏ`, and `ㄲ`, `ㅒ` from the shift key); anything else is appended as it is.
|
|
97
|
+
- Two vowels join (`ㅗ` and `ㅏ` into `ㅘ`), a consonant after a syllable becomes its final and joins the final before it into a compound one (`ㄹ` and `ㄱ` into `ㄺ`), and a vowel after a final moves it to a new syllable, a compound final moving only its second consonant. Two lone consonants join the same way (`ㄱ` and `ㅅ` into `ㄳ`) and a vowel splits them again.
|
|
98
|
+
- The same key never makes a double consonant: `ㄱ` after `각` is appended, since `ㄲ` needs the shift key.
|
|
99
|
+
- `eraseLetter(text)` is one backspace: it takes back the last key, so a compound final loses its second consonant, a joined vowel splits back, and a syllable with a simple vowel and no final leaves its initial. Anything else loses its last code point, and `""` stays `""`.
|
|
100
|
+
|
|
68
101
|
### Typing rules
|
|
69
102
|
- A consonant matches itself and every syllable it starts, and the last syllable matches the syllables it grows into while typing: `된자` matches `된장찌개` and `도` matches `돼지국밥`. A lone letter grows the same way: `ㄱ` matches `ㄳ` and `ㅗ` matches `ㅘ`.
|
|
70
103
|
- Every earlier syllable is finished and matches only itself: `오리` matches `오리털` but not `올리브`, while `ㅇ리` and `ㅇㄹ` match both.
|
package/package.json
CHANGED
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
"url": "https://github.com/artxe/lube-series/issues"
|
|
5
5
|
},
|
|
6
6
|
"description": "Simplify Korean Consonant Search in JavaScript",
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@typescript/native": "npm:typescript@^7.0.2"
|
|
9
|
-
},
|
|
10
7
|
"engines": {
|
|
11
8
|
"node": ">=14"
|
|
12
9
|
},
|
|
@@ -27,8 +24,10 @@
|
|
|
27
24
|
"choseong",
|
|
28
25
|
"hangul",
|
|
29
26
|
"hangul-lube",
|
|
27
|
+
"josa",
|
|
30
28
|
"korean",
|
|
31
29
|
"lube",
|
|
30
|
+
"particle",
|
|
32
31
|
"regexp",
|
|
33
32
|
"search",
|
|
34
33
|
"syntax sugar",
|
|
@@ -45,10 +44,10 @@
|
|
|
45
44
|
"sideEffects": false,
|
|
46
45
|
"type": "module",
|
|
47
46
|
"types": "types/index.d.ts",
|
|
48
|
-
"version": "2.
|
|
47
|
+
"version": "2.1.0",
|
|
49
48
|
"scripts": {
|
|
50
49
|
"dts": "node ../../scripts/tsc_with_clean.js hangul-lube",
|
|
51
50
|
"lint": "pnpm -w lint",
|
|
52
|
-
"test": "pnpm -w exec vitest run
|
|
51
|
+
"test": "pnpm -w exec vitest run test/hangul-lube"
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import compound_consonant_letters from "./compound_consonant_letters.js"
|
|
2
|
+
import {
|
|
3
|
+
final_letters,
|
|
4
|
+
initial_letters,
|
|
5
|
+
medial_letters,
|
|
6
|
+
parts_of,
|
|
7
|
+
split_vowels,
|
|
8
|
+
syllable
|
|
9
|
+
} from "./jamo.js"
|
|
10
|
+
/**
|
|
11
|
+
* Takes back one key on a two-set (dubeolsik) Korean keyboard: returns the text after one
|
|
12
|
+
* backspace, undoing what `typeLetter` composed. A compound final loses its second consonant, a
|
|
13
|
+
* single final goes, a joined vowel splits back into its first vowel, a syllable with a simple
|
|
14
|
+
* vowel and no final leaves its initial consonant, and a lone joined vowel or compound consonant
|
|
15
|
+
* loses its second part. Anything else loses its last code point, so an emoji goes whole.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {string}
|
|
18
|
+
* @example eraseLetter("갉") //=> "갈"
|
|
19
|
+
*
|
|
20
|
+
* eraseLetter("각") //=> "가"
|
|
21
|
+
*
|
|
22
|
+
* eraseLetter("과") //=> "고"
|
|
23
|
+
*
|
|
24
|
+
* eraseLetter("가") //=> "ㄱ"
|
|
25
|
+
*
|
|
26
|
+
* eraseLetter("ㅘ") //=> "ㅗ"
|
|
27
|
+
*/
|
|
28
|
+
export default function(text) {
|
|
29
|
+
const chars = Array.from(text)
|
|
30
|
+
const last = chars.pop() ?? ""
|
|
31
|
+
const head = chars.join("")
|
|
32
|
+
const parts = parts_of(last)
|
|
33
|
+
if (parts) {
|
|
34
|
+
if (parts.final > 0) {
|
|
35
|
+
const pair = compound_consonant_letters[final_letters.charAt(parts.final - 1)]
|
|
36
|
+
return head + syllable(
|
|
37
|
+
parts.initial,
|
|
38
|
+
parts.medial,
|
|
39
|
+
pair ? pair[0] : ""
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const split = split_vowels[medial_letters.charAt(parts.medial)]
|
|
43
|
+
return head + (split
|
|
44
|
+
? syllable(
|
|
45
|
+
parts.initial,
|
|
46
|
+
medial_letters.indexOf(split[0]),
|
|
47
|
+
""
|
|
48
|
+
)
|
|
49
|
+
: initial_letters.charAt(parts.initial))
|
|
50
|
+
}
|
|
51
|
+
return head + (split_vowels[last]?.[0] ?? compound_consonant_letters[last]?.[0] ?? "")
|
|
52
|
+
}
|
package/src/get_letter_range.js
CHANGED
|
@@ -20,10 +20,16 @@ export default function(letter, gap, last) {
|
|
|
20
20
|
}
|
|
21
21
|
const consonant = first_consonant_letters[letter]
|
|
22
22
|
const growth = letter_growths[letter]
|
|
23
|
-
if (consonant || growth)
|
|
23
|
+
if (consonant || growth) {
|
|
24
|
+
return `[${letter}${growth ?? ""}${consonant ?? ""}]`
|
|
25
|
+
}
|
|
24
26
|
const code = letter.charCodeAt(0)
|
|
25
|
-
if (code < 0xAC00 || code > 0xD7A3)
|
|
26
|
-
|
|
27
|
+
if (code < 0xAC00 || code > 0xD7A3) {
|
|
28
|
+
return special_character_regex.test(letter) ? "\\" + letter : letter
|
|
29
|
+
}
|
|
30
|
+
if (!last) {
|
|
31
|
+
return letter
|
|
32
|
+
}
|
|
27
33
|
const syllable = code - 0xAC00
|
|
28
34
|
const final = syllable % 28
|
|
29
35
|
if (!final) {
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
export { default as eraseLetter } from "./erase_letter.js"
|
|
2
|
+
export { default as particle } from "./particle.js"
|
|
1
3
|
export { default as pattern } from "./pattern.js"
|
|
2
|
-
export { default as searcher } from "./searcher.js"
|
|
4
|
+
export { default as searcher } from "./searcher.js"
|
|
5
|
+
export { default as typeLetter } from "./type_letter.js"
|
package/src/jamo.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import compound_consonant_letters from "./compound_consonant_letters.js"
|
|
2
|
+
const HANGUL = 0xac00
|
|
3
|
+
const HANGUL_END = 0xd7a3
|
|
4
|
+
export const final_letters = "ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"
|
|
5
|
+
export const initial_letters = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"
|
|
6
|
+
export const medial_letters = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"
|
|
7
|
+
/** @type {Record<string, string | undefined>} */
|
|
8
|
+
export const joined_finals = Object.fromEntries(
|
|
9
|
+
Object.entries(compound_consonant_letters).flatMap(
|
|
10
|
+
([ joined, pair ]) => pair ? [ [ pair.join(""), joined ] ] : []
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
/** @type {Record<string, string | undefined>} */
|
|
14
|
+
export const joined_vowels = {
|
|
15
|
+
ㅗㅏ: "ㅘ",
|
|
16
|
+
ㅗㅐ: "ㅙ",
|
|
17
|
+
ㅗㅣ: "ㅚ",
|
|
18
|
+
ㅜㅓ: "ㅝ",
|
|
19
|
+
ㅜㅔ: "ㅞ",
|
|
20
|
+
ㅜㅣ: "ㅟ",
|
|
21
|
+
ㅡㅣ: "ㅢ"
|
|
22
|
+
}
|
|
23
|
+
/** @type {Record<string, [ first: string, second: string ] | undefined>} */
|
|
24
|
+
export const split_vowels = Object.fromEntries(
|
|
25
|
+
Object.entries(joined_vowels).map(
|
|
26
|
+
([ pair, joined ]) => [
|
|
27
|
+
joined,
|
|
28
|
+
[ pair.charAt(0), pair.charAt(1) ]
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} char
|
|
34
|
+
* @returns {{ final: number, initial: number, medial: number } | null}
|
|
35
|
+
*/
|
|
36
|
+
export function parts_of(char) {
|
|
37
|
+
const code = char.charCodeAt(0)
|
|
38
|
+
if (!(code >= HANGUL && code <= HANGUL_END)) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const index = code - HANGUL
|
|
42
|
+
return {
|
|
43
|
+
final: index % 28,
|
|
44
|
+
initial: Math.floor(index / 28 / 21),
|
|
45
|
+
medial: Math.floor(index / 28) % 21
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} initial
|
|
50
|
+
* @param {number} medial
|
|
51
|
+
* @param {string} final
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function syllable(initial, medial, final) {
|
|
55
|
+
return String.fromCharCode(
|
|
56
|
+
HANGUL + (initial * 21 + medial) * 28 + (final ? final_letters.indexOf(final) + 1 : 0)
|
|
57
|
+
)
|
|
58
|
+
}
|
package/src/letters_of.js
CHANGED
package/src/particle.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const CONSONANT = 1
|
|
2
|
+
const DIGIT_SOUNDS = "1201001220"
|
|
3
|
+
/** @type {Record<string, [ after_consonant: string, after_vowel: string ]>} */
|
|
4
|
+
const FORMS = {
|
|
5
|
+
"(으)": [ "으", "" ],
|
|
6
|
+
"(이)": [ "이", "" ],
|
|
7
|
+
"과(와)": [ "과", "와" ],
|
|
8
|
+
"아(야)": [ "아", "야" ],
|
|
9
|
+
"은(는)": [ "은", "는" ],
|
|
10
|
+
"을(를)": [ "을", "를" ],
|
|
11
|
+
"이(가)": [ "이", "가" ]
|
|
12
|
+
}
|
|
13
|
+
const GYEONG_ZEROS = 16
|
|
14
|
+
const HANGUL = 0xac00
|
|
15
|
+
const HANGUL_END = 0xd7a3
|
|
16
|
+
const JO_ZEROS = 12
|
|
17
|
+
const LATIN_END = /[A-Za-z]+$/
|
|
18
|
+
const NAMED_CONSONANT = "MN"
|
|
19
|
+
const NAMED_RIEUL = "LR"
|
|
20
|
+
const NUMBER_END = /(\d\.)?([\d,]*\d)$/
|
|
21
|
+
const RIEUL = 2
|
|
22
|
+
const RIEUL_FINAL = 8
|
|
23
|
+
const SEPARATORS = /,/g
|
|
24
|
+
const SPELLED_CONSONANT = /(?:m|n|ng)$/
|
|
25
|
+
const TRAILING_MARKS = /[^\p{L}\p{N}]+$/u
|
|
26
|
+
const TRAILING_ZEROS = /0+$/
|
|
27
|
+
const VOWEL = 0
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} digits
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
function digit_sound(digits) {
|
|
33
|
+
return Number(
|
|
34
|
+
DIGIT_SOUNDS[Number(digits.slice(-1))]
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} text
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
function final_sound(text) {
|
|
42
|
+
const word = text.replace(TRAILING_MARKS, "")
|
|
43
|
+
const number = NUMBER_END.exec(word)
|
|
44
|
+
if (number) {
|
|
45
|
+
const digits = /** @type {string} */(number[2])/**/.replace(SEPARATORS, "")
|
|
46
|
+
const zeros = digits.length - digits.replace(TRAILING_ZEROS, "").length
|
|
47
|
+
if (number[1] || zeros == 0 || zeros == digits.length) {
|
|
48
|
+
return digit_sound(digits)
|
|
49
|
+
}
|
|
50
|
+
return zeros >= JO_ZEROS && zeros < GYEONG_ZEROS ? VOWEL : CONSONANT
|
|
51
|
+
}
|
|
52
|
+
const code = word.charCodeAt(word.length - 1)
|
|
53
|
+
if (code >= HANGUL && code <= HANGUL_END) {
|
|
54
|
+
const final = (code - HANGUL) % 28
|
|
55
|
+
return final == 0
|
|
56
|
+
? VOWEL
|
|
57
|
+
: final == RIEUL_FINAL
|
|
58
|
+
? RIEUL
|
|
59
|
+
: CONSONANT
|
|
60
|
+
}
|
|
61
|
+
const latin = LATIN_END.exec(word)
|
|
62
|
+
if (!latin) {
|
|
63
|
+
return VOWEL
|
|
64
|
+
}
|
|
65
|
+
const run = latin[0]
|
|
66
|
+
if (run.length == 1 || run == run.toUpperCase()) {
|
|
67
|
+
const letter = run.slice(-1).toUpperCase()
|
|
68
|
+
return NAMED_RIEUL.includes(letter)
|
|
69
|
+
? RIEUL
|
|
70
|
+
: NAMED_CONSONANT.includes(letter)
|
|
71
|
+
? CONSONANT
|
|
72
|
+
: VOWEL
|
|
73
|
+
}
|
|
74
|
+
const lower = run.toLowerCase()
|
|
75
|
+
return lower.endsWith("l")
|
|
76
|
+
? RIEUL
|
|
77
|
+
: SPELLED_CONSONANT.test(lower)
|
|
78
|
+
? CONSONANT
|
|
79
|
+
: VOWEL
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The Korean particle that follows a word, chosen by how the word is read aloud: by the final
|
|
83
|
+
* consonant of its last Hangul syllable, by the Korean reading of a number (`1` is 일, `3` is 삼,
|
|
84
|
+
* `10` is 십), and by the Korean name of a Latin letter (`HP` is 에이치피, `URL` ends in ㄹ) or,
|
|
85
|
+
* for a word spelled in lower case, by how its spelling ends (`l` is ㄹ, `m`, `n` and `ng` a consonant).
|
|
86
|
+
* Marks at the end of the word are passed over. Anything else reads as ending in a vowel.
|
|
87
|
+
* The notation is one of `"은(는)"`, `"이(가)"`, `"을(를)"`, `"과(와)"`, `"아(야)"`, `"(이)"` and `"(으)"`;
|
|
88
|
+
* the last two give the syllable that goes between the word and the next one, and nothing after a vowel.
|
|
89
|
+
* `"(으)"` gives nothing after ㄹ too, as `(으)로` needs. Any other notation throws a `TypeError`.
|
|
90
|
+
* @param {string} word
|
|
91
|
+
* @param {string} notation
|
|
92
|
+
* @returns {string}
|
|
93
|
+
* @example particle("사과", "이(가)") //=> "가"
|
|
94
|
+
*
|
|
95
|
+
* particle("밤", "은(는)") //=> "은"
|
|
96
|
+
*
|
|
97
|
+
* "서울" + particle("서울", "(으)") + "로" //=> "서울로"
|
|
98
|
+
*
|
|
99
|
+
* "HP" + particle("HP", "을(를)") //=> "HP를"
|
|
100
|
+
*/
|
|
101
|
+
export default function(word, notation) {
|
|
102
|
+
if (!Object.prototype.hasOwnProperty.call(FORMS, notation)) {
|
|
103
|
+
throw new TypeError(
|
|
104
|
+
`Unknown notation ${JSON.stringify(notation)}, expected one of ${Object.keys(FORMS).join(", ")}`
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
const sound = final_sound(word)
|
|
108
|
+
const [ after_consonant, after_vowel ] = /** @type {[ string, string ]} */(FORMS[notation])/**/
|
|
109
|
+
return sound == VOWEL || sound == RIEUL && notation == "(으)"
|
|
110
|
+
? after_vowel
|
|
111
|
+
: after_consonant
|
|
112
|
+
}
|
package/src/searcher.js
CHANGED
|
@@ -21,10 +21,16 @@ function gap(letters) {
|
|
|
21
21
|
* @returns {string}
|
|
22
22
|
*/
|
|
23
23
|
function separator(letter) {
|
|
24
|
-
if (!non_word_letter_regex.test(letter))
|
|
25
|
-
|
|
24
|
+
if (!non_word_letter_regex.test(letter)) {
|
|
25
|
+
return non_word_separator
|
|
26
|
+
}
|
|
27
|
+
if (letter.length > 1) {
|
|
28
|
+
return `(?:(?!${letter})[${non_word_class}])*?`
|
|
29
|
+
}
|
|
26
30
|
const compound = compound_consonant_letters[letter]
|
|
27
|
-
if (compound)
|
|
31
|
+
if (compound) {
|
|
32
|
+
return gap(letter + compound[0])
|
|
33
|
+
}
|
|
28
34
|
return gap(
|
|
29
35
|
`${class_special_character_regex.test(letter) ? "\\" : ""}${letter}${letter_growths[letter] ?? ""}`
|
|
30
36
|
)
|
|
@@ -52,11 +58,15 @@ export default function(text) {
|
|
|
52
58
|
const letters = letters_of(text).filter(
|
|
53
59
|
letter => !ignored_regex.test(letter)
|
|
54
60
|
)
|
|
55
|
-
if (letters.length > max_letters)
|
|
61
|
+
if (letters.length > max_letters) {
|
|
62
|
+
return RegExp(never_source, "i")
|
|
63
|
+
}
|
|
56
64
|
const last = letters.length - 1
|
|
57
65
|
let source = ""
|
|
58
66
|
for (const [ index, letter ] of letters.entries()) {
|
|
59
|
-
if (index)
|
|
67
|
+
if (index) {
|
|
68
|
+
source += separator(letter)
|
|
69
|
+
}
|
|
60
70
|
source += get_letter_range(letter, gap, index == last)
|
|
61
71
|
}
|
|
62
72
|
return source ? RegExp(source, "i") : null
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import compound_consonant_letters from "./compound_consonant_letters.js"
|
|
2
|
+
import {
|
|
3
|
+
final_letters,
|
|
4
|
+
initial_letters,
|
|
5
|
+
joined_finals,
|
|
6
|
+
joined_vowels,
|
|
7
|
+
medial_letters,
|
|
8
|
+
parts_of,
|
|
9
|
+
syllable
|
|
10
|
+
} from "./jamo.js"
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} head
|
|
13
|
+
* @param {{ final: number, initial: number, medial: number }} parts
|
|
14
|
+
* @param {number} medial
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
function moved_final(head, parts, medial) {
|
|
18
|
+
const final = /** @type {string} */(final_letters[parts.final - 1])/**/
|
|
19
|
+
const [ keep, move ] = compound_consonant_letters[final] ?? [ "", final ]
|
|
20
|
+
return head + syllable(parts.initial, parts.medial, keep) + syllable(
|
|
21
|
+
initial_letters.indexOf(move),
|
|
22
|
+
medial,
|
|
23
|
+
""
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Types one key on a two-set (dubeolsik) Korean keyboard: returns the text with the key composed into
|
|
28
|
+
* its last character, as an input method does. `letter` is a compatibility jamo (`ㄱ`, `ㅏ`, and
|
|
29
|
+
* the double `ㄲ`, `ㅒ` that the shift key gives); any other string is appended as it is.
|
|
30
|
+
* A consonant and a vowel make a syllable, two vowels join (`ㅗ` and `ㅏ` into `ㅘ`), a consonant
|
|
31
|
+
* after a syllable becomes its final and joins the final before it when the two make a compound
|
|
32
|
+
* final (`ㄹ` and `ㄱ` into `ㄺ`), and a vowel after a final moves it onto a new syllable (a compound
|
|
33
|
+
* final moves only its second consonant). Two lone consonants join the same way (`ㄱ` and `ㅅ` into
|
|
34
|
+
* `ㄳ`), and a vowel splits them again. The same key never makes a double consonant: `ㄱ` after
|
|
35
|
+
* `각` is appended, since `ㄲ` needs the shift key.
|
|
36
|
+
* @param {string} text
|
|
37
|
+
* @param {string} letter
|
|
38
|
+
* @returns {string}
|
|
39
|
+
* @example typeLetter("ㄱ", "ㅏ") //=> "가"
|
|
40
|
+
*
|
|
41
|
+
* typeLetter("가", "ㄱ") //=> "각"
|
|
42
|
+
*
|
|
43
|
+
* typeLetter("각", "ㅏ") //=> "가가"
|
|
44
|
+
*
|
|
45
|
+
* typeLetter("고", "ㅏ") //=> "과"
|
|
46
|
+
*
|
|
47
|
+
* typeLetter("갈", "ㄱ") //=> "갉"
|
|
48
|
+
*/
|
|
49
|
+
export default function(text, letter) {
|
|
50
|
+
const chars = Array.from(text)
|
|
51
|
+
const last = chars.pop() ?? ""
|
|
52
|
+
const head = chars.join("")
|
|
53
|
+
const parts = parts_of(last)
|
|
54
|
+
const single = letter.length == 1
|
|
55
|
+
const medial = single ? medial_letters.indexOf(letter) : -1
|
|
56
|
+
if (medial >= 0 && last) {
|
|
57
|
+
if (initial_letters.includes(last)) {
|
|
58
|
+
return head + syllable(
|
|
59
|
+
initial_letters.indexOf(last),
|
|
60
|
+
medial,
|
|
61
|
+
""
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
const pair = compound_consonant_letters[last]
|
|
65
|
+
if (pair) {
|
|
66
|
+
return head + pair[0] + syllable(
|
|
67
|
+
initial_letters.indexOf(pair[1]),
|
|
68
|
+
medial,
|
|
69
|
+
""
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (parts && parts.final > 0) {
|
|
73
|
+
return moved_final(head, parts, medial)
|
|
74
|
+
}
|
|
75
|
+
const joined = joined_vowels[(parts ? medial_letters.charAt(parts.medial) : last) + letter]
|
|
76
|
+
if (joined) {
|
|
77
|
+
return head + (parts
|
|
78
|
+
? syllable(
|
|
79
|
+
parts.initial,
|
|
80
|
+
medial_letters.indexOf(joined),
|
|
81
|
+
""
|
|
82
|
+
)
|
|
83
|
+
: joined)
|
|
84
|
+
}
|
|
85
|
+
} else if (single && initial_letters.includes(letter) && last) {
|
|
86
|
+
if (parts) {
|
|
87
|
+
const final = parts.final > 0
|
|
88
|
+
? joined_finals[final_letters.charAt(parts.final - 1) + letter]
|
|
89
|
+
: final_letters.includes(letter)
|
|
90
|
+
? letter
|
|
91
|
+
: undefined
|
|
92
|
+
if (final) {
|
|
93
|
+
return head + syllable(parts.initial, parts.medial, final)
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const joined = joined_finals[last + letter]
|
|
97
|
+
if (joined) {
|
|
98
|
+
return head + joined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return text + letter
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Takes back one key on a two-set (dubeolsik) Korean keyboard: returns the text after one
|
|
3
|
+
* backspace, undoing what `typeLetter` composed. A compound final loses its second consonant, a
|
|
4
|
+
* single final goes, a joined vowel splits back into its first vowel, a syllable with a simple
|
|
5
|
+
* vowel and no final leaves its initial consonant, and a lone joined vowel or compound consonant
|
|
6
|
+
* loses its second part. Anything else loses its last code point, so an emoji goes whole.
|
|
7
|
+
* @param {string} text
|
|
8
|
+
* @returns {string}
|
|
9
|
+
* @example eraseLetter("갉") //=> "갈"
|
|
10
|
+
*
|
|
11
|
+
* eraseLetter("각") //=> "가"
|
|
12
|
+
*
|
|
13
|
+
* eraseLetter("과") //=> "고"
|
|
14
|
+
*
|
|
15
|
+
* eraseLetter("가") //=> "ㄱ"
|
|
16
|
+
*
|
|
17
|
+
* eraseLetter("ㅘ") //=> "ㅗ"
|
|
18
|
+
*/
|
|
19
|
+
export default function (text: string): string;
|
|
20
|
+
//# sourceMappingURL=erase_letter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erase_letter.d.ts","sourceRoot":"","sources":["../src/erase_letter.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAZjB,MAYiB,GAXf,MAAM,CAmClB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get_letter_range.d.ts","sourceRoot":"","sources":["../src/get_letter_range.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,WAAU,MAAM,EALnB,MAKmB,EAAE,GAAG,EAJxB,CAAC,OAAO,EAAE,MAAM,KAAK,MAIG,EAAE,IAAI,EAH9B,OAG8B,GAF5B,MAAM,
|
|
1
|
+
{"version":3,"file":"get_letter_range.d.ts","sourceRoot":"","sources":["../src/get_letter_range.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,MAAM,CAAC,OAAO,WAAU,MAAM,EALnB,MAKmB,EAAE,GAAG,EAJxB,CAAC,OAAO,EAAE,MAAM,KAAK,MAIG,EAAE,IAAI,EAH9B,OAG8B,GAF5B,MAAM,CAoClB"}
|
package/types/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { default as eraseLetter } from "./erase_letter.js";
|
|
2
|
+
export { default as particle } from "./particle.js";
|
|
1
3
|
export { default as pattern } from "./pattern.js";
|
|
2
4
|
export { default as searcher } from "./searcher.js";
|
|
5
|
+
export { default as typeLetter } from "./type_letter.js";
|
|
3
6
|
//# sourceMappingURL=index.d.ts.map
|
package/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
|
package/types/jamo.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const final_letters = "\u3131\u3132\u3133\u3134\u3135\u3136\u3137\u3139\u313A\u313B\u313C\u313D\u313E\u313F\u3140\u3141\u3142\u3144\u3145\u3146\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
|
|
2
|
+
export declare const initial_letters = "\u3131\u3132\u3134\u3137\u3138\u3139\u3141\u3142\u3143\u3145\u3146\u3147\u3148\u3149\u314A\u314B\u314C\u314D\u314E";
|
|
3
|
+
export declare const medial_letters = "\u314F\u3150\u3151\u3152\u3153\u3154\u3155\u3156\u3157\u3158\u3159\u315A\u315B\u315C\u315D\u315E\u315F\u3160\u3161\u3162\u3163";
|
|
4
|
+
/** @type {Record<string, string | undefined>} */
|
|
5
|
+
export declare const joined_finals: Record<string, string | undefined>;
|
|
6
|
+
/** @type {Record<string, string | undefined>} */
|
|
7
|
+
export declare const joined_vowels: Record<string, string | undefined>;
|
|
8
|
+
/** @type {Record<string, [ first: string, second: string ] | undefined>} */
|
|
9
|
+
export declare const split_vowels: Record<string, [first: string, second: string] | undefined>;
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} char
|
|
12
|
+
* @returns {{ final: number, initial: number, medial: number } | null}
|
|
13
|
+
*/
|
|
14
|
+
export declare function parts_of(char: string): {
|
|
15
|
+
final: number;
|
|
16
|
+
initial: number;
|
|
17
|
+
medial: number;
|
|
18
|
+
} | null;
|
|
19
|
+
/**
|
|
20
|
+
* @param {number} initial
|
|
21
|
+
* @param {number} medial
|
|
22
|
+
* @param {string} final
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export declare function syllable(initial: number, medial: number, final: string): string;
|
|
26
|
+
//# sourceMappingURL=jamo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jamo.d.ts","sourceRoot":"","sources":["../src/jamo.js"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa,uKAAgC,CAAA;AAC1D,eAAO,MAAM,eAAe,uHAAwB,CAAA;AACpD,eAAO,MAAM,cAAc,mIAA0B,CAAA;AACrD,iDAAiD;AACjD,eAAO,MAAM,aAAa,EADf,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAK3C,CAAA;AACD,iDAAiD;AACjD,eAAO,MAAM,aAAa,EADf,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAS3C,CAAA;AACD,4EAA4E;AAC5E,eAAO,MAAM,YAAY,EADd,MAAM,CAAC,MAAM,EAAE,CAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAE,GAAG,SAAS,CAQtE,CAAA;AACD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,MAGkB,GAFhB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAarE;AACD;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EALrB,MAKqB,EAAE,MAAM,EAJ7B,MAI6B,EAAE,KAAK,EAHpC,MAGoC,GAFlC,MAAM,CAMlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"letters_of.d.ts","sourceRoot":"","sources":["../src/letters_of.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"letters_of.d.ts","sourceRoot":"","sources":["../src/letters_of.js"],"names":[],"mappings":"AACA;;;GAGG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAHjB,MAGiB,GAFf,MAAM,EAAE,CAepB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Korean particle that follows a word, chosen by how the word is read aloud: by the final
|
|
3
|
+
* consonant of its last Hangul syllable, by the Korean reading of a number (`1` is 일, `3` is 삼,
|
|
4
|
+
* `10` is 십), and by the Korean name of a Latin letter (`HP` is 에이치피, `URL` ends in ㄹ) or,
|
|
5
|
+
* for a word spelled in lower case, by how its spelling ends (`l` is ㄹ, `m`, `n` and `ng` a consonant).
|
|
6
|
+
* Marks at the end of the word are passed over. Anything else reads as ending in a vowel.
|
|
7
|
+
* The notation is one of `"은(는)"`, `"이(가)"`, `"을(를)"`, `"과(와)"`, `"아(야)"`, `"(이)"` and `"(으)"`;
|
|
8
|
+
* the last two give the syllable that goes between the word and the next one, and nothing after a vowel.
|
|
9
|
+
* `"(으)"` gives nothing after ㄹ too, as `(으)로` needs. Any other notation throws a `TypeError`.
|
|
10
|
+
* @param {string} word
|
|
11
|
+
* @param {string} notation
|
|
12
|
+
* @returns {string}
|
|
13
|
+
* @example particle("사과", "이(가)") //=> "가"
|
|
14
|
+
*
|
|
15
|
+
* particle("밤", "은(는)") //=> "은"
|
|
16
|
+
*
|
|
17
|
+
* "서울" + particle("서울", "(으)") + "로" //=> "서울로"
|
|
18
|
+
*
|
|
19
|
+
* "HP" + particle("HP", "을(를)") //=> "HP를"
|
|
20
|
+
*/
|
|
21
|
+
export default function (word: string, notation: string): string;
|
|
22
|
+
//# sourceMappingURL=particle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"particle.d.ts","sourceRoot":"","sources":["../src/particle.js"],"names":[],"mappings":"AAgFA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAXjB,MAWiB,EAAE,QAAQ,EAV3B,MAU2B,GATzB,MAAM,CAoBlB"}
|
package/types/searcher.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searcher.d.ts","sourceRoot":"","sources":["../src/searcher.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"searcher.d.ts","sourceRoot":"","sources":["../src/searcher.js"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAXjB,MAWiB,GAVf,MAAM,OAAC,CA0BnB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types one key on a two-set (dubeolsik) Korean keyboard: returns the text with the key composed into
|
|
3
|
+
* its last character, as an input method does. `letter` is a compatibility jamo (`ㄱ`, `ㅏ`, and
|
|
4
|
+
* the double `ㄲ`, `ㅒ` that the shift key gives); any other string is appended as it is.
|
|
5
|
+
* A consonant and a vowel make a syllable, two vowels join (`ㅗ` and `ㅏ` into `ㅘ`), a consonant
|
|
6
|
+
* after a syllable becomes its final and joins the final before it when the two make a compound
|
|
7
|
+
* final (`ㄹ` and `ㄱ` into `ㄺ`), and a vowel after a final moves it onto a new syllable (a compound
|
|
8
|
+
* final moves only its second consonant). Two lone consonants join the same way (`ㄱ` and `ㅅ` into
|
|
9
|
+
* `ㄳ`), and a vowel splits them again. The same key never makes a double consonant: `ㄱ` after
|
|
10
|
+
* `각` is appended, since `ㄲ` needs the shift key.
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @param {string} letter
|
|
13
|
+
* @returns {string}
|
|
14
|
+
* @example typeLetter("ㄱ", "ㅏ") //=> "가"
|
|
15
|
+
*
|
|
16
|
+
* typeLetter("가", "ㄱ") //=> "각"
|
|
17
|
+
*
|
|
18
|
+
* typeLetter("각", "ㅏ") //=> "가가"
|
|
19
|
+
*
|
|
20
|
+
* typeLetter("고", "ㅏ") //=> "과"
|
|
21
|
+
*
|
|
22
|
+
* typeLetter("갈", "ㄱ") //=> "갉"
|
|
23
|
+
*/
|
|
24
|
+
export default function (text: string, letter: string): string;
|
|
25
|
+
//# sourceMappingURL=type_letter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type_letter.d.ts","sourceRoot":"","sources":["../src/type_letter.js"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,WAAU,IAAI,EAbjB,MAaiB,EAAE,MAAM,EAZzB,MAYyB,GAXvB,MAAM,CAiElB"}
|