hanlint 0.0.16 → 0.0.18
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/README.md +12 -10
- package/data/calqueFrame.json +60 -0
- package/data/cliches.json +10 -0
- package/data/collocationStops.txt +28 -0
- package/data/easyWords.json +0 -234
- package/data/exemplars.json +23 -2
- package/data/koreanProse.txt +64 -0
- package/data/patterns.json +2 -2
- package/data/phraseRepeat.json +11 -0
- package/data/ruleCategories.json +2 -0
- package/data/ruleDocs.json +18 -16
- package/data/ruleMechanisms.json +2 -0
- package/data/screenNarration.json +7 -7
- package/data/screenSentence.json +56 -8
- package/data/screenTone.json +4 -4
- package/data/screenWord.json +48 -88
- package/data/spacing.json +1 -1
- package/data/usageCounts.report.json +1676 -0
- package/data/version.json +1 -1
- package/package.json +1 -1
- package/src/analysis/index.js +1 -1
- package/src/analysis/tokenize.js +30 -15
- package/src/cli/help.js +3 -2
- package/src/cli/main.js +227 -88
- package/src/cli/walk.js +1 -1
- package/src/cli/welcome.js +1 -1
- package/src/config/settings.js +17 -0
- package/src/data/profiles.js +2 -2
- package/src/data/prose.js +27 -0
- package/src/document/sourceText.js +199 -36
- package/src/fingerprint/build.js +13 -4
- package/src/fingerprint/dictionaries.js +31 -4
- package/src/fingerprint/markers.js +22 -0
- package/src/index.js +3 -1
- package/src/report/compactReport.js +7 -3
- package/src/report/jsonReport.js +2 -2
- package/src/report/sheet.js +0 -0
- package/src/report/textReport.js +27 -8
- package/src/report/writingSpec.js +3 -3
- package/src/rules/registry.js +5 -1
- package/src/rules/screen/screenWord.js +2 -1
- package/src/rules/sentence/calqueFrame.js +11 -0
- package/src/rules/sentence/cliche.js +7 -3
- package/src/rules/sentence/hardWord.js +6 -3
- package/src/rules/sentence/japaneseLoan.js +7 -3
- package/src/rules/sentence/nounPile.js +10 -4
- package/src/rules/sentence/phraseRepeat.js +53 -0
- package/src/rules/sentence/redundantPair.js +7 -3
- package/src/rules/sentence/translationese.js +7 -3
- package/src/rules/shared/dictionaryRule.js +37 -6
- package/src/text.js +1 -1
- package/src/usage/counts.js +54 -0
- package/src/usage/index.js +8 -0
- package/src/usage/sentences.js +681 -0
package/src/report/textReport.js
CHANGED
|
@@ -35,21 +35,40 @@ function candidateLines(findings) {
|
|
|
35
35
|
return lines;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** 접은 확인 권고. 규칙마다 개수와 줄 번호 한 줄. 뜻은 파이썬 textReport.py 의 noticeLines 가 소유한다. @param {import("../rules/finding.js").Finding[]} notices */
|
|
39
|
+
function noticeLines(notices) {
|
|
40
|
+
/** @type {Map<string, number[]>} */
|
|
41
|
+
const byRule = new Map();
|
|
42
|
+
for (const finding of notices) {
|
|
43
|
+
if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
|
|
44
|
+
/** @type {number[]} */ (byRule.get(finding.rule)).push(finding.line);
|
|
45
|
+
}
|
|
46
|
+
const lines = [`확인 권고 ${notices.length}건 (접음. 다 보려면 --notices)`];
|
|
47
|
+
for (const rule of [...byRule.keys()].sort()) {
|
|
48
|
+
const numbers = [...new Set(/** @type {number[]} */ (byRule.get(rule)))].sort((a, b) => a - b);
|
|
49
|
+
lines.push(` ${rule} ${/** @type {number[]} */ (byRule.get(rule)).length} ${numbers.join(", ")}줄`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] @param {boolean} [unfoldNotices] */
|
|
55
|
+
export function renderText(path, findings, register = null, preset = null, customExemplars = [], unfoldNotices = false) {
|
|
56
|
+
if (!findings.length) return `${path} 오류 없음`;
|
|
41
57
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
42
58
|
const notices = findings.length - errors;
|
|
43
|
-
const summary = `${path}
|
|
59
|
+
const summary = `${path} 오류 ${errors}건` + (notices ? `, 확인 권고 ${notices}건` : "");
|
|
44
60
|
const lines = [summary, ""];
|
|
45
|
-
|
|
61
|
+
const folded = unfoldNotices ? [] : findings.filter((f) => f.severity === "notice");
|
|
62
|
+
const shown = findings.filter((f) => unfoldNotices || f.severity === "error");
|
|
63
|
+
for (const f of shown) {
|
|
46
64
|
const tag = `[${f.rule}]` + (f.severity === "notice" ? " 확인" : "");
|
|
47
65
|
lines.push(`${path}:${f.line} ${tag}`, ` ${f.quote}`, ` ${f.why}`);
|
|
48
|
-
if (f.fix) lines.push(` 고친
|
|
66
|
+
if (f.fix) lines.push(` ${f.severity === "notice" ? "검토할 대안" : "고친 뒤"}: ${f.fix}`);
|
|
49
67
|
lines.push("");
|
|
50
68
|
}
|
|
51
|
-
lines.push(...
|
|
52
|
-
|
|
69
|
+
if (folded.length) lines.push(...noticeLines(folded), "");
|
|
70
|
+
lines.push(...exemplarLines(shown, register, preset, customExemplars));
|
|
71
|
+
const candidates = candidateLines(shown);
|
|
53
72
|
if (candidates.length) lines.push("", ...candidates);
|
|
54
73
|
return lines.join("\n").replace(/\n+$/, "");
|
|
55
74
|
}
|
|
@@ -34,7 +34,7 @@ function selectedProfile(config) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* 현재
|
|
37
|
+
* 현재 규칙 세트를 쓰기 전 숫자 사양으로 편다. 품질 점수나 새 임계는 만들지 않는다.
|
|
38
38
|
* @param {import("./settings.js").Config} [config]
|
|
39
39
|
* @param {string} [register]
|
|
40
40
|
* @param {number | null} [targetChars]
|
|
@@ -161,11 +161,11 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** 사람이 읽는
|
|
164
|
+
/** 사람이 읽는 형식. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
|
|
165
165
|
export function renderWritingSpec(spec) {
|
|
166
166
|
const target = spec.targetChars !== null ? `, ${spec.targetChars}자` : "";
|
|
167
167
|
const lines = [
|
|
168
|
-
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은
|
|
168
|
+
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은 규칙 세트의 임계와 ${spec.profile} 프로파일을 쓰기 전 숫자로 편다`,
|
|
169
169
|
];
|
|
170
170
|
const width = Math.max(...spec.rows.map((row) => row.label.length));
|
|
171
171
|
for (const row of spec.rows) lines.push(` ${row.label.padEnd(width)} ${row.guidance}`);
|
package/src/rules/registry.js
CHANGED
|
@@ -24,9 +24,11 @@ import * as spacing from "./orthography/spacing.js";
|
|
|
24
24
|
import * as spelling from "./orthography/spelling.js";
|
|
25
25
|
import * as factListParagraph from "./paragraph/factListParagraph.js";
|
|
26
26
|
import * as paraFragment from "./paragraph/paraFragment.js";
|
|
27
|
+
import * as calqueFrame from "./sentence/calqueFrame.js";
|
|
27
28
|
import * as cliche from "./sentence/cliche.js";
|
|
28
29
|
import * as hardWord from "./sentence/hardWord.js";
|
|
29
30
|
import * as connectorRepeat from "./sentence/connectorRepeat.js";
|
|
31
|
+
import * as phraseRepeat from "./sentence/phraseRepeat.js";
|
|
30
32
|
import * as danglingDeixis from "./sentence/danglingDeixis.js";
|
|
31
33
|
import * as draftHistory from "./sentence/draftHistory.js";
|
|
32
34
|
import * as numberOrphan from "./sentence/numberOrphan.js";
|
|
@@ -70,8 +72,10 @@ import * as screenWord from "./screen/screenWord.js";
|
|
|
70
72
|
|
|
71
73
|
/** @type {Rule[]} */
|
|
72
74
|
export const RULES = [
|
|
75
|
+
calqueFrame,
|
|
73
76
|
cliche,
|
|
74
77
|
connectorRepeat,
|
|
78
|
+
phraseRepeat,
|
|
75
79
|
danglingDeixis,
|
|
76
80
|
dash,
|
|
77
81
|
draftHistory,
|
|
@@ -133,7 +137,7 @@ export const RULES = [
|
|
|
133
137
|
export const MECHANISMS = {
|
|
134
138
|
dictionary: "사전. 낱말 목록과 정규식이 맞는 자리",
|
|
135
139
|
repeat: "반복. 같은 모양이 창 안에서 N 번",
|
|
136
|
-
threshold: "셈.
|
|
140
|
+
threshold: "셈. 분석표의 값이 임계를 넘거나 모양이 계약과 다른 자리",
|
|
137
141
|
contrast: "대조. 두 자리를 맞대 어긋난 곳",
|
|
138
142
|
reader: "독자 상태. 문장 순서대로 독자가 손에 든 것과 본 것에 그 자리가 요구하는 것을 맞댄다",
|
|
139
143
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
3
4
|
|
|
4
5
|
export const name = "screenWord";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
8
|
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
9
|
export function run(doc) {
|
|
9
|
-
return overridingFindings(doc, "screenWord", name);
|
|
10
|
+
return overridingFindings(doc, "screenWord", name, undefined, NOTICE);
|
|
10
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "calqueFrame";
|
|
6
|
+
export const mechanism = "dictionary";
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
return overridingFindings(doc, "calqueFrame", name, undefined, NOTICE);
|
|
11
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "cliche";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "cliches", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -5,7 +5,10 @@ import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
|
5
5
|
export const name = "hardWord";
|
|
6
6
|
export const mechanism = "dictionary";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "easyWords", name, NOTICE, config);
|
|
11
14
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "japaneseLoan";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "japaneseLoan", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { nounRuns } from "../../analysis/index.js";
|
|
3
|
+
import { attested, usageKindOf } from "../../usage/counts.js";
|
|
2
4
|
import { SENTENCE, finding } from "../finding.js";
|
|
3
5
|
|
|
4
6
|
export const name = "nounPile";
|
|
@@ -10,12 +12,16 @@ export const mechanism = "threshold";
|
|
|
10
12
|
*/
|
|
11
13
|
export function run(doc, config) {
|
|
12
14
|
const findings = [];
|
|
15
|
+
const kind = usageKindOf(config);
|
|
13
16
|
for (const sentence of doc.sentences) {
|
|
14
|
-
if (sentence.nounRun
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
17
|
+
if (sentence.nounRun < config.nounPileMin) continue;
|
|
18
|
+
if (kind) {
|
|
19
|
+
const piles = nounRuns(sentence.text).filter(([, length]) => length >= config.nounPileMin).map(([chain]) => chain);
|
|
20
|
+
if (piles.length && piles.every((chain) => attested(chain, kind, config.usageMin))) continue;
|
|
18
21
|
}
|
|
22
|
+
findings.push(
|
|
23
|
+
finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
|
|
24
|
+
);
|
|
19
25
|
}
|
|
20
26
|
return findings;
|
|
21
27
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { loadEntries } from "../../data/load.js";
|
|
3
|
+
import { compile } from "../../regex.js";
|
|
4
|
+
import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
5
|
+
|
|
6
|
+
export const name = "phraseRepeat";
|
|
7
|
+
export const mechanism = "repeat";
|
|
8
|
+
|
|
9
|
+
/** 산문 문장이 이 수보다 적으면 비율이 튄다. 뜻은 파이썬 rules/sentence/phraseRepeat.py 가 소유한다. */
|
|
10
|
+
const MIN_SENTENCES = 8;
|
|
11
|
+
/** 되풀이로 세려면 몇 번 나와야 하는가. 한 번은 버릇이 아니다. */
|
|
12
|
+
const MIN_HITS = 2;
|
|
13
|
+
|
|
14
|
+
/** @returns {{pattern: import("../../regex.js").Pattern, share: number, label: string, why: string, source: string}[]} */
|
|
15
|
+
function entries() {
|
|
16
|
+
return loadEntries("phraseRepeat.json").map((one) => ({
|
|
17
|
+
pattern: compile(String(one.pattern)),
|
|
18
|
+
share: Number(one.share),
|
|
19
|
+
label: String(one.label),
|
|
20
|
+
why: String(one.why),
|
|
21
|
+
source: String(one.source),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
27
|
+
* @param {import("../../config/settings.js").Config} config
|
|
28
|
+
*/
|
|
29
|
+
export function run(doc, config) {
|
|
30
|
+
void config;
|
|
31
|
+
const findings = [];
|
|
32
|
+
const prose = doc.sentences.filter((one) => one.length > 0 && one.ending !== "없음");
|
|
33
|
+
if (prose.length < MIN_SENTENCES) return findings;
|
|
34
|
+
for (const { pattern, share, label, why, source } of entries()) {
|
|
35
|
+
const hit = prose.filter((one) => pattern.search(one.text) !== null);
|
|
36
|
+
if (hit.length < MIN_HITS || hit.length / prose.length <= share) continue;
|
|
37
|
+
const lines = [...new Set(hit.map((one) => one.line))].sort((a, b) => a - b).join(", ");
|
|
38
|
+
const share100 = `${Math.round(share * 100)}%`;
|
|
39
|
+
findings.push(
|
|
40
|
+
finding(
|
|
41
|
+
name,
|
|
42
|
+
hit[0].line,
|
|
43
|
+
hit[0].text,
|
|
44
|
+
`\`${label}\` 꼴이 산문 ${prose.length}문장 가운데 ${hit.length}번이다 (${lines}줄. 상한 ${share100}). ${why} (${source})`,
|
|
45
|
+
null,
|
|
46
|
+
NOTICE,
|
|
47
|
+
DOCUMENT,
|
|
48
|
+
-1,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return findings;
|
|
53
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "redundantPair";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "redundantPair", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "translationese";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "translationese", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** 사전 규칙의 공통 구현. cliche, translationese, redundantPair, japaneseLoan 이 사전 이름만 바꿔 쓴다. */
|
|
3
3
|
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
4
|
+
import { conventional, usageKindOf } from "../../usage/counts.js";
|
|
4
5
|
import { ERROR, SENTENCE, finding } from "../finding.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -41,11 +42,24 @@ function matchFinding(sentence, match, ruleName, severity) {
|
|
|
41
42
|
);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* 사전의 매치를 지적으로. config 를 주면 그 종류의 관용 항목 (usage.conventional) 은 넘긴다. 뜻은 파이썬
|
|
47
|
+
* rules/shared/dictionaryRule.py 의 dictionaryFindings 가 소유한다.
|
|
48
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
49
|
+
* @param {string} dictionary
|
|
50
|
+
* @param {string} ruleName
|
|
51
|
+
* @param {string} [severity]
|
|
52
|
+
* @param {import("../../config/settings.js").Config | null} [config]
|
|
53
|
+
* @returns {import("../finding.js").Finding[]}
|
|
54
|
+
*/
|
|
55
|
+
export function dictionaryFindings(doc, dictionary, ruleName, severity = ERROR, config = null) {
|
|
45
56
|
const findings = [];
|
|
57
|
+
const kind = config ? usageKindOf(config) : null;
|
|
46
58
|
for (const sentence of doc.sentences) {
|
|
47
59
|
for (const match of sentence.matches) {
|
|
48
|
-
if (match.dictionary
|
|
60
|
+
if (match.dictionary !== dictionary) continue;
|
|
61
|
+
if (kind && config && conventional(dictionary, match.pattern, kind, config.usageShare)) continue;
|
|
62
|
+
findings.push(matchFinding(sentence, match, ruleName, severity));
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
return findings;
|
|
@@ -57,9 +71,10 @@ export function dictionaryFindings(doc, dictionary, ruleName, severity = ERROR)
|
|
|
57
71
|
* @param {string} dictionary
|
|
58
72
|
* @param {string} ruleName
|
|
59
73
|
* @param {string} [severity]
|
|
74
|
+
* @param {string | null} [builtinSeverity]
|
|
60
75
|
* @returns {import("../finding.js").Finding[]}
|
|
61
76
|
*/
|
|
62
|
-
export function overridingFindings(doc, dictionary, ruleName, severity = ERROR) {
|
|
77
|
+
export function overridingFindings(doc, dictionary, ruleName, severity = ERROR, builtinSeverity = null) {
|
|
63
78
|
const findings = [];
|
|
64
79
|
for (const sentence of doc.sentences) {
|
|
65
80
|
/** @type {Map<string, import("../../fingerprint/dictionaries.js").DictionaryMatch>} */
|
|
@@ -67,14 +82,15 @@ export function overridingFindings(doc, dictionary, ruleName, severity = ERROR)
|
|
|
67
82
|
for (const match of sentence.matches) {
|
|
68
83
|
if (match.dictionary === dictionary) chosen.set(`${match.start}:${match.end}`, match);
|
|
69
84
|
}
|
|
70
|
-
for (const match of [...chosen.values()].sort((a, b) => a.start - b.start)) findings.push(matchFinding(sentence, match, ruleName, severity));
|
|
85
|
+
for (const match of [...chosen.values()].sort((a, b) => a.start - b.start)) findings.push(matchFinding(sentence, match, ruleName, match.pattern && builtinSeverity ? builtinSeverity : severity));
|
|
71
86
|
}
|
|
72
87
|
return findings;
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
91
|
* 문장마다 가장 앞의 매치 하나만 지적한다. 뜻은 파이썬 rules/shared/dictionaryRule.py 의 firstMatchFindings 가 소유한다.
|
|
77
|
-
* 구체 무늬와 종결어미가 한 사전에 같이 사는 화면 사전에 쓴다.
|
|
92
|
+
* 구체 무늬와 종결어미가 한 사전에 같이 사는 화면 사전에 쓴다. 항목에 to 규칙이 있으면 문장 전체를 낱말로 다시 쓴
|
|
93
|
+
* 제안을 fix 로 낸다 (진행, 실패, 요구, 완료). 뜻을 골라야 하는 일반 종결어미는 to 가 없어 fix 도 없다.
|
|
78
94
|
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
79
95
|
* @param {string} dictionary
|
|
80
96
|
* @param {string} ruleName
|
|
@@ -86,18 +102,33 @@ export function firstMatchFindings(doc, dictionary, ruleName, severity = ERROR)
|
|
|
86
102
|
for (const sentence of doc.sentences) {
|
|
87
103
|
const match = sentence.matches.find((m) => m.dictionary === dictionary);
|
|
88
104
|
if (!match) continue;
|
|
105
|
+
const [fragment, replacement] = match.rewrite ? changedSpan(sentence.text, match.rewrite) : [null, null];
|
|
89
106
|
findings.push(
|
|
90
107
|
finding(
|
|
91
108
|
ruleName,
|
|
92
109
|
sentence.line + countNewlines(sentence.text.slice(0, match.start)),
|
|
93
110
|
sentence.text,
|
|
94
111
|
`\`${match.text}\` ${match.why} (${match.source})`,
|
|
95
|
-
|
|
112
|
+
match.rewrite,
|
|
96
113
|
severity,
|
|
97
114
|
SENTENCE,
|
|
98
115
|
sentence.index,
|
|
116
|
+
fragment,
|
|
117
|
+
replacement,
|
|
99
118
|
),
|
|
100
119
|
);
|
|
101
120
|
}
|
|
102
121
|
return findings;
|
|
103
122
|
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 두 글의 공통 앞뒤를 뺀 바뀐 조각. 편집기의 `이대로 고치기` 가 이 조각만 바꾼다. 뜻은 파이썬 changedSpan 이 소유한다.
|
|
126
|
+
* @param {string} before @param {string} after @returns {[string, string]}
|
|
127
|
+
*/
|
|
128
|
+
export function changedSpan(before, after) {
|
|
129
|
+
let head = 0;
|
|
130
|
+
while (head < Math.min(before.length, after.length) && before[head] === after[head]) head++;
|
|
131
|
+
let tail = 0;
|
|
132
|
+
while (tail < Math.min(before.length, after.length) - head && before[before.length - 1 - tail] === after[after.length - 1 - tail]) tail++;
|
|
133
|
+
return [before.slice(head, before.length - tail), after.slice(head, after.length - tail)];
|
|
134
|
+
}
|
package/src/text.js
CHANGED
|
@@ -64,7 +64,7 @@ export function countIn(text, needle, end = text.length) {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* 파이썬 round(value, digits). 정확한 이진 값을 십진으로 풀어 반올림하고 정확히 절반이면 짝수로 간다.
|
|
67
|
-
* Math.round 는 절반을 위로 올려 13/16 을 0.813 으로 만들고 파이썬은 0.812 다. 두
|
|
67
|
+
* Math.round 는 절반을 위로 올려 13/16 을 0.813 으로 만들고 파이썬은 0.812 다. 두 구현의 글자가 같으려면 파이썬 쪽이다.
|
|
68
68
|
* toFixed(100) 은 1e21 아래의 수를 정확한 십진 전개로 준다. 지문의 비율과 평균이 사는 범위다.
|
|
69
69
|
* @param {number} value
|
|
70
70
|
* @param {number} digits
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 명사 연쇄 빈도표. 글 종류마다 data/usageCounts.<종류>.json 하나이고 값은 그 연쇄가 나온 문서 수다.
|
|
4
|
+
* 뜻은 파이썬 usage/counts.py 가 소유한다. 표의 키는 nounRuns 의 어절들을 빈칸 하나로 이은 것이다.
|
|
5
|
+
*/
|
|
6
|
+
import { USAGE_OF } from "../config/settings.js";
|
|
7
|
+
import { readText } from "../data/load.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {{ kind: string, source: string, documents: number, minLength: number, minDocuments: number, chains: Record<string, number>, patterns?: Record<string, Record<string, number>> }} UsageTable */
|
|
10
|
+
|
|
11
|
+
/** @type {Map<string, UsageTable>} */
|
|
12
|
+
const tables = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 이 설정이 볼 빈도표의 종류. usageKind 가 null 이면 프리셋이 정하고 "" 면 보지 않는다.
|
|
16
|
+
* @param {import("../config/settings.js").Config} config @returns {string | null}
|
|
17
|
+
*/
|
|
18
|
+
export function usageKindOf(config) {
|
|
19
|
+
const kind = config.usageKind === null ? USAGE_OF[config.preset] : config.usageKind;
|
|
20
|
+
return kind || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 표 전체. @param {string} kind @returns {UsageTable} */
|
|
24
|
+
export function usageTable(kind) {
|
|
25
|
+
let table = tables.get(kind);
|
|
26
|
+
if (!table) {
|
|
27
|
+
table = JSON.parse(readText(`usageCounts.${kind}.json`));
|
|
28
|
+
tables.set(kind, /** @type {UsageTable} */ (table));
|
|
29
|
+
}
|
|
30
|
+
return /** @type {UsageTable} */ (table);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 이 연쇄가 그 종류의 말뭉치에서 몇 편의 문서에 나왔나. 표에 없으면 0. @param {string[]} chain @param {string} kind */
|
|
34
|
+
export function chainDocuments(chain, kind) {
|
|
35
|
+
return usageTable(kind).chains[chain.join(" ")] ?? 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 연쇄가 minimum 편 이상의 문서에 나왔으면 그 종류의 관용이다. @param {string[]} chain @param {string} kind @param {number} minimum */
|
|
39
|
+
export function attested(chain, kind, minimum) {
|
|
40
|
+
return chainDocuments(chain, kind) >= minimum;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 사전 항목이 그 종류의 말뭉치에서 몇 편의 문서에 나왔나. @param {string} dictionary @param {string} pattern @param {string} kind */
|
|
44
|
+
export function patternDocuments(dictionary, pattern, kind) {
|
|
45
|
+
return usageTable(kind).patterns?.[dictionary]?.[pattern] ?? 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 사전 항목이 그 종류의 문서 share 이상에 나오면 관용이다. 뜻은 파이썬 usage/counts.py 의 conventional 이 소유한다.
|
|
50
|
+
* @param {string} dictionary @param {string} pattern @param {string} kind @param {number} share
|
|
51
|
+
*/
|
|
52
|
+
export function conventional(dictionary, pattern, kind, share) {
|
|
53
|
+
return patternDocuments(dictionary, pattern, kind) >= share * usageTable(kind).documents;
|
|
54
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 용례 (usage). 실제 글에서 낱말이 어떻게 쓰였는지를 든다. 뜻은 파이썬 usage/ 가 소유한다.
|
|
4
|
+
* counts 는 실리는 빈도표, sentences 는 사용자 기계의 문장 역인덱스다.
|
|
5
|
+
* fingerprint 와 같은 층이다. rules 와 cli 가 쓰고, analysis 와 config 와 data 만 쓴다.
|
|
6
|
+
*/
|
|
7
|
+
export { attested, chainDocuments, conventional, patternDocuments, usageKindOf, usageTable } from "./counts.js";
|
|
8
|
+
export { UsageIndex, buildIndex, defaultRoot as defaultUsageRoot, indexTokens, listIndexes, loadIndex, queryCores, readDocuments } from "./sentences.js";
|