hanlint 0.0.17 → 0.0.19

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 (136) hide show
  1. package/README.md +23 -20
  2. package/data/calqueFrame.json +60 -0
  3. package/data/cliches.json +39 -29
  4. package/data/collocationStops.txt +28 -0
  5. package/data/confusable.json +20 -20
  6. package/data/easyWords.json +12 -246
  7. package/data/exemplars.json +50 -29
  8. package/data/hiddenDeps.txt +11 -11
  9. package/data/japaneseLoan.json +27 -27
  10. package/data/koreanProse.txt +64 -0
  11. package/data/patterns.json +26 -26
  12. package/data/phraseRepeat.json +11 -0
  13. package/data/platformApis.txt +11 -11
  14. package/data/redundantPair.json +16 -16
  15. package/data/ruleCategories.json +2 -0
  16. package/data/ruleDocs.json +56 -54
  17. package/data/ruleMechanisms.json +2 -0
  18. package/data/screenNarration.json +10 -10
  19. package/data/screenSentence.json +60 -12
  20. package/data/screenTone.json +6 -6
  21. package/data/screenWord.json +70 -110
  22. package/data/spacing.json +12 -12
  23. package/data/spelling.json +29 -29
  24. package/data/translationese.json +23 -24
  25. package/data/usageCounts.report.json +1676 -0
  26. package/data/version.json +1 -1
  27. package/package.json +1 -1
  28. package/src/analysis/grammar/ending.js +1 -1
  29. package/src/analysis/grammar/hangul.js +4 -4
  30. package/src/analysis/grammar/josa.js +1 -1
  31. package/src/analysis/grammar/register.js +1 -1
  32. package/src/analysis/grammar/voice.js +1 -1
  33. package/src/analysis/index.js +3 -3
  34. package/src/analysis/josa.js +1 -1
  35. package/src/analysis/splitSentences.js +1 -1
  36. package/src/analysis/tokenize.js +41 -26
  37. package/src/baseline/store.js +8 -8
  38. package/src/cli/help.js +11 -10
  39. package/src/cli/main.js +332 -182
  40. package/src/cli/walk.js +2 -2
  41. package/src/cli/welcome.js +8 -8
  42. package/src/config/loadConfig.js +3 -3
  43. package/src/config/patch.js +6 -6
  44. package/src/config/readerContract.js +26 -26
  45. package/src/config/settings.js +44 -25
  46. package/src/config/toml.js +9 -9
  47. package/src/data/exemplars.js +21 -21
  48. package/src/data/load.js +6 -6
  49. package/src/data/operations.js +14 -14
  50. package/src/data/patches.js +10 -10
  51. package/src/data/patterns.js +5 -5
  52. package/src/data/profiles.js +6 -6
  53. package/src/data/prose.js +27 -0
  54. package/src/document/model.js +4 -4
  55. package/src/document/parseMarkdown.js +7 -7
  56. package/src/document/plainText.js +3 -3
  57. package/src/document/sourceText.js +117 -19
  58. package/src/edit/applyFixes.js +0 -0
  59. package/src/fingerprint/build.js +18 -9
  60. package/src/fingerprint/codeBlocks.js +3 -3
  61. package/src/fingerprint/codeMarkers.js +4 -4
  62. package/src/fingerprint/dictionaries.js +34 -7
  63. package/src/fingerprint/markers.js +27 -5
  64. package/src/fingerprint/readerState.js +9 -9
  65. package/src/fingerprint/sourceSentence.js +1 -1
  66. package/src/fingerprint/topics.js +1 -1
  67. package/src/guard/contract.js +6 -6
  68. package/src/guard/editLocality.js +1 -1
  69. package/src/guard/outline.js +1 -1
  70. package/src/guard/receipt.js +13 -13
  71. package/src/guard/surface.js +4 -4
  72. package/src/index.js +9 -7
  73. package/src/learn/edits.js +2 -2
  74. package/src/learn/operations.js +1 -1
  75. package/src/regex.js +2 -2
  76. package/src/report/compactReport.js +7 -3
  77. package/src/report/fingerprintJson.js +2 -2
  78. package/src/report/githubReport.js +1 -1
  79. package/src/report/jsonReport.js +3 -3
  80. package/src/report/registerMatch.js +2 -2
  81. package/src/report/sheet.js +0 -0
  82. package/src/report/textReport.js +35 -16
  83. package/src/report/writingSpec.js +17 -17
  84. package/src/rules/code/inputFileSource.js +1 -1
  85. package/src/rules/code/installImport.js +2 -2
  86. package/src/rules/code/platformApi.js +1 -1
  87. package/src/rules/document/countMismatch.js +2 -1
  88. package/src/rules/document/enoughOnce.js +2 -1
  89. package/src/rules/document/fieldEcho.js +10 -6
  90. package/src/rules/document/keywordHeading.js +1 -1
  91. package/src/rules/document/keywordMissing.js +1 -1
  92. package/src/rules/document/noQuestion.js +2 -2
  93. package/src/rules/document/promiseRecall.js +1 -1
  94. package/src/rules/document/tableOddCell.js +1 -1
  95. package/src/rules/finding.js +4 -4
  96. package/src/rules/paragraph/factListParagraph.js +1 -1
  97. package/src/rules/paragraph/paraFragment.js +1 -1
  98. package/src/rules/readerDebt.js +1 -1
  99. package/src/rules/registry.js +30 -26
  100. package/src/rules/screen/screenWord.js +2 -1
  101. package/src/rules/sentence/calqueFrame.js +11 -0
  102. package/src/rules/sentence/cliche.js +7 -3
  103. package/src/rules/sentence/connectorRepeat.js +2 -1
  104. package/src/rules/sentence/danglingDeixis.js +1 -1
  105. package/src/rules/sentence/dash.js +1 -1
  106. package/src/rules/sentence/deixis.js +1 -1
  107. package/src/rules/sentence/doubleNegative.js +2 -1
  108. package/src/rules/sentence/doublePassive.js +2 -1
  109. package/src/rules/sentence/draftHistory.js +2 -1
  110. package/src/rules/sentence/endingRepeat.js +3 -2
  111. package/src/rules/sentence/euiChain.js +1 -1
  112. package/src/rules/sentence/fillerOpener.js +1 -1
  113. package/src/rules/sentence/hardWord.js +6 -3
  114. package/src/rules/sentence/imperativePeriod.js +1 -1
  115. package/src/rules/sentence/japaneseLoan.js +7 -3
  116. package/src/rules/sentence/longSentence.js +1 -1
  117. package/src/rules/sentence/negationRedefine.js +1 -1
  118. package/src/rules/sentence/nounPile.js +10 -4
  119. package/src/rules/sentence/numberOrphan.js +1 -1
  120. package/src/rules/sentence/outsideProfile.js +8 -3
  121. package/src/rules/sentence/phraseRepeat.js +54 -0
  122. package/src/rules/sentence/redundantPair.js +7 -3
  123. package/src/rules/sentence/translationese.js +7 -3
  124. package/src/rules/shared/candidates.js +2 -2
  125. package/src/rules/shared/dictionaryRule.js +46 -8
  126. package/src/rules/shared/repeat.js +2 -2
  127. package/src/rules/structure/bridgeRepeat.js +3 -3
  128. package/src/rules/structure/emojiBullet.js +1 -1
  129. package/src/rules/structure/headingSkip.js +1 -1
  130. package/src/rules/structure/headingUniform.js +2 -1
  131. package/src/rules/structure/loneSubheading.js +1 -1
  132. package/src/rules/structure/moreLater.js +1 -1
  133. package/src/text.js +2 -2
  134. package/src/usage/counts.js +54 -0
  135. package/src/usage/index.js +8 -0
  136. package/src/usage/sentences.js +681 -0
