hanlint 0.0.7 → 0.0.9
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/LICENSE +21 -0
- package/README.md +32 -9
- package/data/adjectiveStems.txt +118 -0
- package/data/baselineAnchors.txt +3 -0
- package/data/blindEvaluation.schema.json +26 -0
- package/data/bridgeOpeners.txt +10 -0
- package/data/candidateActionNouns.txt +39 -0
- package/data/candidateSplitEndings.txt +13 -0
- package/data/cliches.json +1 -1
- package/data/confusable.json +2 -2
- package/data/deixis.txt +10 -3
- package/data/draftHistory.txt +10 -0
- package/data/easyWords.json +0 -42
- package/data/enoughMarkers.txt +6 -0
- package/data/entailmentPredictions.schema.json +52 -0
- package/data/euiNouns.txt +3 -4
- package/data/evidenceEntailmentBenchmark.schema.json +126 -0
- package/data/exemplars.json +323 -0
- package/data/holeKinds.json +2 -14
- package/data/iVerbStems.txt +26 -0
- package/data/inNouns.txt +147 -0
- package/data/irregularStems.txt +64 -0
- package/data/nonNouns.txt +153 -0
- package/data/panelAssignmentReview.schema.json +117 -0
- package/data/panelJudgePredictions.schema.json +78 -0
- package/data/panelReviewBatch.schema.json +79 -0
- package/data/panelTrialSet.schema.json +44 -0
- package/data/patterns.json +103 -0
- package/data/platformApis.txt +3 -1
- package/data/profiles.json +1 -0
- package/data/readerCalls.txt +12 -7
- package/data/redundantPair.json +1 -1
- package/data/rieulStems.txt +30 -0
- package/data/ruleCategories.json +52 -0
- package/data/ruleDocs.json +27 -19
- package/data/ruleMechanisms.json +52 -0
- package/data/spacing.json +0 -12
- package/data/spelling.json +7 -25
- package/data/translationese.json +10 -0
- package/data/verbTails.txt +17 -0
- package/data/version.json +1 -1
- package/data/writingBrief.schema.json +64 -0
- package/data/writingBriefV2.schema.json +116 -0
- package/data/writingTrial.schema.json +36 -0
- package/koglType1.LICENSE.md +25 -0
- package/package.json +18 -6
- package/src/analysis/grammar/ending.js +336 -0
- package/src/analysis/grammar/hangul.js +94 -0
- package/src/analysis/grammar/index.js +5 -0
- package/src/analysis/grammar/josa.js +73 -0
- package/src/analysis/grammar/register.js +115 -0
- package/src/analysis/grammar/voice.js +43 -0
- package/src/analysis/index.js +4 -31
- package/src/analysis/josa.js +2 -0
- package/src/analysis/{surface/splitSentences.js → splitSentences.js} +1 -1
- package/src/analysis/{surface/tokenize.js → tokenize.js} +70 -13
- package/src/baseline/store.js +114 -0
- package/src/cli/main.js +508 -61
- package/src/cli/walk.js +30 -0
- package/src/cli/welcome.js +52 -0
- package/src/config/settings.js +111 -12
- package/src/data/exemplars.js +177 -0
- package/src/data/load.js +10 -0
- package/src/data/operations.js +278 -0
- package/src/data/patches.js +125 -0
- package/src/data/patterns.js +48 -0
- package/src/data/profiles.js +102 -0
- package/src/document/model.js +2 -0
- package/src/document/parseMarkdown.js +84 -11
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +60 -39
- package/src/{rules/shared → fingerprint}/codeBlocks.js +11 -11
- package/src/fingerprint/codeMarkers.js +73 -0
- package/src/fingerprint/markers.js +9 -0
- package/src/fingerprint/readerState.js +126 -0
- package/src/fingerprint/sourceSentence.js +37 -0
- package/src/fingerprint/topics.js +1 -1
- package/src/index.js +2 -16
- package/src/report/compactReport.js +6 -1
- package/src/report/fingerprintJson.js +8 -3
- package/src/report/jsonReport.js +50 -3
- package/src/report/operationMatch.js +31 -0
- package/src/report/patchMatch.js +44 -0
- package/src/report/registerMatch.js +40 -0
- package/src/report/textReport.js +41 -5
- package/src/rules/code/duplicateBlock.js +4 -3
- package/src/rules/code/firstResultDistance.js +1 -0
- package/src/rules/code/inputFileSource.js +23 -68
- package/src/rules/code/installImport.js +6 -5
- package/src/rules/code/platformApi.js +7 -6
- package/src/rules/document/countMismatch.js +22 -6
- package/src/rules/document/enoughOnce.js +25 -0
- package/src/rules/document/fieldEcho.js +38 -0
- package/src/rules/document/keywordHeading.js +32 -0
- package/src/rules/document/keywordMissing.js +1 -0
- package/src/rules/document/noQuestion.js +5 -1
- package/src/rules/document/promiseRecall.js +4 -2
- package/src/rules/document/tableOddCell.js +61 -0
- package/src/rules/finding.js +24 -2
- package/src/rules/orthography/confusable.js +1 -0
- package/src/rules/orthography/spacing.js +1 -0
- package/src/rules/orthography/spelling.js +1 -0
- package/src/rules/paragraph/factListParagraph.js +1 -0
- package/src/rules/paragraph/paraFragment.js +21 -11
- package/src/rules/registry.js +70 -9
- package/src/rules/sentence/cliche.js +1 -0
- package/src/rules/sentence/connectorRepeat.js +8 -7
- package/src/rules/sentence/danglingDeixis.js +23 -3
- package/src/rules/sentence/dash.js +2 -1
- package/src/rules/sentence/deixis.js +7 -3
- package/src/rules/sentence/doubleNegative.js +4 -1
- package/src/rules/sentence/doublePassive.js +38 -1
- package/src/rules/sentence/draftHistory.js +24 -0
- package/src/rules/sentence/endingRepeat.js +6 -2
- package/src/rules/sentence/euiChain.js +3 -2
- package/src/rules/sentence/fillerOpener.js +1 -0
- package/src/rules/sentence/hardWord.js +1 -0
- package/src/rules/sentence/imperativePeriod.js +2 -1
- package/src/rules/sentence/japaneseLoan.js +1 -0
- package/src/rules/sentence/longSentence.js +3 -1
- package/src/rules/sentence/negationRedefine.js +12 -2
- package/src/rules/sentence/nounPile.js +1 -0
- package/src/rules/sentence/numberOrphan.js +45 -0
- package/src/rules/sentence/outsideProfile.js +68 -0
- package/src/rules/sentence/redundantPair.js +1 -0
- package/src/rules/sentence/translationese.js +1 -0
- package/src/rules/shared/candidates.js +129 -0
- package/src/rules/shared/dictionaryRule.js +12 -2
- package/src/rules/shared/localAntecedent.js +12 -0
- package/src/rules/shared/repeat.js +48 -0
- package/src/rules/structure/blockUnread.js +33 -0
- package/src/rules/structure/bridgeRepeat.js +40 -0
- package/src/rules/structure/emojiBullet.js +1 -0
- package/src/rules/structure/headingSentence.js +9 -4
- package/src/rules/structure/headingSkip.js +1 -0
- package/src/rules/structure/headingUniform.js +12 -20
- package/src/rules/structure/introImage.js +19 -0
- package/src/rules/structure/introLong.js +4 -1
- package/src/rules/structure/loneSubheading.js +42 -0
- package/src/rules/structure/moreLater.js +48 -0
- package/src/rules/structure/sectionNoProse.js +1 -0
- package/src/text.js +19 -0
- package/data/coverageTypes.txt +0 -7
- package/src/rules/document/readerAbsent.js +0 -11
- package/src/rules/paragraph/topicBreak.js +0 -25
- package/src/rules/sentence/ellipsis.js +0 -18
- package/src/rules/shared/runs.js +0 -22
- package/src/rules/structure/sectionResult.js +0 -37
package/data/translationese.json
CHANGED
|
@@ -60,6 +60,16 @@
|
|
|
60
60
|
"why": "번역투다. 목적어나 `~은` 으로 되돌린다",
|
|
61
61
|
"source": "urimal.org 김민지 2020"
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"pattern": "에 대한",
|
|
65
|
+
"why": "번역투다. `문제점에 대한 이해` 는 `문제점을 아는 것` 이다. 목적어로 되돌린다",
|
|
66
|
+
"source": "urimal.org 김민지 2020"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"pattern": "에 관한",
|
|
70
|
+
"why": "번역투다. `설정에 관한 설명` 은 `설정 설명` 이다. 목적어나 `~은` 으로 되돌린다",
|
|
71
|
+
"source": "urimal.org 김민지 2020"
|
|
72
|
+
},
|
|
63
73
|
{
|
|
64
74
|
"pattern": "(?:을|를) 위(?:해|하여|해서)",
|
|
65
75
|
"why": "번역투다. `여행을 가기 위해` 는 `여행 가려고` 다",
|
package/data/verbTails.txt
CHANGED
|
@@ -54,3 +54,20 @@
|
|
|
54
54
|
된
|
|
55
55
|
될
|
|
56
56
|
돼
|
|
57
|
+
# 실측 2026-08-29: 말뭉치에서 `있던`, `힘껏`, `유난히`, `되었는지`, `올바른` 이 명사로 세어졌다. 과거 관형형, 부사 파생,
|
|
58
|
+
# 의문형 연결, 관형형 꼬리다. `교수이자` 의 이자와 `알고자` 의 고자와 `은지` 는 대출이자, 신고자, 김은지 같은 명사와
|
|
59
|
+
# 표층으로 안 갈려 넣지 않는다 (검증에서 걸렸다).
|
|
60
|
+
던
|
|
61
|
+
껏
|
|
62
|
+
히
|
|
63
|
+
는지
|
|
64
|
+
른
|
|
65
|
+
# 실측 2026-08-29: `새로운 CPU 매니저 스태틱 정책 옵션` 의 새로운 이 명사로 세어졌다. ㅂ 불규칙 형용사의 관형형 꼬리다.
|
|
66
|
+
# 다운 은 카운트다운 과 갈리지 않아 넣지 않는다.
|
|
67
|
+
로운
|
|
68
|
+
라운
|
|
69
|
+
려운
|
|
70
|
+
거운
|
|
71
|
+
벼운
|
|
72
|
+
러운
|
|
73
|
+
쉬운
|
package/data/version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version": "0.0.
|
|
1
|
+
{"version": "0.0.9"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hanlint.dev/schemas/writingBrief.schema.json",
|
|
4
|
+
"title": "hanlint writing brief",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"preset",
|
|
10
|
+
"reader",
|
|
11
|
+
"task",
|
|
12
|
+
"facts",
|
|
13
|
+
"mustInclude",
|
|
14
|
+
"allowedNumbers",
|
|
15
|
+
"forbidden",
|
|
16
|
+
"length"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"version": { "const": 1 },
|
|
20
|
+
"preset": {
|
|
21
|
+
"enum": ["blog", "report", "docs", "guide", "essay", "fiction", "encyclopedia"]
|
|
22
|
+
},
|
|
23
|
+
"reader": { "type": "string", "minLength": 1 },
|
|
24
|
+
"task": { "type": "string", "minLength": 1 },
|
|
25
|
+
"facts": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"minItems": 1,
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"additionalProperties": false,
|
|
31
|
+
"required": ["id", "statement"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"id": { "type": "string", "pattern": "^F[1-9][0-9]*$" },
|
|
34
|
+
"statement": { "type": "string", "minLength": 1 }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"mustInclude": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"uniqueItems": true,
|
|
42
|
+
"items": { "type": "string", "minLength": 1 }
|
|
43
|
+
},
|
|
44
|
+
"allowedNumbers": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"uniqueItems": true,
|
|
47
|
+
"items": { "type": "string", "pattern": "^[0-9]+(?:[.][0-9]+)*$" }
|
|
48
|
+
},
|
|
49
|
+
"forbidden": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"uniqueItems": true,
|
|
52
|
+
"items": { "type": "string", "minLength": 1 }
|
|
53
|
+
},
|
|
54
|
+
"length": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["min", "max"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"min": { "type": "integer", "minimum": 1 },
|
|
60
|
+
"max": { "type": "integer", "minimum": 1 }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hanlint.dev/schemas/writingBriefV2.schema.json",
|
|
4
|
+
"title": "hanlint evidence writing brief",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"version",
|
|
9
|
+
"preset",
|
|
10
|
+
"reader",
|
|
11
|
+
"task",
|
|
12
|
+
"facts",
|
|
13
|
+
"mustInclude",
|
|
14
|
+
"allowedNumbers",
|
|
15
|
+
"forbidden",
|
|
16
|
+
"length",
|
|
17
|
+
"evidence"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"version": { "const": 2 },
|
|
21
|
+
"preset": {
|
|
22
|
+
"enum": ["blog", "report", "docs", "guide", "essay", "fiction", "encyclopedia"]
|
|
23
|
+
},
|
|
24
|
+
"reader": { "type": "string", "minLength": 1 },
|
|
25
|
+
"task": { "type": "string", "minLength": 1 },
|
|
26
|
+
"facts": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"minItems": 1,
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["id", "statement"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": { "type": "string", "pattern": "^F[1-9][0-9]*$" },
|
|
35
|
+
"statement": { "type": "string", "minLength": 1 }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"mustInclude": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"uniqueItems": true,
|
|
43
|
+
"items": { "type": "string", "minLength": 1 }
|
|
44
|
+
},
|
|
45
|
+
"allowedNumbers": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"uniqueItems": true,
|
|
48
|
+
"items": { "type": "string", "pattern": "^[0-9]+(?:[.][0-9]+)*$" }
|
|
49
|
+
},
|
|
50
|
+
"forbidden": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"uniqueItems": true,
|
|
53
|
+
"items": { "type": "string", "minLength": 1 }
|
|
54
|
+
},
|
|
55
|
+
"length": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"required": ["min", "max"],
|
|
59
|
+
"properties": {
|
|
60
|
+
"min": { "type": "integer", "minimum": 1 },
|
|
61
|
+
"max": { "type": "integer", "minimum": 1 }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"evidence": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"minItems": 1,
|
|
67
|
+
"items": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"required": [
|
|
71
|
+
"id",
|
|
72
|
+
"factIds",
|
|
73
|
+
"sourceUrl",
|
|
74
|
+
"revision",
|
|
75
|
+
"checkedAt",
|
|
76
|
+
"locator",
|
|
77
|
+
"excerpt",
|
|
78
|
+
"excerptSha256",
|
|
79
|
+
"license",
|
|
80
|
+
"reviewStatus"
|
|
81
|
+
],
|
|
82
|
+
"properties": {
|
|
83
|
+
"id": { "type": "string", "pattern": "^E[1-9][0-9]*$" },
|
|
84
|
+
"factIds": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"minItems": 1,
|
|
87
|
+
"uniqueItems": true,
|
|
88
|
+
"items": { "type": "string", "pattern": "^F[1-9][0-9]*$" }
|
|
89
|
+
},
|
|
90
|
+
"sourceUrl": { "type": "string", "format": "uri", "pattern": "^https?://" },
|
|
91
|
+
"revision": {
|
|
92
|
+
"type": ["string", "null"],
|
|
93
|
+
"minLength": 1,
|
|
94
|
+
"not": {
|
|
95
|
+
"pattern": "^(?:[Hh][Ee][Aa][Dd]|[Ll][Aa][Tt][Ee][Ss][Tt]|[Mm][Aa][Ii][Nn]|[Mm][Aa][Ss][Tt][Ee][Rr])$"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"checkedAt": {
|
|
99
|
+
"type": ["string", "null"],
|
|
100
|
+
"format": "date-time",
|
|
101
|
+
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
|
|
102
|
+
},
|
|
103
|
+
"locator": { "type": "string", "minLength": 1 },
|
|
104
|
+
"excerpt": { "type": "string", "minLength": 1, "maxLength": 1000 },
|
|
105
|
+
"excerptSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
106
|
+
"license": { "type": "string", "minLength": 1 },
|
|
107
|
+
"reviewStatus": { "enum": ["unreviewed", "humanVerified"] }
|
|
108
|
+
},
|
|
109
|
+
"anyOf": [
|
|
110
|
+
{ "properties": { "revision": { "type": "string", "minLength": 1 } } },
|
|
111
|
+
{ "properties": { "checkedAt": { "type": "string" } } }
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hanlint.dev/schemas/writingTrial.schema.json",
|
|
4
|
+
"title": "hanlint writing trial",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["version", "id", "brief", "baseline", "candidate"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": { "const": 1 },
|
|
10
|
+
"id": { "type": "string", "minLength": 1 },
|
|
11
|
+
"brief": {
|
|
12
|
+
"oneOf": [
|
|
13
|
+
{ "$ref": "writingBrief.schema.json" },
|
|
14
|
+
{ "$ref": "writingBriefV2.schema.json" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"baseline": { "$ref": "#/$defs/generation" },
|
|
18
|
+
"candidate": { "$ref": "#/$defs/generation" }
|
|
19
|
+
},
|
|
20
|
+
"$defs": {
|
|
21
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
22
|
+
"generation": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"required": ["strategyId", "modelId", "modelSha256", "promptSha256", "outputSha256", "text"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"strategyId": { "type": "string", "minLength": 1 },
|
|
28
|
+
"modelId": { "type": "string", "minLength": 1 },
|
|
29
|
+
"modelSha256": { "$ref": "#/$defs/sha256" },
|
|
30
|
+
"promptSha256": { "$ref": "#/$defs/sha256" },
|
|
31
|
+
"outputSha256": { "$ref": "#/$defs/sha256" },
|
|
32
|
+
"text": { "type": "string", "minLength": 1 }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 국립국어원 파생 데이터 라이선스 (공공누리 제1유형)
|
|
2
|
+
|
|
3
|
+
아래 두 파일은 국립국어원이 공공누리 제1유형 (출처 표시) 으로 공개한 자료에서 왔다. 나머지 hanlint
|
|
4
|
+
코드와 데이터의 MIT 와는 별개다.
|
|
5
|
+
|
|
6
|
+
## `learningVocabulary.tsv`
|
|
7
|
+
|
|
8
|
+
- 원저작자: 국립국어원
|
|
9
|
+
- 자료: 한국어 학습용 어휘 목록 (5,965개)
|
|
10
|
+
- 원본: <https://www.korean.go.kr/front/etcData/etcDataView.do?etc_seq=70&mn_id=46&pageIndex=9>
|
|
11
|
+
- 받은 날: 2026-08-30
|
|
12
|
+
- 라이선스: [공공누리 제1유형 출처 표시](https://www.kogl.or.kr/info/licenseType1.do)
|
|
13
|
+
- 변경: cp949 를 UTF-8 로 옮기고 열 이름을 바꿨다. 낱말, 품사, 풀이, 등급의 내용은 바꾸지 않았다.
|
|
14
|
+
만드는 절차와 원본 필드는 `learningVocabularySource.toml` 이 소유한다.
|
|
15
|
+
|
|
16
|
+
## `easyWords.toml`
|
|
17
|
+
|
|
18
|
+
- 원저작자: 국립국어원
|
|
19
|
+
- 자료: 다듬은 말 (<https://www.korean.go.kr/front/imprv/refineList.do>) 과 표준국어대사전
|
|
20
|
+
(<https://stdict.korean.go.kr>) 의 동의어 정보
|
|
21
|
+
- 라이선스: [공공누리 제1유형 출처 표시](https://www.kogl.or.kr/info/licenseType1.do)
|
|
22
|
+
- 변경: 어려운 낱말과 쉬운 말의 짝만 뽑아 TOML 로 옮겼다.
|
|
23
|
+
|
|
24
|
+
공공누리 제1유형은 출처를 밝히면 상업적 이용과 변형을 포함해 자유롭게 쓸 수 있다. 이 파일이 그
|
|
25
|
+
출처 표시다.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanlint",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "한국어
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "한국어 글 검사기. Korean prose linter for Markdown: translationese, noun piles, double passives, weak structure. Zero dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hanlint": "bin/hanlint.js"
|
|
@@ -14,22 +14,34 @@
|
|
|
14
14
|
"bin",
|
|
15
15
|
"src",
|
|
16
16
|
"data",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"koglType1.LICENSE.md",
|
|
17
19
|
"README.md"
|
|
18
20
|
],
|
|
19
21
|
"engines": {
|
|
20
22
|
"node": ">=18"
|
|
21
23
|
},
|
|
22
24
|
"scripts": {
|
|
23
|
-
"test": "node --test test
|
|
25
|
+
"test": "node --test \"test/*.test.js\""
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"pyproc": "0.0.23"
|
|
24
29
|
},
|
|
25
30
|
"keywords": [
|
|
26
31
|
"korean",
|
|
27
|
-
"
|
|
32
|
+
"korean-language",
|
|
33
|
+
"hangul",
|
|
34
|
+
"linter",
|
|
35
|
+
"prose",
|
|
36
|
+
"prose-linter",
|
|
28
37
|
"writing",
|
|
29
38
|
"readability",
|
|
30
|
-
"markdown"
|
|
39
|
+
"markdown",
|
|
40
|
+
"style-checker",
|
|
41
|
+
"proofreading",
|
|
42
|
+
"text-linter"
|
|
31
43
|
],
|
|
32
|
-
"license": "
|
|
44
|
+
"license": "SEE LICENSE IN koglType1.LICENSE.md",
|
|
33
45
|
"repository": {
|
|
34
46
|
"type": "git",
|
|
35
47
|
"url": "git+https://github.com/eddmpython/hanlint.git",
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** 파이썬 `analysis/grammar/ending.py`와 `predicate.py`의 투영. 종결 서술어를 풀고 세 문체로 짠다. */
|
|
3
|
+
import { loadLines } from "../../data/load.js";
|
|
4
|
+
import * as hangul from "./hangul.js";
|
|
5
|
+
|
|
6
|
+
export const VERB = "동사";
|
|
7
|
+
export const ADJECTIVE = "형용사";
|
|
8
|
+
export const COPULA = "계사";
|
|
9
|
+
export const PRESENT = "현재";
|
|
10
|
+
export const PAST = "과거";
|
|
11
|
+
export const FUTURE = "미래";
|
|
12
|
+
export const DECLARATIVE = "평서";
|
|
13
|
+
export const QUESTION = "의문";
|
|
14
|
+
export const KKA_QUESTION = "까의문";
|
|
15
|
+
export const IMPERATIVE = "명령";
|
|
16
|
+
export const PROPOSITIVE = "청유";
|
|
17
|
+
export const HAPNIDA = "합니다";
|
|
18
|
+
export const HANDA = "한다";
|
|
19
|
+
export const HAEYO = "해요";
|
|
20
|
+
export const REGISTERS = [HAPNIDA, HANDA, HAEYO];
|
|
21
|
+
|
|
22
|
+
const HAE_IRREGULAR = new Map([
|
|
23
|
+
["그렇", "그래"],
|
|
24
|
+
["이렇", "이래"],
|
|
25
|
+
["저렇", "저래"],
|
|
26
|
+
["어떻", "어때"],
|
|
27
|
+
]);
|
|
28
|
+
const AUX_LINKS = ["지는", "지도", "지만", "지"];
|
|
29
|
+
|
|
30
|
+
let adjectiveCache = null;
|
|
31
|
+
let rieulCache = null;
|
|
32
|
+
let iVerbCache = null;
|
|
33
|
+
let irregularCache = null;
|
|
34
|
+
|
|
35
|
+
function adjectiveStems() {
|
|
36
|
+
if (!adjectiveCache) adjectiveCache = new Set(loadLines("adjectiveStems.txt"));
|
|
37
|
+
return adjectiveCache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rieulStems() {
|
|
41
|
+
if (!rieulCache) rieulCache = new Set(loadLines("rieulStems.txt"));
|
|
42
|
+
return rieulCache;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function iVerbStems() {
|
|
46
|
+
if (!iVerbCache) iVerbCache = new Set(loadLines("iVerbStems.txt"));
|
|
47
|
+
return iVerbCache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function irregularStems() {
|
|
51
|
+
if (!irregularCache) {
|
|
52
|
+
irregularCache = loadLines("irregularStems.txt")
|
|
53
|
+
.map((line) => line.split("\t"))
|
|
54
|
+
.sort((a, b) => b[0].length - a[0].length);
|
|
55
|
+
}
|
|
56
|
+
return irregularCache;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {string} stem */
|
|
60
|
+
export function isAdjective(stem) {
|
|
61
|
+
return adjectiveStems().has(stem);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {string} stem */
|
|
65
|
+
function irregularClass(stem) {
|
|
66
|
+
for (const [known, kind] of irregularStems()) {
|
|
67
|
+
if (stem === known || stem.endsWith(known)) return kind;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @param {string} base */
|
|
73
|
+
function restoreRieul(base) {
|
|
74
|
+
const last = hangul.lastSyllable(base);
|
|
75
|
+
if (last === null || hangul.finalOf(last) !== hangul.NONE) return base;
|
|
76
|
+
const candidate = base.slice(0, -1) + hangul.withFinal(last, hangul.RIEUL);
|
|
77
|
+
return rieulStems().has(candidate) ? candidate : base;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string} base */
|
|
81
|
+
function stripEu(base) {
|
|
82
|
+
if (base.length >= 2 && base.endsWith("으") && hangul.isSyllable(base.at(-2)) && hangul.finalOf(base.at(-2)) !== hangul.NONE) {
|
|
83
|
+
return base.slice(0, -1);
|
|
84
|
+
}
|
|
85
|
+
return base;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {string} base */
|
|
89
|
+
function tenseOf(base) {
|
|
90
|
+
const last = hangul.lastSyllable(base);
|
|
91
|
+
if (base.endsWith("겠")) return FUTURE;
|
|
92
|
+
if (last !== null && last !== "있" && hangul.finalOf(last) === hangul.SSANGSIOT) return PAST;
|
|
93
|
+
return PRESENT;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {string} base @param {string | null} previous */
|
|
97
|
+
function kindOf(base, previous) {
|
|
98
|
+
if (base === "않" && previous) {
|
|
99
|
+
for (const link of AUX_LINKS) {
|
|
100
|
+
if (previous.endsWith(link) && previous.length > link.length) {
|
|
101
|
+
return isAdjective(previous.slice(0, -link.length)) ? ADJECTIVE : VERB;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return isAdjective(base) ? ADJECTIVE : VERB;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @typedef {object} Predicate
|
|
110
|
+
* @property {string} base
|
|
111
|
+
* @property {string} kind
|
|
112
|
+
* @property {string} tense
|
|
113
|
+
* @property {string} mood
|
|
114
|
+
* @property {boolean} explicitCopula
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/** @param {string} base @param {string} kind @param {string} [tense] @param {string} [mood] @param {boolean} [explicitCopula] @returns {Predicate} */
|
|
118
|
+
function predicate(base, kind, tense = PRESENT, mood = DECLARATIVE, explicitCopula = false) {
|
|
119
|
+
return { base, kind, tense, mood, explicitCopula };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @param {string} base @param {string | null} previous @param {string} mood */
|
|
123
|
+
function classifyPredicate(base, previous, mood) {
|
|
124
|
+
if (!base) return null;
|
|
125
|
+
const tense = tenseOf(base);
|
|
126
|
+
const kind = tense !== PRESENT ? VERB : kindOf(base, previous);
|
|
127
|
+
return predicate(base, kind, tense, mood);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** @param {string} word @param {number} offset @param {number} final */
|
|
131
|
+
function endsWithFinal(word, offset, final) {
|
|
132
|
+
if (word.length < offset) return false;
|
|
133
|
+
const ch = word.at(-offset);
|
|
134
|
+
return hangul.isSyllable(ch) && hangul.finalOf(ch) === final;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {string} word @param {number} offset */
|
|
138
|
+
function stemBefore(word, offset) {
|
|
139
|
+
const ch = word.at(-offset);
|
|
140
|
+
return word.slice(0, -offset) + hangul.withFinal(ch, hangul.NONE);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @param {string} noun @param {string} mood */
|
|
144
|
+
function copulaOrIVerb(noun, mood) {
|
|
145
|
+
const stem = noun + "이";
|
|
146
|
+
return iVerbStems().has(stem) ? predicate(stem, VERB, PRESENT, mood) : predicate(noun, COPULA, PRESENT, mood);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 문장 끝 한글 어절 하나를 푼다. @param {string} word @param {string | null} [previous] @returns {Predicate | null} */
|
|
150
|
+
export function parsePredicate(word, previous = null) {
|
|
151
|
+
if (!word || !hangul.isSyllable(word.at(-1))) return null;
|
|
152
|
+
if ((word === "다" || word === "입니다") && previous) return predicate("", COPULA);
|
|
153
|
+
if (word === "아닙니다" || word === "아니다") return predicate("아니", ADJECTIVE);
|
|
154
|
+
if (word.endsWith("입니다") && word.length > 3) return copulaOrIVerb(word.slice(0, -3), DECLARATIVE);
|
|
155
|
+
if (word.endsWith("입니까") && word.length > 3) return copulaOrIVerb(word.slice(0, -3), QUESTION);
|
|
156
|
+
if (word.endsWith("습니다")) return classifyPredicate(word.slice(0, -3), previous, DECLARATIVE);
|
|
157
|
+
if (word.endsWith("습니까")) return classifyPredicate(word.slice(0, -3), previous, QUESTION);
|
|
158
|
+
if (word.endsWith("니다") && endsWithFinal(word, 3, hangul.BIEUP)) {
|
|
159
|
+
return classifyPredicate(restoreRieul(stemBefore(word, 3)), previous, DECLARATIVE);
|
|
160
|
+
}
|
|
161
|
+
if (word.endsWith("니까") && endsWithFinal(word, 3, hangul.BIEUP)) {
|
|
162
|
+
return classifyPredicate(restoreRieul(stemBefore(word, 3)), previous, QUESTION);
|
|
163
|
+
}
|
|
164
|
+
if (word.endsWith("까요") && word.length > 2) return predicate(word.slice(0, -1), VERB, PRESENT, KKA_QUESTION);
|
|
165
|
+
if (word.endsWith("십시오") && word.length > 3) {
|
|
166
|
+
return predicate(restoreRieul(stripEu(word.slice(0, -3))), VERB, PRESENT, IMPERATIVE);
|
|
167
|
+
}
|
|
168
|
+
if (word.endsWith("세요") && word.length > 2) {
|
|
169
|
+
return predicate(restoreRieul(stripEu(word.slice(0, -2))), VERB, PRESENT, IMPERATIVE);
|
|
170
|
+
}
|
|
171
|
+
if (word.endsWith("읍시다")) return predicate(word.slice(0, -3), VERB, PRESENT, PROPOSITIVE);
|
|
172
|
+
if (word.endsWith("시다") && endsWithFinal(word, 3, hangul.BIEUP)) {
|
|
173
|
+
return predicate(restoreRieul(stemBefore(word, 3)), VERB, PRESENT, PROPOSITIVE);
|
|
174
|
+
}
|
|
175
|
+
if (word.endsWith("는다") && word.length > 2) return predicate(word.slice(0, -2), VERB);
|
|
176
|
+
if (word.endsWith("는가") && word.length > 2) return classifyPredicate(word.slice(0, -2), previous, QUESTION);
|
|
177
|
+
if (word.endsWith("자") && word.length > 1) {
|
|
178
|
+
const stem = restoreRieul(word.slice(0, -1));
|
|
179
|
+
if (stem.endsWith("하") || stem.endsWith("보") || rieulStems().has(stem)) {
|
|
180
|
+
return predicate(stem, VERB, PRESENT, PROPOSITIVE);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (word.endsWith("다") && word.length > 1) {
|
|
184
|
+
const stem = word.slice(0, -1);
|
|
185
|
+
const last = stem.at(-1);
|
|
186
|
+
if (hangul.isSyllable(last) && hangul.finalOf(last) === hangul.NIEUN && !word.endsWith("이다")) {
|
|
187
|
+
return predicate(restoreRieul(stemBefore(stem, 1)), VERB);
|
|
188
|
+
}
|
|
189
|
+
const tense = tenseOf(stem);
|
|
190
|
+
if (tense !== PRESENT) return predicate(stem, VERB, tense);
|
|
191
|
+
if (word.endsWith("이다") && word.length > 2) return predicate(word.slice(0, -2), COPULA, PRESENT, DECLARATIVE, true);
|
|
192
|
+
if (stem === "않") return predicate(stem, kindOf(stem, previous));
|
|
193
|
+
if (isAdjective(stem)) return predicate(stem, ADJECTIVE);
|
|
194
|
+
return predicate(stem, COPULA);
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 어간에 아/어를 붙인 꼴. @param {string} stem */
|
|
200
|
+
export function conjugate(stem) {
|
|
201
|
+
if (stem === "아니") return "아니에";
|
|
202
|
+
if (stem.endsWith("하")) return stem.slice(0, -1) + "해";
|
|
203
|
+
const last = hangul.lastSyllable(stem);
|
|
204
|
+
if (last === null) return stem + "어";
|
|
205
|
+
const [, vowel, final] = hangul.split(last);
|
|
206
|
+
const kind = irregularClass(stem);
|
|
207
|
+
if (kind === "르" && stem.length >= 2 && hangul.isSyllable(stem.at(-2))) {
|
|
208
|
+
const before = stem.at(-2);
|
|
209
|
+
const bright = hangul.BRIGHT.has(hangul.vowelOf(before));
|
|
210
|
+
return stem.slice(0, -2) + hangul.withFinal(before, hangul.RIEUL) + (bright ? "라" : "러");
|
|
211
|
+
}
|
|
212
|
+
if (kind === "ㅂ") return stem.slice(0, -1) + hangul.withFinal(last, hangul.NONE) + "워";
|
|
213
|
+
if (kind === "ㅂ와") return stem.slice(0, -1) + hangul.withFinal(last, hangul.NONE) + "와";
|
|
214
|
+
if (kind === "ㄷ") return stem.slice(0, -1) + hangul.withFinal(last, hangul.RIEUL) + (hangul.BRIGHT.has(vowel) ? "아" : "어");
|
|
215
|
+
if (kind === "ㅅ") return stem.slice(0, -1) + hangul.withFinal(last, hangul.NONE) + (hangul.BRIGHT.has(vowel) ? "아" : "어");
|
|
216
|
+
if (kind === "ㅎ") {
|
|
217
|
+
for (const [known, changed] of HAE_IRREGULAR) {
|
|
218
|
+
if (stem.endsWith(known)) return stem.slice(0, -known.length) + changed;
|
|
219
|
+
}
|
|
220
|
+
const opened = hangul.withFinal(last, hangul.NONE);
|
|
221
|
+
const target = new Map([
|
|
222
|
+
[hangul.A, hangul.AE],
|
|
223
|
+
[hangul.YA, hangul.YAE],
|
|
224
|
+
[hangul.EO, hangul.E],
|
|
225
|
+
[hangul.YEO, hangul.YE],
|
|
226
|
+
]).get(vowel) ?? hangul.AE;
|
|
227
|
+
return stem.slice(0, -1) + hangul.withVowel(opened, target);
|
|
228
|
+
}
|
|
229
|
+
if (final !== hangul.NONE) return stem + (hangul.BRIGHT.has(vowel) ? "아" : "어");
|
|
230
|
+
if ([hangul.A, hangul.YA, hangul.EO, hangul.YEO, hangul.AE, hangul.E, hangul.YE].includes(vowel)) return stem;
|
|
231
|
+
if (vowel === hangul.OH) return stem.slice(0, -1) + hangul.withVowel(last, hangul.WA);
|
|
232
|
+
if (vowel === hangul.U) return stem.slice(0, -1) + hangul.withVowel(last, hangul.WO);
|
|
233
|
+
if (vowel === hangul.OE) return stem.slice(0, -1) + hangul.withVowel(last, hangul.WAE);
|
|
234
|
+
if (vowel === hangul.IH) return stem.slice(0, -1) + hangul.withVowel(last, hangul.YEO);
|
|
235
|
+
if (vowel === hangul.EU) {
|
|
236
|
+
const before = stem.length >= 2 && hangul.isSyllable(stem.at(-2)) ? stem.at(-2) : null;
|
|
237
|
+
const bright = before !== null && hangul.BRIGHT.has(hangul.vowelOf(before));
|
|
238
|
+
return stem.slice(0, -1) + hangul.withVowel(last, bright ? hangul.A : hangul.EO);
|
|
239
|
+
}
|
|
240
|
+
return stem + "어";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {string} stem */
|
|
244
|
+
function hasFinal(stem) {
|
|
245
|
+
const last = hangul.lastSyllable(stem);
|
|
246
|
+
return last !== null ? hangul.finalOf(last) : hangul.MIEUM;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @param {string} stem */
|
|
250
|
+
function polite(stem) {
|
|
251
|
+
const final = hasFinal(stem);
|
|
252
|
+
if (final === hangul.NONE || final === hangul.RIEUL) {
|
|
253
|
+
return stem.slice(0, -1) + hangul.withFinal(stem.at(-1), hangul.BIEUP) + "니다";
|
|
254
|
+
}
|
|
255
|
+
return stem + "습니다";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @param {string} stem */
|
|
259
|
+
function plainVerb(stem) {
|
|
260
|
+
const final = hasFinal(stem);
|
|
261
|
+
if (final === hangul.NONE || final === hangul.RIEUL) {
|
|
262
|
+
return stem.slice(0, -1) + hangul.withFinal(stem.at(-1), hangul.NIEUN) + "다";
|
|
263
|
+
}
|
|
264
|
+
return stem + "는다";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @param {string} stem @param {string} ending */
|
|
268
|
+
function withEu(stem, ending) {
|
|
269
|
+
const final = hasFinal(stem);
|
|
270
|
+
if (final === hangul.NONE) return stem + ending;
|
|
271
|
+
if (final === hangul.RIEUL) {
|
|
272
|
+
const base = ending === "세요" ? stem.slice(0, -1) + hangul.withFinal(stem.at(-1), hangul.NONE) : stem;
|
|
273
|
+
return base + ending;
|
|
274
|
+
}
|
|
275
|
+
const kind = irregularClass(stem);
|
|
276
|
+
const last = stem.at(-1);
|
|
277
|
+
if (kind === "ㅂ" || kind === "ㅂ와") return stem.slice(0, -1) + hangul.withFinal(last, hangul.NONE) + "우" + ending;
|
|
278
|
+
if (kind === "ㄷ") return stem.slice(0, -1) + hangul.withFinal(last, hangul.RIEUL) + "으" + ending;
|
|
279
|
+
if (kind === "ㅅ") return stem.slice(0, -1) + hangul.withFinal(last, hangul.NONE) + "으" + ending;
|
|
280
|
+
return stem + "으" + ending;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 서술어를 target 문체로 짠다. @param {Predicate} value @param {string} target */
|
|
284
|
+
export function render(value, target) {
|
|
285
|
+
const { base, kind, tense, mood } = value;
|
|
286
|
+
if (mood === KKA_QUESTION) return target === HANDA ? base : base + "요";
|
|
287
|
+
if (tense !== PRESENT) {
|
|
288
|
+
if (target === HAPNIDA) return base + (mood === QUESTION ? "습니까" : "습니다");
|
|
289
|
+
if (target === HANDA) return base + (mood === QUESTION ? "는가" : "다");
|
|
290
|
+
return base + "어요";
|
|
291
|
+
}
|
|
292
|
+
if (kind === COPULA) {
|
|
293
|
+
if (!base) return { [HAPNIDA]: "입니다", [HANDA]: "다", [HAEYO]: "예요" }[target];
|
|
294
|
+
const final = hasFinal(base);
|
|
295
|
+
if (target === HAPNIDA) return base + (mood === QUESTION ? "입니까" : "입니다");
|
|
296
|
+
if (target === HANDA) {
|
|
297
|
+
if (mood === QUESTION) return base + "인가";
|
|
298
|
+
return base + (value.explicitCopula || final !== hangul.NONE ? "이다" : "다");
|
|
299
|
+
}
|
|
300
|
+
return base + (final !== hangul.NONE ? "이에요" : "예요");
|
|
301
|
+
}
|
|
302
|
+
if (mood === IMPERATIVE) return target === HANDA ? withEu(base, "라") : withEu(base, "세요");
|
|
303
|
+
if (mood === PROPOSITIVE) {
|
|
304
|
+
if (target === HAPNIDA) {
|
|
305
|
+
const final = hasFinal(base);
|
|
306
|
+
if (final === hangul.NONE || final === hangul.RIEUL) {
|
|
307
|
+
return base.slice(0, -1) + hangul.withFinal(base.at(-1), hangul.BIEUP) + "시다";
|
|
308
|
+
}
|
|
309
|
+
return base + "읍시다";
|
|
310
|
+
}
|
|
311
|
+
if (target === HANDA) return base + "자";
|
|
312
|
+
return conjugate(base) + "요";
|
|
313
|
+
}
|
|
314
|
+
if (target === HAPNIDA) {
|
|
315
|
+
const form = polite(base);
|
|
316
|
+
return mood === QUESTION ? form.slice(0, -2) + "니까" : form;
|
|
317
|
+
}
|
|
318
|
+
if (target === HANDA) {
|
|
319
|
+
if (kind === ADJECTIVE) {
|
|
320
|
+
if (mood === QUESTION) {
|
|
321
|
+
const final = hasFinal(base);
|
|
322
|
+
if (final === hangul.NONE || final === hangul.RIEUL) {
|
|
323
|
+
return base.slice(0, -1) + hangul.withFinal(base.at(-1), hangul.NIEUN) + "가";
|
|
324
|
+
}
|
|
325
|
+
return base + "은가";
|
|
326
|
+
}
|
|
327
|
+
return base + "다";
|
|
328
|
+
}
|
|
329
|
+
if (mood === QUESTION) {
|
|
330
|
+
const stem = hasFinal(base) === hangul.RIEUL ? base.slice(0, -1) + hangul.withFinal(base.at(-1), hangul.NONE) : base;
|
|
331
|
+
return stem + "는가";
|
|
332
|
+
}
|
|
333
|
+
return plainVerb(base);
|
|
334
|
+
}
|
|
335
|
+
return conjugate(base) + "요";
|
|
336
|
+
}
|