hanlint 0.0.17 → 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.
Files changed (54) hide show
  1. package/README.md +12 -10
  2. package/data/calqueFrame.json +60 -0
  3. package/data/cliches.json +10 -0
  4. package/data/collocationStops.txt +28 -0
  5. package/data/easyWords.json +0 -234
  6. package/data/exemplars.json +23 -2
  7. package/data/koreanProse.txt +64 -0
  8. package/data/patterns.json +2 -2
  9. package/data/phraseRepeat.json +11 -0
  10. package/data/ruleCategories.json +2 -0
  11. package/data/ruleDocs.json +18 -16
  12. package/data/ruleMechanisms.json +2 -0
  13. package/data/screenNarration.json +7 -7
  14. package/data/screenSentence.json +56 -8
  15. package/data/screenTone.json +4 -4
  16. package/data/screenWord.json +48 -88
  17. package/data/spacing.json +1 -1
  18. package/data/usageCounts.report.json +1676 -0
  19. package/data/version.json +1 -1
  20. package/package.json +1 -1
  21. package/src/analysis/index.js +1 -1
  22. package/src/analysis/tokenize.js +30 -15
  23. package/src/cli/help.js +3 -2
  24. package/src/cli/main.js +227 -88
  25. package/src/cli/walk.js +1 -1
  26. package/src/cli/welcome.js +1 -1
  27. package/src/config/settings.js +17 -0
  28. package/src/data/profiles.js +2 -2
  29. package/src/data/prose.js +27 -0
  30. package/src/document/sourceText.js +104 -6
  31. package/src/fingerprint/build.js +13 -4
  32. package/src/fingerprint/dictionaries.js +31 -4
  33. package/src/fingerprint/markers.js +22 -0
  34. package/src/index.js +3 -1
  35. package/src/report/compactReport.js +7 -3
  36. package/src/report/jsonReport.js +2 -2
  37. package/src/report/sheet.js +0 -0
  38. package/src/report/textReport.js +27 -8
  39. package/src/report/writingSpec.js +3 -3
  40. package/src/rules/registry.js +5 -1
  41. package/src/rules/screen/screenWord.js +2 -1
  42. package/src/rules/sentence/calqueFrame.js +11 -0
  43. package/src/rules/sentence/cliche.js +7 -3
  44. package/src/rules/sentence/hardWord.js +6 -3
  45. package/src/rules/sentence/japaneseLoan.js +7 -3
  46. package/src/rules/sentence/nounPile.js +10 -4
  47. package/src/rules/sentence/phraseRepeat.js +53 -0
  48. package/src/rules/sentence/redundantPair.js +7 -3
  49. package/src/rules/sentence/translationese.js +7 -3
  50. package/src/rules/shared/dictionaryRule.js +37 -6
  51. package/src/text.js +1 -1
  52. package/src/usage/counts.js +54 -0
  53. package/src/usage/index.js +8 -0
  54. package/src/usage/sentences.js +681 -0
@@ -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
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
9
- export function run(doc) {
10
- return dictionaryFindings(doc, "easyWords", name, NOTICE);
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
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "japaneseLoan", name);
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 >= config.nounPileMin) {
15
- findings.push(
16
- finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
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
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "redundantPair", name);
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
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "translationese", name);
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
- export function dictionaryFindings(doc, dictionary, ruleName, severity = ERROR) {
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 === dictionary) findings.push(matchFinding(sentence, match, ruleName, severity));
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
- * 구체 무늬와 종결어미가 한 사전에 같이 사는 화면 사전에 쓴다. fix 는 내지 않는다.
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
- null,
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";