@@ -1,18 +1,18 @@
1
1
  // @ts-check
2
2
  /**
3
- * 본보기. 규칙마다 고치기 전과 후의 짝. 파이썬 data/exemplars.py 와 같다.
3
+ * 예시. 규칙마다 고치기 전과 고친 뒤의 짝. 파이썬 data/exemplars.py와 같다.
4
4
  *
5
- * 지적은 무엇이 틀렸는지 말하고 본보기는 무엇이 맞는지 보인다. 자리표시자 규약은 fixture 와 같다.
6
- * `{em}` 은 긴 줄표, `{dot}` 은 마침표다.
5
+ * 지적은 무엇이 틀렸는지 말하고 예시는 무엇이 맞는지 보인다. 자리표시자 규칙은 fixture와 같다.
6
+ * `{em}`은 긴 줄표, `{dot}`은 마침표다.
7
7
  */
8
8
  import { loadEntries } from "./load.js";
9
9
 
10
10
  const EM_DASH = String.fromCharCode(0x2014);
11
11
  const EN_DASH = String.fromCharCode(0x2013);
12
12
  const ELLIPSIS_CHAR = String.fromCharCode(0x2026);
13
- /** 한 줄 본보기의 표시 폭 상한. 글자 수가 아니라 폭이다. 뜻은 파이썬 data/exemplars.py 가 소유한다. */
13
+ /** 한 줄 예시의 표시 폭 상한. 글자 수가 아니라 폭이다. 까닭은 파이썬 data/exemplars.py에 적었다. */
14
14
  const ONE_LINE_LIMIT = 96;
15
- /** 칸을 둘 먹는 글자. 한중일 W 와 F 다. 파이썬 unicodedata.east_asian_width 와 같은 범위를 든다. */
15
+ /** 두 칸을 차지하는 글자. 한중일 W와 F다. 파이썬 unicodedata.east_asian_width와 같은 범위다. */
16
16
  const WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/;
17
17
 
18
18
  /**
@@ -21,7 +21,7 @@ const WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF
21
21
  * @property {string} before 그 규칙에 실제로 잡히는 글
22
22
  * @property {string} after 같은 뜻이면서 잡히지 않는 글
23
23
  * @property {string} moved 무엇이 달라졌는지 한 마디
24
- * @property {string[]} presets 비어 있으면 기본, 값이 있으면 그 프리셋의 문맥 본보기
24
+ * @property {string[]} presets 비어 있으면 기본, 값이 있으면 그 프리셋의 문맥 예시
25
25
  */
26
26
 
27
27
  /** @param {string} text */
@@ -29,14 +29,14 @@ export function expand(text) {
29
29
  return text.split("{em}").join(EM_DASH).split("{en}").join(EN_DASH).split("{dot}").join(".");
30
30
  }
31
31
 
32
- /** 터미널이 먹는 칸 수. 한중일 글자는 둘, 나머지는 하나다. @param {string} text */
32
+ /** 터미널에서 차지하는 칸 수. 한중일 글자는 둘, 나머지는 하나다. @param {string} text */
33
33
  export function displayWidth(text) {
34
34
  let width = 0;
35
35
  for (const ch of text) width += WIDE.test(ch) ? 2 : 1;
36
36
  return width;
37
37
  }
38
38
 
