hanspell 0.9.6 → 0.10.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/{LICENSE-MIT → LICENSE} +1 -1
- package/README.md +38 -17
- package/lib/cli.js +11 -10
- package/lib/daum-spell-check.js +43 -36
- package/lib/index.d.ts +38 -0
- package/lib/index.js +2 -2
- package/lib/naver-spell-check.js +161 -0
- package/package.json +19 -15
- package/.eslintrc.yml +0 -15
- package/.prettierrc +0 -6
- package/.travis.yml +0 -6
- package/lib/pnu-spell-check.js +0 -106
package/{LICENSE-MIT → LICENSE}
RENAMED
package/README.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# hanspell
|
|
2
2
|
|
|
3
|
-
`hanspell`은 (주)다음과
|
|
3
|
+
`hanspell`은 (주)다음과 네이버(주)의 웹 서비스를 이용한 한글 맞춤법 검사기입니다.
|
|
4
4
|
|
|
5
5
|
[비주얼 스튜디오 코드 한스펠](https://github.com/9beach/vscode-hanspell)과 [하스켈](https://www.haskell.org/)로 작성한 [hanspell-hs](https://github.com/9beach/hanspell-hs)도 있으니 참고하세요.
|
|
6
6
|
|
|
7
|
-
[](https://badge.fury.io/js/hanspell)
|
|
8
8
|
|
|
9
9
|
## 설치
|
|
10
10
|
|
|
11
|
-
[
|
|
11
|
+
[한스펠 릴리스](https://github.com/9beach/hanspell/releases)에서 실행파일을 다운로드 하세요.
|
|
12
|
+
|
|
13
|
+
소스를 받아서 설치하려면 [Node.js](https://nodejs.org/ko/) 18 이상을 설치한 뒤 다음을 실행하세요.
|
|
12
14
|
|
|
13
15
|
```sh
|
|
14
16
|
npm install -g hanspell
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
Node.js 환경에 따라 `sudo` 명령이 필요할 수도
|
|
19
|
+
Node.js 환경에 따라 `sudo` 명령이 필요할 수도 있습니다.
|
|
18
20
|
|
|
19
21
|
```sh
|
|
20
22
|
sudo npm install -g hanspell
|
|
@@ -24,11 +26,11 @@ sudo npm install -g hanspell
|
|
|
24
26
|
|
|
25
27
|
```console
|
|
26
28
|
$ hanspell-cli -h
|
|
27
|
-
사용법: hanspell-cli [-d | -
|
|
29
|
+
사용법: hanspell-cli [-d | -n | -a | -h]
|
|
28
30
|
|
|
29
31
|
옵션:
|
|
30
32
|
-d, --daum [default] 다음 서비스를 이용해서 맞춤법을 교정합니다
|
|
31
|
-
-
|
|
33
|
+
-n, --naver 네이버 서비스를 이용해서 맞춤법을 교정합니다
|
|
32
34
|
-a, --all 두 서비스의 모든 결과를 반영해서 맞춤법을 교정합니다
|
|
33
35
|
-h, --info 도움말을 출력합니다
|
|
34
36
|
|
|
@@ -133,7 +135,7 @@ Node.js 프로젝트에서 `hanspell` 라이브러리를 사용하려면 다음
|
|
|
133
135
|
cd my-project && npm install --save hanspell
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
`hanspell` 라이브러리에는 `spellCheckByDAUM` 함수와 `
|
|
138
|
+
`hanspell` 라이브러리에는 `spellCheckByDAUM` 함수와 `spellCheckByNAVER` 함수가 있습니다. 다음은 사용 예입니다.
|
|
137
139
|
|
|
138
140
|
```javascript
|
|
139
141
|
// hanspell-example.js
|
|
@@ -148,10 +150,10 @@ const error = function (err) {
|
|
|
148
150
|
};
|
|
149
151
|
|
|
150
152
|
hanspell.spellCheckByDAUM(sentence, 6000, console.log, end, error);
|
|
151
|
-
hanspell.
|
|
153
|
+
hanspell.spellCheckByNAVER(sentence, 6000, console.log, end, error);
|
|
152
154
|
```
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
다음과 비슷한 결과가 예상됩니다.
|
|
155
157
|
|
|
156
158
|
```console
|
|
157
159
|
[
|
|
@@ -167,23 +169,42 @@ hanspell.spellCheckByPNU(sentence, 6000, console.log, end, error);
|
|
|
167
169
|
{
|
|
168
170
|
token: '리랜드는',
|
|
169
171
|
suggestions: [ '이랜드는' ],
|
|
170
|
-
info: '
|
|
172
|
+
info: '맞춤법 오류입니다.'
|
|
171
173
|
},
|
|
172
174
|
{
|
|
173
|
-
token: '
|
|
174
|
-
suggestions: [ '굵은
|
|
175
|
-
info: '
|
|
175
|
+
token: '굵은게,',
|
|
176
|
+
suggestions: [ '굵은 게,' ],
|
|
177
|
+
info: '띄어쓰기 오류입니다.'
|
|
176
178
|
}
|
|
177
179
|
]
|
|
178
180
|
// check ends
|
|
179
181
|
```
|
|
180
182
|
|
|
181
|
-
두 함수의 호출 결과는 모두 `token`, `suggestions` 속성을 가집니다.
|
|
182
|
-
`spellCheckByDAUM`은 `type`, `context`
|
|
183
|
+
두 함수의 호출 결과는 모두 `token`, `suggestions`, `info` 속성을 가집니다.
|
|
184
|
+
`spellCheckByDAUM`은 `type`, `context` 속성을 추가로 가집니다.
|
|
185
|
+
|
|
186
|
+
TypeScript 사용자는 별도 설정 없이 타입이 자동으로 인식됩니다.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { spellCheckByNAVER, NaverTypo } from 'hanspell';
|
|
190
|
+
|
|
191
|
+
spellCheckByNAVER(
|
|
192
|
+
'안뇽하세요.',
|
|
193
|
+
6000,
|
|
194
|
+
(data: NaverTypo[]) => console.log(data),
|
|
195
|
+
() => console.log('// check ends'),
|
|
196
|
+
(err) => console.error(err),
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
네이버 검사기는 교정 후보를 항상 하나만 제공하고, `info`는 오류 분류(맞춤법/
|
|
202
|
+
띄어쓰기/표준어 추천)만 알려 줍니다.
|
|
183
203
|
|
|
184
|
-
위의 예시에서 `sentence`가
|
|
204
|
+
위의 예시에서 `sentence`가 250 단어 또는 1000자를 넘으면, 인자로 전달된
|
|
185
205
|
`console.log`는 여러 번 호출되지만 `end`는 마지막에 한 번만 호출됩니다.
|
|
186
206
|
|
|
187
207
|
## 라이선스 고지
|
|
188
208
|
|
|
189
|
-
이 프로그램의 소스 코드는 MIT 라이선스를
|
|
209
|
+
이 프로그램의 소스 코드는 MIT 라이선스를 따릅니다. 다음과 네이버의 맞춤법 웹
|
|
210
|
+
서비스는 각 제공사의 이용 약관에 따라 사용해야 합니다.
|
package/lib/cli.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const chalk = require('chalk');
|
|
8
8
|
const fs = require('fs');
|
|
9
|
-
const minimatch = require('minimatch');
|
|
9
|
+
const { minimatch } = require('minimatch');
|
|
10
10
|
const readline = require('readline');
|
|
11
11
|
|
|
12
12
|
const hanspell = require('./index');
|
|
@@ -20,6 +20,7 @@ const typomap = new Map();
|
|
|
20
20
|
// Node v8 supports zero-length lookahead and lookbehind assertions.
|
|
21
21
|
const lookaround = (() => {
|
|
22
22
|
try {
|
|
23
|
+
// eslint-disable-next-line prefer-regex-literals
|
|
23
24
|
RegExp('(?<=[^ㄱ-ㅎㅏ-ㅣ가-힣])test(?=[^ㄱ-ㅎㅏ-ㅣ가-힣])');
|
|
24
25
|
return true;
|
|
25
26
|
} catch (err) {
|
|
@@ -94,15 +95,15 @@ function checkDAUM() {
|
|
|
94
95
|
hanspell.spellCheckByDAUM(sentence, HTTP_TIMEOUT, fixTypos, printSentence);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
// Spell check by
|
|
98
|
-
function
|
|
99
|
-
hanspell.
|
|
98
|
+
// Spell check by NAVER service.
|
|
99
|
+
function checkNAVER() {
|
|
100
|
+
hanspell.spellCheckByNAVER(sentence, HTTP_TIMEOUT, fixTypos, printSentence);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
// Spell check by
|
|
103
|
+
// Spell check by both NAVER and DAUM services.
|
|
103
104
|
function checkAll() {
|
|
104
105
|
const input = sentence;
|
|
105
|
-
hanspell.
|
|
106
|
+
hanspell.spellCheckByNAVER(input, HTTP_TIMEOUT, fixTypos, () =>
|
|
106
107
|
hanspell.spellCheckByDAUM(input, HTTP_TIMEOUT, fixTypos, printSentence),
|
|
107
108
|
);
|
|
108
109
|
}
|
|
@@ -151,11 +152,11 @@ function readAndCheck(check) {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
const HELP = `사용법: hanspell-cli [-d | -
|
|
155
|
+
const HELP = `사용법: hanspell-cli [-d | -n | -a | -h]
|
|
155
156
|
|
|
156
157
|
옵션:
|
|
157
158
|
-d, --daum [default] 다음 서비스를 이용해서 맞춤법을 교정합니다
|
|
158
|
-
-
|
|
159
|
+
-n, --naver 네이버 서비스를 이용해서 맞춤법을 교정합니다
|
|
159
160
|
-a, --all 두 서비스의 모든 결과를 반영해서 맞춤법을 교정합니다
|
|
160
161
|
-h, --info 도움말을 출력합니다
|
|
161
162
|
|
|
@@ -175,8 +176,8 @@ process.argv.slice(2).forEach((opt) => {
|
|
|
175
176
|
readAndCheck(checkAll);
|
|
176
177
|
} else if (opt === '-d' || opt === '--daum') {
|
|
177
178
|
readAndCheck(checkDAUM);
|
|
178
|
-
} else if (opt === '-
|
|
179
|
-
readAndCheck(
|
|
179
|
+
} else if (opt === '-n' || opt === '--naver') {
|
|
180
|
+
readAndCheck(checkNAVER);
|
|
180
181
|
} else {
|
|
181
182
|
console.log(HELP);
|
|
182
183
|
process.exit(1);
|
package/lib/daum-spell-check.js
CHANGED
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* @fileOverview Interface for DAUM spell checker.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const request = require('request');
|
|
5
|
+
const { decode } = require('html-entities');
|
|
7
6
|
|
|
8
7
|
const split = require('./split-string').byLength;
|
|
9
8
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
9
|
+
const DAUM_URL = 'https://dic.daum.net/grammar_checker.do';
|
|
10
|
+
const DAUM_MAX_CHARS = 1000;
|
|
11
|
+
const DAUM_MIN_INTERVAL = 400;
|
|
12
|
+
const DAUM_UA =
|
|
13
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
14
|
+
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
12
15
|
|
|
13
16
|
// Parses attribute from server response.
|
|
14
17
|
function getAttr(string, key) {
|
|
@@ -64,65 +67,69 @@ function parseJSON(response) {
|
|
|
64
67
|
return typos;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
+
async function postOne(sentence, timeout) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(DAUM_URL, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
'User-Agent': DAUM_UA,
|
|
78
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
79
|
+
},
|
|
80
|
+
body: new URLSearchParams({ sentence }),
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
const body = await res.text();
|
|
84
|
+
return { status: res.status, body };
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
70
89
|
|
|
71
90
|
// Splits a long sentence, and makes spell check requests to the server.
|
|
72
91
|
// `check` is called at each short sentence with the parsed JSON parameter.
|
|
73
92
|
function spellCheck(sentence, timeout, check, end, error) {
|
|
74
93
|
if (sentence.length === 0) {
|
|
75
|
-
if (end
|
|
76
|
-
end();
|
|
77
|
-
}
|
|
94
|
+
if (end) end();
|
|
78
95
|
return;
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
// Removes HTML tags.
|
|
99
|
+
const cleaned = sentence.replace(/<[^ㄱ-ㅎㅏ-ㅣ가-힣>]+>/g, '');
|
|
100
|
+
const data = split(cleaned, '.,\n', DAUM_MAX_CHARS);
|
|
82
101
|
let count = data.length;
|
|
83
102
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
const handle = async (part) => {
|
|
104
|
+
try {
|
|
105
|
+
const { status, body } = await postOne(part, timeout);
|
|
106
|
+
if (status !== 200) throw new Error(`HTTP ${status}`);
|
|
87
107
|
if (body.indexOf('="screen_out">맞춤법 검사기 본문</h2>') === -1) {
|
|
88
108
|
console.error(
|
|
89
109
|
`-- 한스펠 오류: 다음 서비스가 유효하지 않은 양식을 반환했습니다. (${DAUM_URL})`,
|
|
90
110
|
);
|
|
91
|
-
|
|
92
|
-
if (error) error(err);
|
|
111
|
+
if (error) error(new Error('invalid response'));
|
|
93
112
|
} else {
|
|
94
113
|
check(parseJSON(body));
|
|
95
114
|
}
|
|
96
|
-
}
|
|
115
|
+
} catch (err) {
|
|
97
116
|
console.error(
|
|
98
117
|
'-- 한스펠 오류: 다음 서버의 접속 오류로 일부 문장 교정에 실패했습니다.',
|
|
99
118
|
);
|
|
100
119
|
if (error) error(err);
|
|
120
|
+
} finally {
|
|
121
|
+
count -= 1;
|
|
122
|
+
if (count === 0 && end) end();
|
|
101
123
|
}
|
|
102
|
-
if (count === 0 && end !== null) end();
|
|
103
124
|
};
|
|
104
125
|
|
|
105
126
|
let i = 0;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
request.post(
|
|
109
|
-
{
|
|
110
|
-
url: DAUM_URL,
|
|
111
|
-
timeout,
|
|
112
|
-
form: {
|
|
113
|
-
sentence: data[i],
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
getResponse,
|
|
117
|
-
);
|
|
118
|
-
|
|
127
|
+
function next() {
|
|
128
|
+
handle(data[i]);
|
|
119
129
|
i += 1;
|
|
120
|
-
if (i < data.length)
|
|
121
|
-
setTimeout(post, DAUM_MIN_INTERVAL);
|
|
122
|
-
}
|
|
130
|
+
if (i < data.length) setTimeout(next, DAUM_MIN_INTERVAL);
|
|
123
131
|
}
|
|
124
|
-
|
|
125
|
-
post();
|
|
132
|
+
next();
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
module.exports = spellCheck;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for hanspell.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SpellCheckTypo {
|
|
6
|
+
token: string;
|
|
7
|
+
suggestions: string[];
|
|
8
|
+
info?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DaumTypo extends SpellCheckTypo {
|
|
12
|
+
type: string;
|
|
13
|
+
context: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type NaverTypo = SpellCheckTypo;
|
|
17
|
+
|
|
18
|
+
export type CheckCallback<T extends SpellCheckTypo = SpellCheckTypo> = (
|
|
19
|
+
data: T[],
|
|
20
|
+
) => void;
|
|
21
|
+
export type EndCallback = () => void;
|
|
22
|
+
export type ErrorCallback = (err: Error | unknown) => void;
|
|
23
|
+
|
|
24
|
+
export function spellCheckByDAUM(
|
|
25
|
+
sentence: string,
|
|
26
|
+
timeout: number,
|
|
27
|
+
check: CheckCallback<DaumTypo>,
|
|
28
|
+
end?: EndCallback | null,
|
|
29
|
+
error?: ErrorCallback,
|
|
30
|
+
): void;
|
|
31
|
+
|
|
32
|
+
export function spellCheckByNAVER(
|
|
33
|
+
sentence: string,
|
|
34
|
+
timeout: number,
|
|
35
|
+
check: CheckCallback<NaverTypo>,
|
|
36
|
+
end?: EndCallback | null,
|
|
37
|
+
error?: ErrorCallback,
|
|
38
|
+
): void;
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const spellCheckByDAUM = require('./daum-spell-check');
|
|
6
|
-
const
|
|
6
|
+
const spellCheckByNAVER = require('./naver-spell-check');
|
|
7
7
|
|
|
8
|
-
module.exports = { spellCheckByDAUM,
|
|
8
|
+
module.exports = { spellCheckByDAUM, spellCheckByNAVER };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileOverview Interface for Naver spell checker.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { decode } = require('html-entities');
|
|
6
|
+
|
|
7
|
+
const split = require('./split-string').byWordCount;
|
|
8
|
+
|
|
9
|
+
const NAVER_MAX_WORDS = 250;
|
|
10
|
+
const NAVER_PROXY_URL =
|
|
11
|
+
'https://m.search.naver.com/p/csearch/ocontent/util/SpellerProxy';
|
|
12
|
+
const NAVER_PASSPORT_PAGE =
|
|
13
|
+
'https://search.naver.com/search.naver?query=%EB%A7%9E%EC%B6%A4%EB%B2%95+%EA%B2%80%EC%82%AC%EA%B8%B0';
|
|
14
|
+
const NAVER_UA =
|
|
15
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
16
|
+
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
17
|
+
|
|
18
|
+
const COLOR_INFO = {
|
|
19
|
+
red: '맞춤법 오류입니다.',
|
|
20
|
+
green: '띄어쓰기 오류입니다.',
|
|
21
|
+
blue: '표준어 의심이거나 대치어 추천입니다.',
|
|
22
|
+
violet: '통계적 교정입니다.',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let cachedPassportKey = null;
|
|
26
|
+
|
|
27
|
+
async function fetchPassportKey() {
|
|
28
|
+
const res = await fetch(NAVER_PASSPORT_PAGE, {
|
|
29
|
+
headers: { 'User-Agent': NAVER_UA },
|
|
30
|
+
});
|
|
31
|
+
const body = await res.text();
|
|
32
|
+
const m = body.match(/passportKey=([a-f0-9]+)/);
|
|
33
|
+
if (!m) {
|
|
34
|
+
throw new Error('네이버 passportKey 추출 실패');
|
|
35
|
+
}
|
|
36
|
+
return m[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function getPassportKey(forceRefresh) {
|
|
40
|
+
if (!forceRefresh && cachedPassportKey) return cachedPassportKey;
|
|
41
|
+
cachedPassportKey = await fetchPassportKey();
|
|
42
|
+
return cachedPassportKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Strips the `jQuery(...)` JSONP wrapper.
|
|
46
|
+
function unwrapJsonp(body) {
|
|
47
|
+
const start = body.indexOf('(');
|
|
48
|
+
const end = body.lastIndexOf(')');
|
|
49
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
50
|
+
throw new Error('네이버 응답 파싱 실패');
|
|
51
|
+
}
|
|
52
|
+
return JSON.parse(body.substring(start + 1, end));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parses Naver response into `[{ token, suggestions, info }]`.
|
|
56
|
+
function parseResult(result) {
|
|
57
|
+
if (!result || result.errata_count === 0) return [];
|
|
58
|
+
|
|
59
|
+
const origins = [];
|
|
60
|
+
const reSpan = /<span class='result_underline'>([\s\S]*?)<\/span>/g;
|
|
61
|
+
let m = reSpan.exec(result.origin_html);
|
|
62
|
+
while (m !== null) {
|
|
63
|
+
origins.push(m[1]);
|
|
64
|
+
m = reSpan.exec(result.origin_html);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fixes = [];
|
|
68
|
+
const reEm = /<em class='([a-z]+)_text'>([\s\S]*?)<\/em>/g;
|
|
69
|
+
m = reEm.exec(result.html);
|
|
70
|
+
while (m !== null) {
|
|
71
|
+
fixes.push({ color: m[1], text: m[2] });
|
|
72
|
+
m = reEm.exec(result.html);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const len = Math.min(origins.length, fixes.length);
|
|
76
|
+
const typos = [];
|
|
77
|
+
for (let i = 0; i < len; i += 1) {
|
|
78
|
+
typos.push({
|
|
79
|
+
token: decode(origins[i]),
|
|
80
|
+
suggestions: [decode(fixes[i].text)],
|
|
81
|
+
info: COLOR_INFO[fixes[i].color] || '',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return typos;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function callNaver(text, timeout) {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
90
|
+
|
|
91
|
+
const tryOnce = async () => {
|
|
92
|
+
const key = await getPassportKey(false);
|
|
93
|
+
const url = `${NAVER_PROXY_URL}?_callback=jQuery&q=${encodeURIComponent(
|
|
94
|
+
text,
|
|
95
|
+
)}&where=nexearch&color_blindness=0&passportKey=${key}`;
|
|
96
|
+
return fetch(url, {
|
|
97
|
+
headers: {
|
|
98
|
+
'User-Agent': NAVER_UA,
|
|
99
|
+
Referer: 'https://search.naver.com/',
|
|
100
|
+
},
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
let res = await tryOnce();
|
|
107
|
+
let body = await res.text();
|
|
108
|
+
if (body.includes('유효한 키가 아닙니다')) {
|
|
109
|
+
cachedPassportKey = null;
|
|
110
|
+
res = await tryOnce();
|
|
111
|
+
body = await res.text();
|
|
112
|
+
}
|
|
113
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
114
|
+
return body;
|
|
115
|
+
} finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Splits a long sentence, and makes spell check requests to the server.
|
|
121
|
+
// `check` is called at each short sentence with the parsed array.
|
|
122
|
+
function spellCheck(sentence, timeout, check, end, error) {
|
|
123
|
+
if (sentence.length === 0) {
|
|
124
|
+
if (end) end();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Removes HTML tags.
|
|
129
|
+
const cleaned = sentence.replace(/<[^ㄱ-ㅎㅏ-ㅣ가-힣>]+>/g, '');
|
|
130
|
+
const parts = split(cleaned, NAVER_MAX_WORDS);
|
|
131
|
+
let count = parts.length;
|
|
132
|
+
|
|
133
|
+
const handleOne = async (part) => {
|
|
134
|
+
try {
|
|
135
|
+
const body = await callNaver(part, timeout);
|
|
136
|
+
const json = unwrapJsonp(body);
|
|
137
|
+
const result = json && json.message && json.message.result;
|
|
138
|
+
if (!result) {
|
|
139
|
+
const err = (json && json.message && json.message.error) || '응답 없음';
|
|
140
|
+
console.error(
|
|
141
|
+
`-- 한스펠 오류: 네이버 서비스가 유효하지 않은 양식을 반환했습니다. (${err})`,
|
|
142
|
+
);
|
|
143
|
+
if (error) error(new Error(err));
|
|
144
|
+
} else {
|
|
145
|
+
check(parseResult(result));
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(
|
|
149
|
+
'-- 한스펠 오류: 네이버 서버의 접속 오류로 일부 문장 교정에 실패했습니다.',
|
|
150
|
+
);
|
|
151
|
+
if (error) error(err);
|
|
152
|
+
} finally {
|
|
153
|
+
count -= 1;
|
|
154
|
+
if (count === 0 && end) end();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
parts.forEach(handleOne);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = spellCheck;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hanspell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "(주)다음과
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "(주)다음과 네이버(주)의 온라인 서비스를 이용한 한글 맞춤법 검사기.",
|
|
5
5
|
"homepage": "https://github.com/9beach/hanspell",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "9beach",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"main": "lib/index.js",
|
|
19
|
+
"types": "lib/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"lib/"
|
|
22
|
+
],
|
|
19
23
|
"scripts": {
|
|
20
24
|
"all": "npm run lint && npm test",
|
|
21
25
|
"test": "mocha && test/cli.test.sh",
|
|
@@ -25,6 +29,9 @@
|
|
|
25
29
|
"bin": {
|
|
26
30
|
"hanspell-cli": "lib/cli.js"
|
|
27
31
|
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
28
35
|
"keywords": [
|
|
29
36
|
"한글",
|
|
30
37
|
"맞춤법",
|
|
@@ -38,20 +45,17 @@
|
|
|
38
45
|
"SpellCheck"
|
|
39
46
|
],
|
|
40
47
|
"dependencies": {
|
|
41
|
-
"chalk": "^
|
|
42
|
-
"html-entities": "^
|
|
43
|
-
"minimatch": "^
|
|
44
|
-
"readline": "^1.3.0",
|
|
45
|
-
"request": "^2.65.0",
|
|
46
|
-
"unescape": "^1.0.1"
|
|
48
|
+
"chalk": "^4.1.2",
|
|
49
|
+
"html-entities": "^2.6.0",
|
|
50
|
+
"minimatch": "^10.2.5"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
|
-
"eslint": "^
|
|
50
|
-
"eslint-config-airbnb-base": "^
|
|
51
|
-
"eslint-config-prettier": "^
|
|
52
|
-
"eslint-plugin-import": "^2.
|
|
53
|
-
"eslint-plugin-prettier": "^
|
|
54
|
-
"mocha": "^
|
|
55
|
-
"prettier": "^
|
|
53
|
+
"eslint": "^8.57.1",
|
|
54
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
55
|
+
"eslint-config-prettier": "^9.1.0",
|
|
56
|
+
"eslint-plugin-import": "^2.32.0",
|
|
57
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
58
|
+
"mocha": "^11.7.6",
|
|
59
|
+
"prettier": "^3.8.3"
|
|
56
60
|
}
|
|
57
61
|
}
|
package/.eslintrc.yml
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
env:
|
|
2
|
-
browser: true
|
|
3
|
-
commonjs: true
|
|
4
|
-
es2021: true
|
|
5
|
-
mocha: true
|
|
6
|
-
extends:
|
|
7
|
-
- eslint:recommended
|
|
8
|
-
- airbnb-base
|
|
9
|
-
- plugin:prettier/recommended
|
|
10
|
-
parserOptions:
|
|
11
|
-
ecmaVersion: 12
|
|
12
|
-
rules:
|
|
13
|
-
max-len: ["error", {"code": 80, "ignoreUrls": true}]
|
|
14
|
-
prettier/prettier: "error"
|
|
15
|
-
no-console: "off"
|
package/.prettierrc
DELETED
package/.travis.yml
DELETED
package/lib/pnu-spell-check.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileOverview Interface for Pusan National University spell checker.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const request = require('request');
|
|
6
|
-
const Entities = require('html-entities').AllHtmlEntities;
|
|
7
|
-
|
|
8
|
-
const split = require('./split-string').byWordCount;
|
|
9
|
-
|
|
10
|
-
const entities = new Entities();
|
|
11
|
-
const { decode } = entities;
|
|
12
|
-
|
|
13
|
-
// Parses server response.
|
|
14
|
-
function parseJSON(response) {
|
|
15
|
-
try {
|
|
16
|
-
return response
|
|
17
|
-
.match(/\tdata = \[.*;/g)
|
|
18
|
-
.map((data) => JSON.parse(data.substring(8, data.length - 1)))[0][0]
|
|
19
|
-
.errInfo.map((pnutypo) => {
|
|
20
|
-
let suggestions = pnutypo.candWord.replace(/\|$/, '');
|
|
21
|
-
if (suggestions === '') {
|
|
22
|
-
suggestions = decode(pnutypo.orgStr);
|
|
23
|
-
}
|
|
24
|
-
const info = pnutypo.help
|
|
25
|
-
.replace(/< *[bB][rR] *\/>/g, '\n')
|
|
26
|
-
.replace(/\n\n/g, '\n')
|
|
27
|
-
.replace(/\n\(예\) /g, '\n(예)\n')
|
|
28
|
-
.replace(/ \(예\) /g, '\n(예)\n')
|
|
29
|
-
.replace(/ */g, '\n');
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
token: decode(pnutypo.orgStr),
|
|
33
|
-
suggestions: decode(suggestions).split('|'),
|
|
34
|
-
info: decode(info),
|
|
35
|
-
};
|
|
36
|
-
});
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if (
|
|
39
|
-
response.indexOf(
|
|
40
|
-
'기술적 한계로 찾지 못한 맞춤법 오류나 문법 오류가 있을 수 있습니다.',
|
|
41
|
-
) !== -1
|
|
42
|
-
) {
|
|
43
|
-
console.error(
|
|
44
|
-
'-- 한스펠 오류: 부산대 서비스가 유효하지 않은 양식을 반환했습니다.',
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const PNU_MAX_WORDS = 250;
|
|
53
|
-
const PNU_URL = 'http://speller.cs.pusan.ac.kr/results';
|
|
54
|
-
|
|
55
|
-
// Splits a long sentence, and makes spell check requests to the server.
|
|
56
|
-
// `check` is called at each short sentence with the parsed JSON parameter.
|
|
57
|
-
function spellCheck(sentence, timeout, check, end, error) {
|
|
58
|
-
if (sentence.length === 0) {
|
|
59
|
-
if (end !== null) {
|
|
60
|
-
end();
|
|
61
|
-
}
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Due to PNU server's weird behavior, replaces '\n' to '\n '.
|
|
66
|
-
const data = split(
|
|
67
|
-
`${sentence.replace(/([^\r])\n/g, '$1\r\n')}\r\n`,
|
|
68
|
-
PNU_MAX_WORDS,
|
|
69
|
-
);
|
|
70
|
-
let count = data.length;
|
|
71
|
-
|
|
72
|
-
const getResponse = (err, response, body) => {
|
|
73
|
-
if (!err && response.statusCode === 200) {
|
|
74
|
-
if (body.indexOf('<title>한국어 맞춤법/문법 검사기</title>') === -1) {
|
|
75
|
-
console.error(
|
|
76
|
-
`-- 한스펠 오류: 부산대 서비스가 유효하지 않은 양식을 반환했습니다. (${PNU_URL})`,
|
|
77
|
-
);
|
|
78
|
-
if (error) error(err);
|
|
79
|
-
} else {
|
|
80
|
-
check(parseJSON(body));
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
console.error(
|
|
84
|
-
'-- 한스펠 오류: 부산대 서버의 접속 오류로 일부 문장 교정에 실패했습니다.',
|
|
85
|
-
);
|
|
86
|
-
if (error) error(err);
|
|
87
|
-
}
|
|
88
|
-
count -= 1;
|
|
89
|
-
if (count === 0 && end !== null) end();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
data.forEach((part) =>
|
|
93
|
-
request.post(
|
|
94
|
-
{
|
|
95
|
-
url: PNU_URL,
|
|
96
|
-
timeout,
|
|
97
|
-
form: {
|
|
98
|
-
text1: part,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
getResponse,
|
|
102
|
-
),
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
module.exports = spellCheck;
|