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.
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 +199 -36
  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
@@ -35,21 +35,40 @@ function candidateLines(findings) {
35
35
  return lines;
36
36
  }
37
37
 
38
- /** @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] */
39
- export function renderText(path, findings, register = null, preset = null, customExemplars = []) {
40
- if (!findings.length) return `${path} 집은 자리 없음`;
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} 집은 자리 ${errors}` + (notices ? `, 확인할 자리 ${notices}` : "");
59
+ const summary = `${path} 오류 ${errors}건` + (notices ? `, 확인 권고 ${notices}건` : "");
44
60
  const lines = [summary, ""];
45
- for (const f of findings) {
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(` 고친 뒤: ${f.fix}`);
66
+ if (f.fix) lines.push(` ${f.severity === "notice" ? "검토할 대안" : "고친 뒤"}: ${f.fix}`);
49
67
  lines.push("");
50
68
  }
51
- lines.push(...exemplarLines(findings, register, preset, customExemplars));
52
- const candidates = candidateLines(findings);
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
- /** 사람이 읽는 꼴. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
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}. 같은 규칙판의 임계와 ${spec.profile} 프로파일을 쓰기 전 숫자로 편다`,
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}`);
@@ -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
- /** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
8
- export function run(doc) {
9
- return dictionaryFindings(doc, "cliches", 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, "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
- /** @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";