39
- /** 한 줄로 보일 때의 꼴. 줄바꿈은 공백으로 눕히고 폭이 넘치면 자른다. @param {string} text @param {number} [limit] */
39
+ /** 한 줄로 보일 때의 모양. 줄바꿈은 공백으로 바꾸고 폭이 넘치면 자른다. @param {string} text @param {number} [limit] */
40
40
  export function shorten(text, limit = ONE_LINE_LIMIT) {
41
41
  const flat = text.split(/\s+/).filter(Boolean).join(" ");
42
42
  if (displayWidth(flat) <= limit) return flat;
@@ -61,7 +61,7 @@ let allCache = null;
61
61
  /** @type {Map<string, Exemplar> | null} */
62
62
  let defaultCache = null;
63
63
 
64
- /** 기본과 문맥 본보기를 모두 읽는다. @returns {Exemplar[]} */
64
+ /** 기본 예시와 문맥 예시를 모두 읽는다. @returns {Exemplar[]} */
65
65
  export function allExemplars() {
66
66
  if (!allCache) {
67
67
  allCache = [];
@@ -72,12 +72,12 @@ export function allExemplars() {
72
72
  const rule = /** @type {string} */ (entry.rule);
73
73
  const presets = /** @type {string[]} */ (entry.presets ?? []);
74
74
  if (!presets.length) {
75
- if (defaults.has(rule)) throw new Error(`기본 본보기가 겹친다: ${rule}`);
75
+ if (defaults.has(rule)) throw new Error(`기본 예시가 겹친다: ${rule}`);
76
76
  defaults.add(rule);
77
77
  } else {
78
78
  const used = contexts.get(rule) ?? new Set();
79
79
  const overlap = presets.filter((preset) => used.has(preset)).sort();
80
- if (overlap.length) throw new Error(`문맥 본보기의 프리셋이 겹친다: ${rule} ${overlap.join(", ")}`);
80
+ if (overlap.length) throw new Error(`문맥 예시의 프리셋이 겹친다: ${rule} ${overlap.join(", ")}`);
81
81
  for (const preset of presets) used.add(preset);
82
82
  contexts.set(rule, used);
83
83
  }
@@ -93,7 +93,7 @@ export function allExemplars() {
93
93
  return allCache;
94
94
  }
95
95
 
96
- /** 규칙 이름 → 본보기. @returns {Map<string, Exemplar>} */
96
+ /** 규칙 이름 → 예시. @returns {Map<string, Exemplar>} */
97
97
  export function exemplars() {
98
98
  if (!defaultCache) {
99
99
  defaultCache = new Map(
@@ -103,9 +103,9 @@ export function exemplars() {
103
103
  return defaultCache;
104
104
  }
105
105
 
106
- /** 설정의 `[[exemplars]]` 를 검증한다. @param {unknown} entries @param {string[]} presetNames @returns {Exemplar[]} */
106
+ /** 설정의 `[[exemplars]]`를 검증한다. @param {unknown} entries @param {string[]} presetNames @returns {Exemplar[]} */
107
107
  export function projectExemplars(entries, presetNames) {
108
- if (!Array.isArray(entries)) throw new Error("exemplars 는 [[exemplars]] 배열이다");
108
+ if (!Array.isArray(entries)) throw new Error("exemplars는 [[exemplars]] 배열이어야 한다");
109
109
  const allowedKeys = new Set(["rule", "before", "after", "moved", "presets"]);
110
110
  const knownRules = new Set(exemplars().keys());
111
111
  const knownPresets = new Set(presetNames);
@@ -114,7 +114,7 @@ export function projectExemplars(entries, presetNames) {
114
114
  const contexts = new Map();
115
115
  return entries.map((raw, offset) => {
116
116
  const index = offset + 1;
117
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`exemplars ${index}번째 항목은 표다`);
117
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`exemplars ${index}번째 항목은 표여야 한다`);
118
118
  const entry = /** @type {Record<string, unknown>} */ (raw);
119
119
  const unknown = Object.keys(entry).filter((key) => !allowedKeys.has(key)).sort();
120
120
  if (unknown.length) throw new Error(`exemplars ${index}번째 항목의 모르는 키: ${unknown.join(", ")}`);
@@ -123,7 +123,7 @@ export function projectExemplars(entries, presetNames) {
123
123
  for (const key of ["rule", "before", "after", "moved"]) {
124
124
  const value = entry[key];
125
125
  if (typeof value !== "string" || !value.trim()) {
126
- throw new Error(`exemplars ${index}번째 항목의 ${key} 는 비지 않은 문자열이다`);
126
+ throw new Error(`exemplars ${index}번째 항목의 ${key} 값은 비지 않은 문자열이어야 한다`);
127
127
  }
128
128
  values[key] = value;
129
129
  }
@@ -131,21 +131,21 @@ export function projectExemplars(entries, presetNames) {
131
131
  if (!knownRules.has(rule)) throw new Error(`exemplars ${index}번째 항목의 모르는 규칙: ${rule}`);
132
132
  const rawPresets = entry.presets ?? [];
133
133
  if (!Array.isArray(rawPresets) || !rawPresets.every((item) => typeof item === "string")) {
134
- throw new Error(`exemplars ${index}번째 항목의 presets 는 문자열 배열이다`);
134
+ throw new Error(`exemplars ${index}번째 항목의 presets는 문자열 배열이어야 한다`);
135
135
  }
136
136
  const presets = /** @type {string[]} */ (rawPresets);
137
137
  const unknownPresets = [...new Set(presets.filter((preset) => !knownPresets.has(preset)))].sort();
138
138
  if (unknownPresets.length) {
139
139
  throw new Error(`exemplars ${index}번째 항목의 모르는 프리셋: ${unknownPresets.join(", ")}`);
140
140
  }
141
- if (new Set(presets).size !== presets.length) throw new Error(`프로젝트 본보기의 프리셋이 겹친다: ${rule}`);
141
+ if (new Set(presets).size !== presets.length) throw new Error(`프로젝트 예시의 프리셋이 겹친다: ${rule}`);
142
142
  if (!presets.length) {
143
- if (defaults.has(rule)) throw new Error(`프로젝트 기본 본보기가 겹친다: ${rule}`);
143
+ if (defaults.has(rule)) throw new Error(`프로젝트 기본 예시가 겹친다: ${rule}`);
144
144
  defaults.add(rule);
145
145
  } else {
146
146
  const used = contexts.get(rule) ?? new Set();
147
147
  const overlap = presets.filter((preset) => used.has(preset)).sort();
148
- if (overlap.length) throw new Error(`프로젝트 본보기의 프리셋이 겹친다: ${rule} ${overlap.join(", ")}`);
148
+ if (overlap.length) throw new Error(`프로젝트 예시의 프리셋이 겹친다: ${rule} ${overlap.join(", ")}`);
149
149
  for (const preset of presets) used.add(preset);
150
150
  contexts.set(rule, used);
151
151
  }
@@ -166,7 +166,7 @@ export function exemplarFor(rule, preset = null, customExemplars = []) {
166
166
  return exemplars().get(rule);
167
167
  }
168
168
 
169
- /** 전과 후를 제 줄씩. @param {Exemplar} exemplar @returns {[string, string]} */
169
+ /** 전과 후를 한 줄씩. @param {Exemplar} exemplar @returns {[string, string]} */
170
170
  export function twoLines(exemplar) {
171
171
  return [shorten(exemplar.before), shorten(exemplar.after)];
172
172
  }
package/src/data/load.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
  /**
3
- * data 폴더를 읽는다. 파이썬 정본의 투영이라 파일 모양이 같다. txt 는 한 줄에 항목 하나, json 은 toml 을 옮긴 것.
3
+ * data 폴더를 읽는다. 파이썬 자료에서 만든 사본이라 파일 모양이 같다. txt는 한 줄에 항목 하나이고, json은 toml을 옮긴 것이다.
4
4
  * 한 번 읽은 것은 기억한다.
5
5
  */
6
6
  import { compile } from "../regex.js";
@@ -24,7 +24,7 @@ export function readText(name) {
24
24
  return readData(name);
25
25
  }
26
26
 
27
- /** `#` 으로 시작하면 주석, 빈 줄은 무시. @param {string} name @returns {string[]} */
27
+ /** `#`으로 시작하면 주석, 빈 줄은 무시. @param {string} name @returns {string[]} */
28
28
  export function loadLines(name) {
29
29
  return cached(`lines:${name}`, () =>
30
30
  readText(name)
@@ -39,7 +39,7 @@ export function loadPatterns(name) {
39
39
  return cached(`patterns:${name}`, () => loadLines(name).map((line) => compile(line)));
40
40
  }
41
41
 
42
- /** toml 의 `[[entry]]` 목록. @param {string} name @returns {Record<string, unknown>[]} */
42
+ /** toml의 `[[entry]]` 목록. @param {string} name @returns {Record<string, unknown>[]} */
43
43
  export function loadEntries(name) {
44
44
  return cached(`entries:${name}`, () => {
45
45
  const data = JSON.parse(readText(name.replace(/\.toml$/, ".json")));
@@ -47,17 +47,17 @@ export function loadEntries(name) {
47
47
  });
48
48
  }
49
49
 
50
- /** @returns {Record<string, string>} 규칙 이름 → 기술서 (파이썬 docstring 의 투영) */
50
+ /** @returns {Record<string, string>} 규칙 이름 → 설명서(파이썬 docstring에서 만든 사본) */
51
51
  export function loadRuleDocs() {
52
52
  return cached("ruleDocs", () => JSON.parse(readText("ruleDocs.json")));
53
53
  }
54
54
 
55
- /** @returns {Record<string, string>} 규칙 이름 → 부류 (규칙 파일이 사는 폴더의 투영) */
55
+ /** @returns {Record<string, string>} 규칙 이름 → 묶음(규칙 파일이 있는 폴더에서 만든 사본) */
56
56
  export function loadRuleCategories() {
57
57
  return cached("ruleCategories", () => JSON.parse(readText("ruleCategories.json")));
58
58
  }
59
59
 
60
- /** @returns {Record<string, string>} 규칙 이름 → 기제 (파이썬 등록부의 투영) */
60
+ /** @returns {Record<string, string>} 규칙 이름 → 방식(파이썬 규칙 목록에서 만든 사본) */
61
61
  export function loadRuleMechanisms() {
62
62
  return cached("ruleMechanisms", () => JSON.parse(readText("ruleMechanisms.json")));
63
63
  }
@@ -1,20 +1,20 @@
1
1
  // @ts-check
2
- /** 승인 고침에서 뜻을 추측하지 않고 재사용할 수 있는 표면 치환. 파이썬 data/operations.py와 같다. */
2
+ /** 승인한 고침에서 뜻을 추측하지 않고 다시 쓸 수 있는 치환. 파이썬 data/operations.py와 같다. */
3
3
 
4
4
  const URL_PATTERN = /https?:\/\/[^\s)>\]]*[\p{L}\p{N}_/#=%&+~-]/gu;
5
5
  const NUMBER_PATTERN = /(?<![A-Za-z가-힣])[-+]?\d+(?:[.,:]\d+)*(?:%|[가-힣]+)?/gu;
6
6
  const LATIN_PATTERN = /(?<![A-Za-z0-9_])(?:[A-Za-z_][A-Za-z0-9_.:/<>-]*)(?![A-Za-z0-9_])/gu;
7
- // 파이썬 `re` 의 `\w` 는 유니코드라 한글을 담지만 JS 의 `\w` 는 /u 를 붙여도 ASCII 뿐이다. 그대로 두면
8
- // docs/렌더/index.md 를 파이썬만 경로 원자로 보고 npm 은 그 안을 치환해 버린다 (말뭉치 실문장
9
- // 32,274개에서 280건이 갈렸다). URL_PATTERN 이 이미 쓰는 표기로 맞춘다.
7
+ // 파이썬 `re`의 `\w`는 유니코드라 한글을 담지만 JS의 `\w`는 /u를 붙여도 ASCII뿐이다. 그대로 두면
8
+ // docs/렌더/index.md를 파이썬만 경로로 보고 npm은 그 안을 바꿔 버린다(말뭉치 실제 문장
9
+ // 32,274개에서 280건이 서로 달랐다). URL_PATTERN이 이미 쓰는 표기로 맞춘다.
10
10
  const WORD = "[\\p{L}\\p{N}_]";
11
11
  const PATH_PATTERN = new RegExp(
12
12
  `(?<!${WORD})(?:[\\p{L}\\p{N}_.-]+/)+[\\p{L}\\p{N}_.-]+|(?<!${WORD})[\\p{L}\\p{N}_-]+\\.[A-Za-z0-9]{1,8}(?!${WORD})`,
13
13
  "gu",
14
14
  );
15
15
  const INLINE_CODE_PATTERN = /`([^`\n]+)`/gu;
16
- // d 플래그로 진짜 그룹 시작을 읽는다. indexOf 로 찾으면 [렌더](렌더) 처럼 링크 글자와 목적지가 같을 때
17
- // 링크 글자 쪽을 목적지로 착각한다. 파이썬은 match.start(1) 을 쓴다.
16
+ // d 플래그로 진짜 그룹 시작을 읽는다. indexOf로 찾으면 [렌더](렌더)처럼 링크 글자와 목적지가 같을 때
17
+ // 링크 글자 쪽을 목적지로 착각한다. 파이썬은 match.start(1)을 쓴다.
18
18
  const LINK_DESTINATION_PATTERN = /\[[^\u005D]*\]\(([^)]+)\)/gdu;
19
19
  const HTML_TAG_PATTERN = /<[^>\n]+>/gu;
20
20
  const DEICTIC_FRAGMENT = /이것|그것|저것|이러한|그러한|해당|이는|그는|그녀|그들/u;
@@ -58,7 +58,7 @@ export function protectedSpans(text) {
58
58
  const [start, end] = /** @type {RegExpIndicesArray} */ (match.indices)[1];
59
59
  spans.push([start, end]);
60
60
  }
61
- // 파이썬은 sorted(set(spans)) 라 겹친 구간을 한 번만 둔다 (코드 안의 URL 처럼).
61
+ // 파이썬은 sorted(set(spans))라 겹친 구간을 한 번만 둔다(코드 안의 URL처럼).
62
62
  const unique = [...new Map(spans.map((span) => [`${span[0]}:${span[1]}`, span])).values()];
63
63
  return unique.sort((left, right) => left[0] - right[0] || left[1] - right[1]);
64
64
  }
@@ -129,7 +129,7 @@ export function surfaceSkeleton(text) {
129
129
  }
130
130
 
131
131
  /**
132
- * 승인 전후 전체에서 기계가 보증할 수 있는 표면 치환 하나를 추출한다.
132
+ * 승인 전후 전체에서 기계가 보증할 수 있는 치환 하나를 뽑는다.
133
133
  * @param {string} before @param {string} after @param {string[]} [presets] @param {string[]} [protectedTerms]
134
134
  * @returns {SurfaceOperation | undefined}
135
135
  */
@@ -168,26 +168,26 @@ export function operationFromApproval(before, after, presets = [], protectedTerm
168
168
 
169
169
  /** @param {unknown} entries @param {string[]} presetNames @returns {SurfaceOperation[]} */
170
170
  export function projectOperations(entries, presetNames) {
171
- if (!Array.isArray(entries)) throw new Error("operations 는 [[operations]] 배열이다");
171
+ if (!Array.isArray(entries)) throw new Error("operations는 [[operations]] 배열이어야 한다");
172
172
  const knownPresets = new Set(presetNames);
173
173
  const selectors = new Set();
174
174
  return entries.map((raw, offset) => {
175
175
  const index = offset + 1;
176
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`operations ${index}번째 항목은 표다`);
176
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`operations ${index}번째 항목은 표여야 한다`);
177
177
  const entry = /** @type {Record<string, unknown>} */ (raw);
178
178
  const unknown = Object.keys(entry).filter((key) => !["before", "after", "presets"].includes(key)).sort();
179
179
  if (unknown.length) throw new Error(`operations ${index}번째 항목의 모르는 키: ${unknown.join(", ")}`);
180
180
  const before = entry.before;
181
181
  const after = entry.after;
182
182
  if (typeof before !== "string" || !before.trim() || before !== before.trim()) {
183
- throw new Error(`operations ${index}번째 항목의 before 는 양끝 공백 없는 문자열이다`);
183
+ throw new Error(`operations ${index}번째 항목의 before는 양끝 공백 없는 문자열이어야 한다`);
184
184
  }
185
185
  if (typeof after !== "string" || !after.trim() || after !== after.trim()) {
186
- throw new Error(`operations ${index}번째 항목의 after 는 양끝 공백 없는 문자열이다`);
186
+ throw new Error(`operations ${index}번째 항목의 after는 양끝 공백 없는 문자열이어야 한다`);
187
187
  }
188
188
  const rawPresets = entry.presets;
189
189
  if (!Array.isArray(rawPresets) || !rawPresets.length || !rawPresets.every((item) => typeof item === "string")) {
190
- throw new Error(`operations ${index}번째 항목의 presets 는 비지 않은 문자열 배열이다`);
190
+ throw new Error(`operations ${index}번째 항목의 presets는 비지 않은 문자열 배열이어야 한다`);
191
191
  }
192
192
  const presets = /** @type {string[]} */ (rawPresets);
193
193
  const unknownPresets = [...new Set(presets.filter((preset) => !knownPresets.has(preset)))].sort();
@@ -195,7 +195,7 @@ export function projectOperations(entries, presetNames) {
195
195
  if (new Set(presets).size !== presets.length) throw new Error(`operation의 프리셋이 겹친다: ${before}`);
196
196
  const operation = operationFromApproval(before, after, presets);
197
197
  if (!operation || operation.before !== before || operation.after !== after) {
198
- throw new Error(`operations ${index}번째 항목은 안전한 표면 치환이 아니다: ${before} -> ${after}`);
198
+ throw new Error(`operations ${index}번째 항목은 안전한 치환이 아니다: ${before} -> ${after}`);
199
199
  }
200
200
  for (const preset of presets) {
201
201
  const selector = JSON.stringify([preset, before]);
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 글쓴이가 승인한 원문 완전 일치 고침. 파이썬 data/patches.py와 같은 선택 계약이다. */
2
+ /** 글쓴이가 승인한, 원문이 완전히 같을 때만 쓰는 고침. 파이썬 data/patches.py와 같은 조건으로 고른다. */
3
3
  import { exemplars, expand } from "./exemplars.js";
4
4
 
5
5
  export const READER_KINDS = ["recent", "known", "new", "none"];
@@ -13,7 +13,7 @@ export const READER_KINDS = ["recent", "known", "new", "none"];
13
13
  * @property {string} cue
14
14
  * @property {string} reader
15
15
  * @property {string[]} presets
16
- * @property {string} sentence 마크다운 표식을 걷은 선택용 원문
16
+ * @property {string} sentence 마크다운 표식을 뺀 선택용 원문
17
17
  * @property {string} sourceText 마크다운 표식을 보존한 선택용 원문
18
18
  */
19
19
 
@@ -22,21 +22,21 @@ export function flatCue(text) {
22
22
  return text.split(/\s+/).filter(Boolean).join(" ");
23
23
  }
24
24
 
25
- /** 승인 원문은 유니코드 조합과 줄, 연속 공백만 눕혀 비교한다. @param {string} text */
25
+ /** 승인 원문은 유니코드 조합과 줄바꿈, 연속 공백만 정규화해 비교한다. @param {string} text */
26
26
  export function flatSentence(text) {
27
27
  return text.normalize("NFC").split(/\s+/).filter(Boolean).join(" ");
28
28
  }
29
29
 
30
30
  /** 설정의 `[[patches]]`를 검증한다. @param {unknown} entries @param {string[]} presetNames @returns {Patch[]} */
31
31
  export function projectPatches(entries, presetNames) {
32
- if (!Array.isArray(entries)) throw new Error("patches 는 [[patches]] 배열이다");
32
+ if (!Array.isArray(entries)) throw new Error("patches는 [[patches]] 배열이어야 한다");
33
33
  const allowedKeys = new Set(["rule", "before", "after", "moved", "sourceText", "sentence", "cue", "reader", "presets"]);
34
34
  const knownRules = new Set(exemplars().keys());
35
35
  const knownPresets = new Set(presetNames);
36
36
  const selectors = new Set();
37
37
  return entries.map((raw, offset) => {
38
38
  const index = offset + 1;
39
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`patches ${index}번째 항목은 표다`);
39
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error(`patches ${index}번째 항목은 표여야 한다`);
40
40
  const entry = /** @type {Record<string, unknown>} */ (raw);
41
41
  const unknown = Object.keys(entry).filter((key) => !allowedKeys.has(key)).sort();
42
42
  if (unknown.length) throw new Error(`patches ${index}번째 항목의 모르는 키: ${unknown.join(", ")}`);
@@ -45,7 +45,7 @@ export function projectPatches(entries, presetNames) {
45
45
  for (const key of ["rule", "before", "after", "moved", "cue", "reader"]) {
46
46
  const value = entry[key];
47
47
  if (typeof value !== "string" || !value.trim()) {
48
- throw new Error(`patches ${index}번째 항목의 ${key} 는 비지 않은 문자열이다`);
48
+ throw new Error(`patches ${index}번째 항목의 ${key} 값은 비지 않은 문자열이어야 한다`);
49
49
  }
50
50
  values[key] = value;
51
51
  }
@@ -53,11 +53,11 @@ export function projectPatches(entries, presetNames) {
53
53
  if (!knownRules.has(rule)) throw new Error(`patches ${index}번째 항목의 모르는 규칙: ${rule}`);
54
54
  const reader = values.reader;
55
55
  if (!READER_KINDS.includes(reader)) {
56
- throw new Error(`patches ${index}번째 항목의 reader 는 ${READER_KINDS.join(", ")} 가운데 하나다: ${reader}`);
56
+ throw new Error(`patches ${index}번째 항목의 reader는 ${READER_KINDS.join(", ")} 가운데 하나여야 한다. 받은 값: ${reader}`);
57
57
  }
58
58
  const rawPresets = entry.presets;
59
59
  if (!Array.isArray(rawPresets) || !rawPresets.length || !rawPresets.every((item) => typeof item === "string")) {
60
- throw new Error(`patches ${index}번째 항목의 presets 는 비지 않은 문자열 배열이다`);
60
+ throw new Error(`patches ${index}번째 항목의 presets는 비지 않은 문자열 배열이어야 한다`);
61
61
  }
62
62
  const presets = /** @type {string[]} */ (rawPresets);
63
63
  const unknownPresets = [...new Set(presets.filter((preset) => !knownPresets.has(preset)))].sort();
@@ -70,12 +70,12 @@ export function projectPatches(entries, presetNames) {
70
70
  const after = expand(values.after);
71
71
  const rawSentence = entry.sentence ?? before;
72
72
  if (typeof rawSentence !== "string" || !rawSentence.trim()) {
73
- throw new Error(`patches ${index}번째 항목의 sentence 는 비지 않은 문자열이다`);
73
+ throw new Error(`patches ${index}번째 항목의 sentence는 비지 않은 문자열이어야 한다`);
74
74
  }
75
75
  const sentence = flatSentence(rawSentence);
76
76
  const rawSourceText = entry.sourceText ?? before;
77
77
  if (typeof rawSourceText !== "string" || !rawSourceText.trim()) {
78
- throw new Error(`patches ${index}번째 항목의 sourceText 는 비지 않은 문자열이다`);
78
+ throw new Error(`patches ${index}번째 항목의 sourceText는 비지 않은 문자열이어야 한다`);
79
79
  }
80
80
  const sourceText = flatSentence(rawSourceText);
81
81
  for (const preset of presets) {
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 문형. 빈칸이 있는 문장 틀. 파이썬 data/patterns.py 와 같다.
4
- * 본보기가 고친 사례 하나라면 문형은 그 사례를 다시 쓸 수 있는 틀이다.
3
+ * 문형. 빈칸이 있는 문장 틀. 파이썬 data/patterns.py와 같다.
4
+ * 예시가 고친 사례 하나라면 문형은 그 사례를 다시 쓸 수 있는 틀이다.
5
5
  */
6
6
  import { loadEntries } from "./load.js";
7
7
 
@@ -9,9 +9,9 @@ import { loadEntries } from "./load.js";
9
9
  * @typedef {object} Pattern
10
10
  * @property {string} name
11
11
  * @property {string} form 빈칸이 있는 틀
12
- * @property {string} when 이 틀을 꺼내는 자리
13
- * @property {string} example 그 틀로 쓴 문장. hanlint 를 통과한다
14
- * @property {string} instead 같은 자리에 흔히 쓰는 문장. avoids 의 규칙에 잡힌다
12
+ * @property {string} when 이 틀을 쓰는 경우
13
+ * @property {string} example 그 틀로 쓴 문장. hanlint를 통과한다
14
+ * @property {string} instead 같은 경우에 흔히 쓰는 문장. avoids의 규칙에 잡힌다
15
15
  * @property {string[]} avoids
16
16
  * @property {string} source
17
17
  */
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 참조 분포 (프로파일) 의 자료형과 읽기. 파이썬 data/profiles.py 와 같다. 종류별 프로파일은 profiles.json (투영) 에서,
4
- * 사용자 프로파일은 `hanlint profile build` 가 만든 파일에서 읽는다. 히스토그램은 정확한 계수라 두 판의 셈이 같다.
3
+ * 참조 분포(프로파일)의 자료형과 읽기. 파이썬 data/profiles.py와 같다. 종류별 프로파일은 profiles.json(파이썬 자료의 사본)에서,
4
+ * 사용자 프로파일은 `hanlint profile build`가 만든 파일에서 읽는다. 히스토그램은 정확한 계수라 두 구현의 셈이 같다.
5
5
  */
6
6
  import { readExternal } from "./read.js";
7
7
 
@@ -11,7 +11,7 @@ export const PROFILE_VERSION = 1;
11
11
  /** 히스토그램의 상한. 이 값보다 큰 것은 상한에 합친다. */
12
12
  export const CAP = 200;
13
13
  const PERCENTILES = [50, 90, 95, 99];
14
- /** 기준 말뭉치 종류의 이름. 지적문에 적는다. 파이썬 TYPE_LABELS 와 같다. @type {Record<string, string>} */
14
+ /** 기준 말뭉치 종류의 이름. 지적문에 적는다. 파이썬 TYPE_LABELS와 같다. @type {Record<string, string>} */
15
15
  export const TYPE_LABELS = {
16
16
  blog: "블로그",
17
17
  technicalDocs: "기술 문서",
@@ -52,7 +52,7 @@ function histogramFromDict(data) {
52
52
  return { total: Number(data.total), counts, percentiles };
53
53
  }
54
54
 
55
- /** 값이 이 이상인 것의 몫을 천분율 정수로. 정수 셈이라 두 판의 글자가 같다. @param {Histogram} hist @param {number} value */
55
+ /** 값이 이 이상인 것의 몫을 천분율 정수로. 정수 셈이라 두 구현의 글자가 같다. @param {Histogram} hist @param {number} value */
56
56
  export function shareAtOrAbove(hist, value) {
57
57
  const capped = Math.min(value, CAP);
58
58
  let above = 0;
@@ -87,7 +87,7 @@ function profileFromDict(kind, data) {
87
87
 
88
88
  /** @type {Map<string, Profile> | null} */
89
89
  let shipped = null;
90
- /** hanlint 가 싣는 종류별 프로파일. @param {string} kind @returns {Profile | null} */
90
+ /** hanlint가 싣는 종류별 프로파일. @param {string} kind @returns {Profile | null} */
91
91
  export function profileOf(kind) {
92
92
  if (!shipped) {
93
93
  shipped = new Map();
@@ -105,7 +105,7 @@ export function userProfile(path) {
105
105
  if (!profile) {
106
106
  const data = JSON.parse(readExternal(path));
107
107
  if (data.version !== PROFILE_VERSION || !("profile" in data)) {
108
- throw new Error(`프로파일 파일이 아니다: ${path}. hanlint profile build 로 다시 만든다`);
108
+ throw new Error(`프로파일 파일이 아니다: ${path}. hanlint profile build로 다시 만든다`);
109
109
  }
110
110
  profile = profileFromDict("custom", data.profile);
111
111
  users.set(path, profile);
@@ -0,0 +1,27 @@
1
+ // @ts-check
2
+ /**
3
+ * 쓰기 전에 읽는 글쓰기 지침. 파이썬 data/prose.py를 옮긴 것이고 내용은 koreanProse.txt 하나에 있다.
4
+ * primer는 절마다 한 줄로 펴서 첫머리에 싣고, hook --prime은 본문을 그대로 에이전트 문맥에 넣는다.
5
+ */
6
+ import { readText } from "./load.js";
7
+
8
+ export const PROSE = "koreanProse.txt";
9
+
10
+ /** 글쓰기 지침 본문. 끝의 줄바꿈 하나까지 파일 그대로다. @returns {string} */
11
+ export function proseGuide() {
12
+ return readText(PROSE);
13
+ }
14
+
15
+ /**
16
+ * (제목, 본문) 짝. 첫 짝은 문서 제목과 첫머리이고 나머지는 일곱 줄이다. 제목의 `#`은 뗀다.
17
+ * 파이썬 proseSections와 같다. @returns {[string, string][]}
18
+ */
19
+ export function proseSections() {
20
+ /** @type {[string, string[]][]} */
21
+ const sections = [];
22
+ for (const line of proseGuide().split(/\r?\n/)) {
23
+ if (line.startsWith("#")) sections.push([line.replace(/^#+/, "").trim(), []]);
24
+ else if (sections.length) sections[sections.length - 1][1].push(line);
25
+ }
26
+ return sections.map(([title, body]) => [title, body.join("\n").trim()]);
27
+ }
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 문서 모델. 세 층이다. Block (빈 줄로 나뉜 덩어리), Section (H2 하나가 여는 절), Document (전체).
4
- * 줄 번호는 1 부터 세고 원문 기준이다.
3
+ * 문서 모델. 세 단계다. Block (빈 줄로 나뉜 덩어리), Section (H2 하나가 여는 절), Document (전체).
4
+ * 줄 번호는 1부터 세고 원문 기준이다.
5
5
  */
6
6
 
7
7
  export const PROSE = "prose";
@@ -20,7 +20,7 @@ export const HTML = "html";
20
20
  * @property {number} startLine
21
21
  * @property {number} endLine
22
22
  * @property {string} text
23
- * @property {number} level heading 일 때 `#` 의 개수
23
+ * @property {number} level heading 일 때 `#`의 개수
24
24
  * @property {number} index 문서 안 순서
25
25
  */
26
26
 
@@ -37,7 +37,7 @@ export const HTML = "html";
37
37
  * @property {Block[]} blocks
38
38
  * @property {Section[]} sections
39
39
  * @property {[string, number, number][]} disabled 인라인 제어가 끈 (규칙 또는 *, 시작 줄, 끝 줄)
40
- * @property {Block[]} ignored 설정이 지문에서 뺀 펜스. 파일 전체를 보는 규칙만 읽는다
40
+ * @property {Block[]} ignored 설정이 글 분석에서 뺀 펜스. 파일 전체를 보는 규칙만 읽는다
41
41
  */
42
42
 
43
43
  /** @param {Section} section */
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /**
3
- * 마크다운 텍스트를 문서 모델로 바꾼다. 파이썬 document/parseMarkdown.py 와 같은 판정이다.
4
- * 빈 줄로 블록을 나누고 첫 줄로 종류를 정한다. 코드 펜스 안은 통째로 code 다. H2 가 절을 열고 첫 H2 앞은 도입 절이다.
3
+ * 마크다운 텍스트를 문서 모델로 바꾼다. 파이썬 document/parseMarkdown.py와 같은 판정이다.
4
+ * 빈 줄로 블록을 나누고 첫 줄로 종류를 정한다. 코드 펜스 안은 통째로 code다. H2가 절을 열고 첫 H2 앞은 도입 절이다.
5
5
  */
6
6
  import { splitLines } from "../text.js";
7
7
  import { CODE, EMBED, HEADING, HTML, IMAGE, LIST, PROSE, QUOTE, TABLE } from "./model.js";
@@ -16,14 +16,14 @@ const IMAGE_LINE = /^!\[/;
16
16
  const LIST_LINE = /^(\s*)(?:[-*+]|\d+[.)])\s+/;
17
17
  const TABLE_LINE = /^\s*\|/;
18
18
  const URL_LINE = /^\s*https?:\/\/\S+\s*$/;
19
- // 링크 하나만 있는 줄은 끼워 넣은 것 (영상 카드, 실행 칸, 참고 링크) 이라 주소만 있는 줄과 같은 embed 다. 파이썬과 같다.
19
+ // 링크 하나만 있는 줄은 끼워 넣은 것 (영상 카드, 실행 칸, 참고 링크)이라 주소만 있는 줄과 같은 embed다. 파이썬과 같다.
20
20
  const LINK_ONLY_LINE = /^\s*\[[^\]\n]+\]\(\S+?(?:\s+"[^"\n]*")?\)\s*$/;
21
21
  const HTML_LINE = /^\s*</;
22
22
  const META_KEY = /^[A-Za-z][A-Za-z0-9_]*$/;
23
23
  const CONTROL = /^\s*<!--\s*hanlint-(disable-next-line|disable-next|disable|enable)\b([^>]*?)-->\s*$/;
24
24
  const ALL_RULES = "*";
25
25
 
26
- /** frontmatter 를 읽고 본문이 시작하는 줄 번호 (1 부터) 를 함께 준다. @param {string} text */
26
+ /** frontmatter를 읽고 본문이 시작하는 줄 번호(1부터)를 함께 준다. @param {string} text */
27
27
  export function parseFrontmatter(text) {
28
28
  const match = FRONTMATTER.exec(text);
29
29
  if (!match) return { meta: {}, firstLine: 1 };
@@ -219,7 +219,7 @@ export function disabledRanges(blocks) {
219
219
  return ranges;
220
220
  }
221
221
 
222
- /** 코드 블록 첫 줄의 언어 표기 (```python 의 python). 없으면 빈 문자열. 소문자로 준다. @param {string} blockText */
222
+ /** 코드 블록 첫 줄의 언어 표기 (```python의 python). 없으면 빈 문자열. 소문자로 준다. @param {string} blockText */
223
223
  export function fenceLanguage(blockText) {
224
224
  const opening = FENCE_OPEN.exec(blockText.split("\n", 1)[0]);
225
225
  if (!opening) return "";
@@ -228,8 +228,8 @@ export function fenceLanguage(blockText) {
228
228
  }
229
229
 
230
230
  /**
231
- * 언어 표기가 languages 에 든 펜스를 없앤 문서. 블록 순서 번호를 다시 매기고 절을 다시 묶는다. 인라인 제어는 원문 줄
232
- * 기준이라 그대로 두고, 뺀 펜스는 ignored 에 남긴다. 파이썬 dropFences 와 같다.
231
+ * 언어 표기가 languages에 든 펜스를 없앤 문서. 블록 순서 번호를 다시 매기고 절을 다시 묶는다. 인라인 제어는 원문 줄
232
+ * 기준이라 그대로 두고, 뺀 펜스는 ignored에 남긴다. 파이썬 dropFences와 같다.
233
233
  * @param {import("./model.js").Document} doc
234
234
  * @param {string[]} languages
235
235
  * @returns {import("./model.js").Document}
@@ -1,14 +1,14 @@
1
1
  // @ts-check
2
2
  /**
3
3
  * 산문에서 마크다운 표식을 걷는다. 인라인 코드는 백틱만 떼고 내용은 남긴다. 링크는 보이는 글자만. 강조 표식은 뗀다.
4
- * 줄바꿈은 남겨 줄 번호 계산에 쓴다. 파이썬 document/plainText.py 와 같다.
5
- * 인라인 코드가 있던 자리는 codeSpans 로 다시 찾는다. 백틱 안은 사용이 아니라 인용이다.
4
+ * 줄바꿈은 남겨 줄 번호 계산에 쓴다. 파이썬 document/plainText.py와 같다.
5
+ * 인라인 코드가 있던 곳은 codeSpans로 다시 찾는다. 백틱 안은 사용이 아니라 인용이다.
6
6
  */
7
7
 
8
8
  const INLINE_CODE = /`([^`\n]*)`/g;
9
9
  const LINK = /\[([^\]]*)\]\([^)]*\)/g;
10
10
  const EMPHASIS = /(\*\*|__)(?=\S)(.+?)(?<=\S)\1/g;
11
- // 파이썬의 \w 는 유니코드 글자라 한글을 포함한다. JS 는 \p{L}\p{N}_ 로 같은 뜻을 적는다.
11
+ // 파이썬의 \w는 유니코드 글자라 한글을 포함한다. JS는 \p{L}\p{N}_로 같은 뜻을 적는다.
12
12
  const SINGLE_EMPHASIS = /(?<![\p{L}\p{N}_*])\*(?=\S)([^*\n]+?)(?<=\S)\*(?![\p{L}\p{N}_*])/gu;
13
13
  const SPACES = /[ \t]+/g;
14
14