hanlint 0.0.18 → 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.
- package/README.md +17 -16
- package/data/calqueFrame.json +20 -20
- package/data/cliches.json +33 -33
- package/data/confusable.json +20 -20
- package/data/easyWords.json +12 -12
- package/data/exemplars.json +30 -30
- package/data/hiddenDeps.txt +11 -11
- package/data/japaneseLoan.json +27 -27
- package/data/koreanProse.txt +31 -31
- package/data/patterns.json +26 -26
- package/data/phraseRepeat.json +2 -2
- package/data/platformApis.txt +11 -11
- package/data/redundantPair.json +16 -16
- package/data/ruleDocs.json +56 -56
- package/data/screenNarration.json +10 -10
- package/data/screenSentence.json +12 -12
- package/data/screenTone.json +6 -6
- package/data/screenWord.json +70 -70
- package/data/spacing.json +11 -11
- package/data/spelling.json +29 -29
- package/data/translationese.json +23 -24
- package/data/usageCounts.report.json +2 -2
- package/data/version.json +1 -1
- package/package.json +1 -1
- package/src/analysis/grammar/ending.js +1 -1
- package/src/analysis/grammar/hangul.js +4 -4
- package/src/analysis/grammar/josa.js +1 -1
- package/src/analysis/grammar/register.js +1 -1
- package/src/analysis/grammar/voice.js +1 -1
- package/src/analysis/index.js +2 -2
- package/src/analysis/josa.js +1 -1
- package/src/analysis/splitSentences.js +1 -1
- package/src/analysis/tokenize.js +13 -13
- package/src/baseline/store.js +8 -8
- package/src/cli/help.js +11 -11
- package/src/cli/main.js +135 -124
- package/src/cli/walk.js +1 -1
- package/src/cli/welcome.js +8 -8
- package/src/config/loadConfig.js +3 -3
- package/src/config/patch.js +6 -6
- package/src/config/readerContract.js +26 -26
- package/src/config/settings.js +30 -28
- package/src/config/toml.js +9 -9
- package/src/data/exemplars.js +21 -21
- package/src/data/load.js +6 -6
- package/src/data/operations.js +14 -14
- package/src/data/patches.js +10 -10
- package/src/data/patterns.js +5 -5
- package/src/data/profiles.js +5 -5
- package/src/data/prose.js +5 -5
- package/src/document/model.js +4 -4
- package/src/document/parseMarkdown.js +7 -7
- package/src/document/plainText.js +3 -3
- package/src/document/sourceText.js +24 -24
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +6 -6
- package/src/fingerprint/codeBlocks.js +3 -3
- package/src/fingerprint/codeMarkers.js +4 -4
- package/src/fingerprint/dictionaries.js +7 -7
- package/src/fingerprint/markers.js +7 -7
- package/src/fingerprint/readerState.js +9 -9
- package/src/fingerprint/sourceSentence.js +1 -1
- package/src/fingerprint/topics.js +1 -1
- package/src/guard/contract.js +6 -6
- package/src/guard/editLocality.js +1 -1
- package/src/guard/outline.js +1 -1
- package/src/guard/receipt.js +13 -13
- package/src/guard/surface.js +4 -4
- package/src/index.js +6 -6
- package/src/learn/edits.js +2 -2
- package/src/learn/operations.js +1 -1
- package/src/regex.js +2 -2
- package/src/report/compactReport.js +3 -3
- package/src/report/fingerprintJson.js +2 -2
- package/src/report/githubReport.js +1 -1
- package/src/report/jsonReport.js +2 -2
- package/src/report/registerMatch.js +2 -2
- package/src/report/sheet.js +16 -16
- package/src/report/textReport.js +12 -12
- package/src/report/writingSpec.js +15 -15
- package/src/rules/code/inputFileSource.js +1 -1
- package/src/rules/code/installImport.js +2 -2
- package/src/rules/code/platformApi.js +1 -1
- package/src/rules/document/countMismatch.js +2 -1
- package/src/rules/document/enoughOnce.js +2 -1
- package/src/rules/document/fieldEcho.js +10 -6
- package/src/rules/document/keywordHeading.js +1 -1
- package/src/rules/document/keywordMissing.js +1 -1
- package/src/rules/document/noQuestion.js +2 -2
- package/src/rules/document/promiseRecall.js +1 -1
- package/src/rules/document/tableOddCell.js +1 -1
- package/src/rules/finding.js +4 -4
- package/src/rules/paragraph/factListParagraph.js +1 -1
- package/src/rules/paragraph/paraFragment.js +1 -1
- package/src/rules/readerDebt.js +1 -1
- package/src/rules/registry.js +26 -26
- package/src/rules/sentence/connectorRepeat.js +2 -1
- package/src/rules/sentence/danglingDeixis.js +1 -1
- package/src/rules/sentence/dash.js +1 -1
- package/src/rules/sentence/deixis.js +1 -1
- package/src/rules/sentence/doubleNegative.js +2 -1
- package/src/rules/sentence/doublePassive.js +2 -1
- package/src/rules/sentence/draftHistory.js +2 -1
- package/src/rules/sentence/endingRepeat.js +3 -2
- package/src/rules/sentence/euiChain.js +1 -1
- package/src/rules/sentence/fillerOpener.js +1 -1
- package/src/rules/sentence/imperativePeriod.js +1 -1
- package/src/rules/sentence/longSentence.js +1 -1
- package/src/rules/sentence/negationRedefine.js +1 -1
- package/src/rules/sentence/numberOrphan.js +1 -1
- package/src/rules/sentence/outsideProfile.js +8 -3
- package/src/rules/sentence/phraseRepeat.js +2 -1
- package/src/rules/shared/candidates.js +2 -2
- package/src/rules/shared/dictionaryRule.js +9 -2
- package/src/rules/shared/repeat.js +2 -2
- package/src/rules/structure/bridgeRepeat.js +3 -3
- package/src/rules/structure/emojiBullet.js +1 -1
- package/src/rules/structure/headingSkip.js +1 -1
- package/src/rules/structure/headingUniform.js +2 -1
- package/src/rules/structure/loneSubheading.js +1 -1
- package/src/rules/structure/moreLater.js +1 -1
- package/src/text.js +2 -2
- package/src/usage/counts.js +3 -3
- package/src/usage/index.js +3 -3
- package/src/usage/sentences.js +17 -17
package/src/cli/main.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 명령줄 진입점. 파이썬 CLI
|
|
3
|
+
* 명령줄 진입점. 파이썬 CLI와 같은 인자, 같은 출력, 같은 종료 코드다 (0 지적 없음, 1 error 있음, 2 파일이나 설정 문제).
|
|
4
4
|
*
|
|
5
5
|
* ```
|
|
6
6
|
* hanlint 인자가 없으면 첫 화면. 무엇을 칠 수 있는지 보인다
|
|
7
|
-
* hanlint 글.md [다른.md ...] 검사. 서브커맨드 없이 파일이나 폴더만 주면 lint
|
|
7
|
+
* hanlint 글.md [다른.md ...] 검사. 서브커맨드 없이 파일이나 폴더만 주면 lint다. `-`는 stdin
|
|
8
8
|
* hanlint fix 글.md 기계가 고칠 수 있는 지적을 원문에 적용
|
|
9
9
|
* hanlint print 글.md 분석표 계층 JSON
|
|
10
|
-
* hanlint rules 규칙 목록을
|
|
10
|
+
* hanlint rules 규칙 목록을 묶음로 묶어서
|
|
11
11
|
* hanlint explain <규칙> 규칙의 기술서
|
|
12
|
-
* hanlint patterns 문장 틀. --rule
|
|
13
|
-
* hanlint primer --preset <종류> 쓰기 전에 읽는 한 장.
|
|
14
|
-
* hanlint hook 에이전트 훅. --prime
|
|
12
|
+
* hanlint patterns 문장 틀. --rule로 그 규칙을 피하는 것만
|
|
13
|
+
* hanlint primer --preset <종류> 쓰기 전에 읽는 한 장. 글쓰기 지침 일곱 줄과 켜진 규칙의 고치는 법, 예시 전후
|
|
14
|
+
* hanlint hook 에이전트 훅. --prime은 쓰기 전 글쓰기 지침, 나머지는 쓴 마크다운의 비차단 검사
|
|
15
15
|
* hanlint baseline 글들/ 지금 있는 지적을 잠근다. 그다음부터 새것만 막힌다
|
|
16
16
|
* hanlint baseline 글들/ 지금 있는 지적을 잠근다. 그다음부터 새것만 막힌다
|
|
17
17
|
* hanlint doctor 설정과 꺼진 규칙
|
|
18
|
-
* hanlint init 주석 달린 hanlint.toml. --output
|
|
18
|
+
* hanlint init 주석 달린 hanlint.toml. --output과 --preset blog|report|docs
|
|
19
19
|
* hanlint contract init 글.md 원문에서 최소 Reader Contract 초안
|
|
20
20
|
* ```
|
|
21
21
|
* audit, guard, arena, blueprint, evidence, entailment 같은 확장 명령은 파이썬 패키지에 있다.
|
|
@@ -88,11 +88,11 @@ const PYTHON_ONLY = [
|
|
|
88
88
|
];
|
|
89
89
|
const FORMATS = ["text", "compact", "json", "github"];
|
|
90
90
|
const SEVERITIES = ["all", "error", "notice"];
|
|
91
|
-
/** 오탐과 미탐을 받는 자리. 뜻은 파이썬 cli/commands/explain.py
|
|
91
|
+
/** 오탐과 미탐을 받는 자리. 뜻은 파이썬 cli/commands/explain.py가 소유한다. */
|
|
92
92
|
const ISSUES = "github.com/eddmpython/hanlint/issues";
|
|
93
93
|
const STDIN = "-";
|
|
94
94
|
const STDIN_NAME = "<stdin>";
|
|
95
|
-
/** 폴더를 주면 이 확장자만 찾는다. 파이썬 cli/commands/shared.py
|
|
95
|
+
/** 폴더를 주면 이 확장자만 찾는다. 파이썬 cli/commands/shared.py의 MARKDOWN과 같다. */
|
|
96
96
|
const THRESHOLD_FIELDS = [
|
|
97
97
|
"fragmentRun",
|
|
98
98
|
"introMaxParagraphs",
|
|
@@ -180,13 +180,13 @@ function parseArgs(args) {
|
|
|
180
180
|
options[arg] = true;
|
|
181
181
|
continue;
|
|
182
182
|
}
|
|
183
|
-
// 값을 줘도 되고 안 줘도 되는 옵션. 다음 인자가 다른 옵션이거나 없으면 true
|
|
183
|
+
// 값을 줘도 되고 안 줘도 되는 옵션. 다음 인자가 다른 옵션이거나 없으면 true로 둔다 (파이썬의 nargs="?").
|
|
184
184
|
if (kind === "optional" && inlineValue === null && (args[i + 1] === undefined || args[i + 1].startsWith("--"))) {
|
|
185
185
|
options[arg] = true;
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
188
188
|
const value = inlineValue ?? args[++i];
|
|
189
|
-
if (value === undefined) throw new UsageError(`${arg}
|
|
189
|
+
if (value === undefined) throw new UsageError(`${arg}에 값이 없다`);
|
|
190
190
|
if (kind === "list") {
|
|
191
191
|
const list = /** @type {string[]} */ (options[arg] ?? []);
|
|
192
192
|
list.push(value);
|
|
@@ -199,10 +199,10 @@ function parseArgs(args) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* 서브커맨드 없이 파일이나 옵션만 주면 lint
|
|
202
|
+
* 서브커맨드 없이 파일이나 옵션만 주면 lint로 본다. 빈 인자는 여기서 다루지 않는다. `main`이 첫 화면으로 보낸다.
|
|
203
203
|
* @param {string[]} argv
|
|
204
204
|
*/
|
|
205
|
-
export /** 파일 이름이 아니라 서브커맨드를 치려던 것으로 보이나. 뜻은 파이썬 cli/main.py
|
|
205
|
+
export /** 파일 이름이 아니라 서브커맨드를 치려던 것으로 보이나. 뜻은 파이썬 cli/main.py가 소유한다. @param {string} word */
|
|
206
206
|
function looksLikeCommand(word) {
|
|
207
207
|
if (!word || !/^[A-Za-z]+$/.test(word)) return false;
|
|
208
208
|
return !existsSync(word);
|
|
@@ -213,15 +213,15 @@ function normalizeArgv(argv) {
|
|
|
213
213
|
if (COMMANDS.includes(argv[0]) || PYTHON_ONLY.includes(argv[0]) || ["-h", "--help", "--version"].includes(argv[0])) return argv;
|
|
214
214
|
if (looksLikeCommand(argv[0])) {
|
|
215
215
|
const near = nearNames(argv[0], [...COMMANDS, ...PYTHON_ONLY]);
|
|
216
|
-
const hint = near.length ? `
|
|
217
|
-
throw new Error(
|
|
216
|
+
const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}.` : "";
|
|
217
|
+
throw new Error(`없는 명령: ${argv[0]}.${hint} 전체 목록은 hanlint --help`);
|
|
218
218
|
}
|
|
219
219
|
return ["lint", ...argv];
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/** @param {string} value @param {string[]} choices @param {string} option */
|
|
223
223
|
function choose(value, choices, option) {
|
|
224
|
-
if (!choices.includes(value)) throw new UsageError(`${option}
|
|
224
|
+
if (!choices.includes(value)) throw new UsageError(`${option} 값으로 쓸 수 없다: ${value}. ${choices.join(", ")} 가운데 하나를 쓴다`);
|
|
225
225
|
return value;
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -232,10 +232,10 @@ function emit(text, output) {
|
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
writeFileSync(output, text.endsWith("\n") ? text : `${text}\n`, "utf-8");
|
|
235
|
-
process.stderr.write(`${output}
|
|
235
|
+
process.stderr.write(`${output}에 썼다\n`);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
/** 설정을 찾기 시작할 폴더. 첫 실제 파일의 폴더, 전부 stdin
|
|
238
|
+
/** 설정을 찾기 시작할 폴더. 첫 실제 파일의 폴더, 전부 stdin이면 현재 폴더. @param {string[]} paths */
|
|
239
239
|
function startFolder(paths) {
|
|
240
240
|
for (const path of paths) if (path !== STDIN) return dirname(resolve(path));
|
|
241
241
|
return process.cwd();
|
|
@@ -247,8 +247,8 @@ function checkDisabled(config) {
|
|
|
247
247
|
const unknown = [...config.disable].filter((rule) => !names.includes(rule)).sort();
|
|
248
248
|
if (!unknown.length) return;
|
|
249
249
|
const near = nearNames(unknown[0], names);
|
|
250
|
-
const hint = near.length ? `
|
|
251
|
-
throw new Error(
|
|
250
|
+
const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}.` : "";
|
|
251
|
+
throw new Error(`없는 규칙 이름: ${unknown.join(", ")}.${hint} 목록은 hanlint rules --names`);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/** @param {Record<string, string | string[] | boolean>} options @param {string[]} paths */
|
|
@@ -278,7 +278,7 @@ function header(config) {
|
|
|
278
278
|
|
|
279
279
|
/** @param {Map<string, import("../rules/finding.js").Finding[]>} results */
|
|
280
280
|
function summary(results) {
|
|
281
|
-
// 글에 실제로 있는 수. 거른 뒤가 아니라 거르기 전을 센다. 뜻은 파이썬 shared.summary
|
|
281
|
+
// 글에 실제로 있는 수. 거른 뒤가 아니라 거르기 전을 센다. 뜻은 파이썬 shared.summary가 소유한다.
|
|
282
282
|
let errors = 0;
|
|
283
283
|
let notices = 0;
|
|
284
284
|
for (const findings of results.values()) {
|
|
@@ -287,16 +287,16 @@ function summary(results) {
|
|
|
287
287
|
else if (f.severity === "notice") notices += 1;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
return `파일 ${results.size}개,
|
|
290
|
+
return `파일 ${results.size}개, 오류 ${errors}건, 경고 ${notices}건`;
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
/** (이름, 본문).
|
|
293
|
+
/** (이름, 본문). `-`면 stdin을 UTF-8로 읽는다. @param {string} path @param {string} stdinName @returns {[string, string]} */
|
|
294
294
|
function readInput(path, stdinName) {
|
|
295
295
|
if (path === STDIN) return [stdinName, readFileSync(0, "utf-8")];
|
|
296
296
|
return [path, readOne(path)];
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
/** 글 하나를 읽는다. 폴더를 주면 말로 막는다. 뜻은 파이썬 shared.readFile
|
|
299
|
+
/** 글 하나를 읽는다. 폴더를 주면 말로 막는다. 뜻은 파이썬 shared.readFile이 소유한다. @param {string} path */
|
|
300
300
|
function readOne(path) {
|
|
301
301
|
let isFolder = false;
|
|
302
302
|
try {
|
|
@@ -304,11 +304,11 @@ function readOne(path) {
|
|
|
304
304
|
} catch {
|
|
305
305
|
isFolder = false;
|
|
306
306
|
}
|
|
307
|
-
if (isFolder) throw new Error(
|
|
307
|
+
if (isFolder) throw new Error(`폴더를 받았다: ${path}. 이 명령은 글 하나를 본다. 폴더는 hanlint와 hanlint fix가 받는다`);
|
|
308
308
|
return readFileSync(path, "utf-8");
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
/** 폴더를 주면 그 아래 마크다운을 편다. 파일과
|
|
311
|
+
/** 폴더를 주면 그 아래 마크다운을 편다. 파일과 `-`는 그대로 둔다. @param {string[]} paths */
|
|
312
312
|
function collectFiles(paths) {
|
|
313
313
|
/** @type {string[]} */
|
|
314
314
|
const found = [];
|
|
@@ -330,7 +330,7 @@ function collectFiles(paths) {
|
|
|
330
330
|
const inside = markdownUnder(path).sort();
|
|
331
331
|
if (!inside.length)
|
|
332
332
|
throw new Error(
|
|
333
|
-
`${path} 안에 마크다운 파일이 없다. 점으로 시작하는 폴더와 node_modules
|
|
333
|
+
`${path} 안에 마크다운 파일이 없다. 점으로 시작하는 폴더와 node_modules는 건너뛴다. 그 안을 보려면 그 폴더를 직접 준다`,
|
|
334
334
|
);
|
|
335
335
|
found.push(...inside);
|
|
336
336
|
}
|
|
@@ -346,7 +346,7 @@ function commonest(findings) {
|
|
|
346
346
|
return [...counted.keys()].sort((a, b) => (counted.get(b) ?? 0) - (counted.get(a) ?? 0) || a.localeCompare(b))[0];
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
/** `hanlint fix
|
|
349
|
+
/** `hanlint fix`가 실제로 고칠 error 수. 고침이 달렸다는 것과 자리를 잡을 수 있다는 것은 다르다.
|
|
350
350
|
* @param {Map<string, string>} texts @param {Map<string, import("../rules/finding.js").Finding[]>} results */
|
|
351
351
|
function fixableCount(texts, results) {
|
|
352
352
|
let total = 0;
|
|
@@ -358,7 +358,7 @@ function fixableCount(texts, results) {
|
|
|
358
358
|
return total;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/** 검사 끝에 붙는 다음 행동 한 줄. 뜻은 파이썬 cli/commands/shared.py
|
|
361
|
+
/** 검사 끝에 붙는 다음 행동 한 줄. 뜻은 파이썬 cli/commands/shared.py가 소유한다.
|
|
362
362
|
* @param {Map<string, import("../rules/finding.js").Finding[]>} results @param {number} [fixable] */
|
|
363
363
|
function nextStep(results, fixable = 0) {
|
|
364
364
|
const findings = [...results.values()].flat();
|
|
@@ -366,17 +366,17 @@ function nextStep(results, fixable = 0) {
|
|
|
366
366
|
const notices = findings.length - errors.length;
|
|
367
367
|
if (errors.length) {
|
|
368
368
|
const rule = commonest(errors);
|
|
369
|
-
if (fixable) return `다음:
|
|
370
|
-
if (patternsAvoiding(rule).length) return `다음:
|
|
371
|
-
return `다음:
|
|
369
|
+
if (fixable) return `다음: 오류 ${errors.length}건 가운데 ${fixable}건은 hanlint fix가 바로 고친다. 나머지는 손으로 고친다`;
|
|
370
|
+
if (patternsAvoiding(rule).length) return `다음: 오류 ${errors.length}건을 고친다. 다시 쓸 틀은 hanlint patterns --rule ${rule}`;
|
|
371
|
+
return `다음: 오류 ${errors.length}건을 고친다. 규칙이 왜 있는지는 hanlint explain ${rule}`;
|
|
372
372
|
}
|
|
373
|
-
if (notices) return `다음:
|
|
374
|
-
return "다음:
|
|
373
|
+
if (notices) return `다음: 오류는 없다. 경고 ${notices}건을 읽고 고칠지 정한다`;
|
|
374
|
+
return "다음: 규칙으로 찾은 잘못이 없다. 좋은 글이라는 뜻은 아니니 사람이 읽어 본다";
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
/** @param {Baseline} baseline */
|
|
378
378
|
function lockedNote(baseline) {
|
|
379
|
-
return
|
|
379
|
+
return `baseline 파일에 든 지적 ${baseline.count}건은 보이지 않았다 (${baseline.source}). 그 문장을 고치면 다시 나온다`;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
/** 옵션과 설정에서 잠금 파일을 연다. 둘 다 없으면 빈 잠금이다. @param {Record<string, string | string[] | boolean>} options @param {import("../config/settings.js").Config} config */
|
|
@@ -401,10 +401,10 @@ function runBaseline(args) {
|
|
|
401
401
|
}
|
|
402
402
|
const baseline = options["--prune"] ? pruneBaseline(loadBaseline(target), results) : buildBaseline(results, target);
|
|
403
403
|
writeFileSync(target, `${renderBaseline(baseline)}\n`, "utf-8");
|
|
404
|
-
const what = options["--prune"] ? "
|
|
404
|
+
const what = options["--prune"] ? "글에 더 없는 지적을 지웠다" : "지금 있는 지적이다";
|
|
405
405
|
process.stdout.write(
|
|
406
|
-
`${baseline.source}
|
|
407
|
-
"다음: 검사할 때 --baseline
|
|
406
|
+
`${baseline.source}에 ${baseline.count}건을 적었다. ${files.length}개 글에서 ${what}.\n` +
|
|
407
|
+
"다음: 검사할 때 --baseline을 붙이거나 설정에 baseline을 적는다. baseline 파일에 든 문장을 고치면 새 지적으로 다시 나온다\n",
|
|
408
408
|
);
|
|
409
409
|
return 0;
|
|
410
410
|
}
|
|
@@ -446,7 +446,7 @@ function runContract(args) {
|
|
|
446
446
|
}
|
|
447
447
|
const output = options["--output"];
|
|
448
448
|
if (typeof output === "string" && existsSync(output) && !options["--force"]) {
|
|
449
|
-
throw new Error(
|
|
449
|
+
throw new Error(`이미 있는 파일: ${output}. 덮어쓰려면 --force를 붙인다`);
|
|
450
450
|
}
|
|
451
451
|
const outline = options["--outline"];
|
|
452
452
|
const facts = /** @type {string[]} */ (options["--fact"] ?? []);
|
|
@@ -567,18 +567,18 @@ function runFix(args) {
|
|
|
567
567
|
const lines = [];
|
|
568
568
|
for (const path of files) {
|
|
569
569
|
const text = readFileSync(path, "utf-8");
|
|
570
|
-
// notice
|
|
570
|
+
// notice는 제안이라 손으로 정한다. 확정된 error만 원문에 넣는다.
|
|
571
571
|
const errors = runAll(fingerprint(text, config, path), config).filter((f) => f.severity === "error");
|
|
572
572
|
const result = applyFixes(text, errors);
|
|
573
573
|
for (const [line, fragment, replacement] of result.applied) lines.push(`${path}:${line} ${fragment} → ${replacement}`);
|
|
574
574
|
for (const [line, fragment, reason] of result.skipped) lines.push(`${path}:${line} 건너뜀 ${fragment}: ${reason}`);
|
|
575
575
|
if (result.text !== text && !options["--dry-run"]) writeFileSync(path, result.text, "utf-8");
|
|
576
|
-
// 남은 수를 함께 말한다. `0곳 고침, 0곳
|
|
576
|
+
// 남은 수를 함께 말한다. `0곳 고침, 0곳 건너뜀`만 보면 손볼 것이 없다는 뜻으로 읽힌다
|
|
577
577
|
const left = errors.length - result.applied.length;
|
|
578
|
-
const rest = left ? `. 손으로 고칠
|
|
578
|
+
const rest = left ? `. 손으로 고칠 오류 ${left}건이 남았다 (hanlint ${path})` : "";
|
|
579
579
|
lines.push(
|
|
580
580
|
`${path} ${result.applied.length}곳 고침, ${result.skipped.length}곳 건너뜀` +
|
|
581
|
-
(options["--dry-run"] ? " (
|
|
581
|
+
(options["--dry-run"] ? " (미리보기라 파일은 그대로다)" : "") +
|
|
582
582
|
rest,
|
|
583
583
|
);
|
|
584
584
|
}
|
|
@@ -589,7 +589,7 @@ function runFix(args) {
|
|
|
589
589
|
/** @param {string[]} args */
|
|
590
590
|
function runPrint(args) {
|
|
591
591
|
const { options, positionals } = parseArgs(args);
|
|
592
|
-
if (positionals.length !== 1) throw new UsageError("마크다운 파일 하나가 필요하다.
|
|
592
|
+
if (positionals.length !== 1) throw new UsageError("마크다운 파일 하나가 필요하다. `-`는 stdin");
|
|
593
593
|
const layer = choose(/** @type {string} */ (options["--layer"] ?? "all"), LAYERS, "--layer");
|
|
594
594
|
const config = configFrom(options, positionals);
|
|
595
595
|
const [name, text] = readInput(positionals[0], /** @type {string} */ (options["--path"] ?? STDIN_NAME));
|
|
@@ -632,15 +632,15 @@ function runRules(args) {
|
|
|
632
632
|
let tail = `규칙 ${names.length}개`;
|
|
633
633
|
if (off.size) {
|
|
634
634
|
const byPreset = PRESETS[config.preset].length;
|
|
635
|
-
tail +=
|
|
635
|
+
tail += ` 가운데 ${off.size}개가 꺼져 있다 (${config.preset} 프리셋 ${byPreset}개, disable ${off.size - byPreset}개)`;
|
|
636
636
|
}
|
|
637
637
|
lines.push(`${tail}. 하나를 자세히 보려면 hanlint explain <규칙>`);
|
|
638
|
-
lines.push(
|
|
638
|
+
lines.push(`프리셋: ${PRESET_NAMES.join(", ")}. hanlint init --preset <이름>이 설정 파일에 적는다`);
|
|
639
639
|
emit(lines.join("\n"), output);
|
|
640
640
|
return 0;
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
-
/** 여러 줄짜리
|
|
643
|
+
/** 여러 줄짜리 예시를 첫 줄에만 표를 달고 나머지는 맞춰 들여쓴다. @param {string} text @param {string} label */
|
|
644
644
|
function indent(text, label) {
|
|
645
645
|
const pad = " ".repeat(label.length);
|
|
646
646
|
return text.replace(/\n+$/, "").split("\n").map((line, index) => (index === 0 ? label : pad) + line).join("\n");
|
|
@@ -651,7 +651,7 @@ const NEAR_LIMIT = 3;
|
|
|
651
651
|
/** 앞 몇 글자가 같으면 가까운 이름으로 보는가. */
|
|
652
652
|
const NEAR_PREFIX = 3;
|
|
653
653
|
|
|
654
|
-
/** 오타에 가까운 이름. 파이썬 cli/commands/explain.py
|
|
654
|
+
/** 오타에 가까운 이름. 파이썬 cli/commands/explain.py의 nearNames와 같다. @param {string} query @param {string[]} names */
|
|
655
655
|
function nearNames(query, names) {
|
|
656
656
|
const lowered = query.toLowerCase();
|
|
657
657
|
/** @type {[number, string][]} */
|
|
@@ -671,15 +671,15 @@ function runExplain(args) {
|
|
|
671
671
|
const names = ruleNames();
|
|
672
672
|
if (!positionals.length) {
|
|
673
673
|
process.stdout.write("규칙 이름 하나가 필요하다. 예: hanlint explain doublePassive\n");
|
|
674
|
-
process.stdout.write(`\n규칙 ${names.length}개를
|
|
674
|
+
process.stdout.write(`\n규칙 ${names.length}개를 카테고리별로 보려면 hanlint rules, 이름만 보려면 hanlint rules --names\n`);
|
|
675
675
|
return 2;
|
|
676
676
|
}
|
|
677
|
-
if (positionals.length !== 1) throw new UsageError("규칙 이름 하나가 필요하다. hanlint rules
|
|
677
|
+
if (positionals.length !== 1) throw new UsageError("규칙 이름 하나가 필요하다. hanlint rules로 목록을 본다");
|
|
678
678
|
const wanted = positionals[0];
|
|
679
679
|
if (!names.includes(wanted)) {
|
|
680
680
|
const near = nearNames(wanted, names);
|
|
681
|
-
const hint = near.length ? `
|
|
682
|
-
throw new Error(
|
|
681
|
+
const hint = near.length ? ` 비슷한 이름: ${near.join(", ")}` : " 목록은 hanlint rules";
|
|
682
|
+
throw new Error(`없는 규칙: ${wanted}.${hint}`);
|
|
683
683
|
}
|
|
684
684
|
const category = ruleCategory(wanted);
|
|
685
685
|
const config = configFrom(options, []);
|
|
@@ -698,18 +698,18 @@ function runExplain(args) {
|
|
|
698
698
|
return 0;
|
|
699
699
|
}
|
|
700
700
|
const mechanism = ruleMechanism(wanted);
|
|
701
|
-
process.stdout.write(`${wanted} (${CATEGORY_TITLES[category]})\n
|
|
701
|
+
process.stdout.write(`${wanted} (${CATEGORY_TITLES[category]})\n방식: ${MECHANISMS[mechanism]} (${mechanism})\n\n${ruleDoc(wanted)}\n`);
|
|
702
702
|
const exemplar = exemplarOf ? exemplarInRegister(exemplarOf, register) : undefined;
|
|
703
703
|
if (exemplar) {
|
|
704
|
-
process.stdout.write("\n
|
|
704
|
+
process.stdout.write("\n예시\n");
|
|
705
705
|
process.stdout.write(`${indent(exemplar.before, " 전 ")}\n`);
|
|
706
706
|
process.stdout.write(`${indent(exemplar.after, " 후 ")}\n`);
|
|
707
707
|
process.stdout.write(` 달라진 것: ${exemplar.moved}\n`);
|
|
708
708
|
}
|
|
709
709
|
const siblings = names.filter((name) => ruleCategory(name) === category && name !== wanted);
|
|
710
|
-
process.stdout.write(`\n같은
|
|
711
|
-
process.stdout.write(`끄려면 hanlint.toml
|
|
712
|
-
process.stdout.write(
|
|
710
|
+
process.stdout.write(`\n같은 카테고리: ${siblings.join(", ")}\n`);
|
|
711
|
+
process.stdout.write(`끄려면 hanlint.toml의 disable 목록에 넣는다: ${wanted}. 한 곳만 끄려면 <!-- hanlint-disable ${wanted} -->\n`);
|
|
712
|
+
process.stdout.write(`맞는 문장인데 잡혔다면 끄기만 하지 말고 여기에 알린다: ${ISSUES}\n`);
|
|
713
713
|
return 0;
|
|
714
714
|
}
|
|
715
715
|
|
|
@@ -719,7 +719,7 @@ function runPatterns(args) {
|
|
|
719
719
|
const rule = /** @type {string | undefined} */ (options["--rule"]);
|
|
720
720
|
const register = choose(/** @type {string} */ (options["--register"] ?? HAPNIDA), REGISTERS, "--register");
|
|
721
721
|
const chosen = (rule ? patternsAvoiding(rule) : patterns()).map((pattern) => patternInRegister(pattern, register));
|
|
722
|
-
if (!chosen.length) throw new Error(
|
|
722
|
+
if (!chosen.length) throw new Error(`을 피하는 문형이 없다. hanlint patterns로 전부 본다`);
|
|
723
723
|
const output = /** @type {string | undefined} */ (options["--output"]);
|
|
724
724
|
if (options["--format"] === "json") {
|
|
725
725
|
emit(JSON.stringify({ version: 1, patterns: chosen }, null, 2), output);
|
|
@@ -727,7 +727,7 @@ function runPatterns(args) {
|
|
|
727
727
|
}
|
|
728
728
|
const lines = [];
|
|
729
729
|
for (const pattern of chosen) {
|
|
730
|
-
lines.push(`${pattern.name} (${pattern.avoids.join(", ")}
|
|
730
|
+
lines.push(`${pattern.name} (피하는 규칙: ${pattern.avoids.join(", ")})`);
|
|
731
731
|
lines.push(` 틀 ${pattern.form}`);
|
|
732
732
|
lines.push(` 언제 ${pattern.when}`);
|
|
733
733
|
lines.push(indent(pattern.example, " 예시 "));
|
|
@@ -735,13 +735,13 @@ function runPatterns(args) {
|
|
|
735
735
|
lines.push(` 출처 ${pattern.source}`);
|
|
736
736
|
lines.push("");
|
|
737
737
|
}
|
|
738
|
-
lines.push(`문형 ${chosen.length}개.
|
|
738
|
+
lines.push(`문형 ${chosen.length}개. 예시 문장은 모두 hanlint 검사에서 오류가 없다`);
|
|
739
739
|
if (!rule) lines.push("지적을 받은 자리를 다시 쓰려면 hanlint patterns --rule <규칙>");
|
|
740
740
|
emit(lines.join("\n"), output);
|
|
741
741
|
return 0;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
-
/** 여러 줄을 한 줄로. 줄바꿈은 ¶ 다. 파이썬 cli/commands/primer.py
|
|
744
|
+
/** 여러 줄을 한 줄로. 줄바꿈은 ¶ 다. 파이썬 cli/commands/primer.py의 flat과 같다. @param {string} text */
|
|
745
745
|
function flat(text) {
|
|
746
746
|
return text
|
|
747
747
|
.split("\n")
|
|
@@ -751,7 +751,7 @@ function flat(text) {
|
|
|
751
751
|
}
|
|
752
752
|
|
|
753
753
|
/**
|
|
754
|
-
* 설정에서 켜진 규칙마다 항목 하나.
|
|
754
|
+
* 설정에서 켜진 규칙마다 항목 하나. 묶음 순서, 그 안은 이름 순서. 파이썬 primerEntries와 같다.
|
|
755
755
|
* @param {import("../config/settings.js").Config} config @param {string} register
|
|
756
756
|
*/
|
|
757
757
|
function primerEntries(config, register) {
|
|
@@ -761,7 +761,7 @@ function primerEntries(config, register) {
|
|
|
761
761
|
for (const name of ruleNames()) {
|
|
762
762
|
if (ruleCategory(name) !== category || off.has(name)) continue;
|
|
763
763
|
const exemplar = exemplarFor(name, config.preset, config.exemplars);
|
|
764
|
-
if (!exemplar) throw new Error(
|
|
764
|
+
if (!exemplar) throw new Error(`예시가 없는 규칙: ${name}`);
|
|
765
765
|
const shown = exemplarInRegister(exemplar, register);
|
|
766
766
|
entries.push({ rule: name, category, fix: ruleFix(name), before: shown.before, after: shown.after });
|
|
767
767
|
}
|
|
@@ -769,17 +769,17 @@ function primerEntries(config, register) {
|
|
|
769
769
|
return entries;
|
|
770
770
|
}
|
|
771
771
|
|
|
772
|
-
/**
|
|
772
|
+
/** 글쓰기 지침를 절마다 한 줄로. 제목 두 칸 띄고 본문이다. 파이썬 proseLines와 같다. */
|
|
773
773
|
function proseLines() {
|
|
774
774
|
return proseSections().map(([title, body]) => `${title} ${body.split(/\s+/).filter(Boolean).join(" ")}`);
|
|
775
775
|
}
|
|
776
776
|
|
|
777
|
-
/** 사람과 AI
|
|
777
|
+
/** 사람과 AI가 읽는 한 장. 파이썬 renderPrimer와 글자 단위로 같다. @param {ReturnType<typeof primerEntries>} entries @param {string} preset @param {string} register */
|
|
778
778
|
function renderPrimer(entries, preset, register) {
|
|
779
779
|
const lines = [
|
|
780
780
|
...proseLines(),
|
|
781
781
|
"",
|
|
782
|
-
`hanlint primer ${preset} 종류, ${register}체, 규칙 ${entries.length}개. 줄마다 규칙 이름, 고치는 법, 전
|
|
782
|
+
`hanlint primer ${preset} 종류, ${register}체, 규칙 ${entries.length}개. 줄마다 규칙 이름, 고치는 법, 전(그 규칙에 잡히는 글), 후(그 예시에서 고친 글) 순서다. 예시의 줄바꿈은 ¶로 보인다`,
|
|
783
783
|
];
|
|
784
784
|
for (const [category, title] of Object.entries(CATEGORY_TITLES)) {
|
|
785
785
|
const inside = entries.filter((entry) => entry.category === category);
|
|
@@ -787,7 +787,7 @@ function renderPrimer(entries, preset, register) {
|
|
|
787
787
|
lines.push("", `${title} (${inside.length})`);
|
|
788
788
|
for (const entry of inside) lines.push(`${entry.rule} ${entry.fix} 전: ${flat(entry.before)} 후: ${flat(entry.after)}`);
|
|
789
789
|
}
|
|
790
|
-
lines.push("", "
|
|
790
|
+
lines.push("", "예시는 그 문장에만 맞는다. 용어와 사실은 그대로 두고, 바꾼 말을 다른 글에 그대로 옮겨 쓰지 않는다. 규칙을 더 보려면 hanlint explain <규칙>");
|
|
791
791
|
return lines.join("\n");
|
|
792
792
|
}
|
|
793
793
|
|
|
@@ -816,7 +816,7 @@ function runSpec(args) {
|
|
|
816
816
|
if (options["--chars"] !== undefined) {
|
|
817
817
|
const raw = /** @type {string} */ (options["--chars"]);
|
|
818
818
|
targetChars = Number(raw);
|
|
819
|
-
if (!Number.isInteger(targetChars) || targetChars <= 0) throw new UsageError(`--chars
|
|
819
|
+
if (!Number.isInteger(targetChars) || targetChars <= 0) throw new UsageError(`--chars는 1 이상의 정수다: ${raw}`);
|
|
820
820
|
}
|
|
821
821
|
const result = writingSpec(config, register, targetChars);
|
|
822
822
|
const output = /** @type {string | undefined} */ (options["--output"]);
|
|
@@ -825,7 +825,7 @@ function runSpec(args) {
|
|
|
825
825
|
return 0;
|
|
826
826
|
}
|
|
827
827
|
|
|
828
|
-
/**
|
|
828
|
+
/** 글쓰기 지침를 싣는 이벤트. Claude Code와 Codex 모두 이 둘의 additionalContext를 모델 문맥에 넣는다. */
|
|
829
829
|
const PRIME_EVENTS = ["SessionStart", "UserPromptSubmit"];
|
|
830
830
|
|
|
831
831
|
/** 에이전트가 다음 모델 요청에 넣는 비차단 문맥. @param {string} eventName @param {string} context */
|
|
@@ -833,10 +833,10 @@ function hookContext(eventName, context) {
|
|
|
833
833
|
return JSON.stringify({ hookSpecificOutput: { hookEventName: eventName, additionalContext: context } });
|
|
834
834
|
}
|
|
835
835
|
|
|
836
|
-
/** Finding을 되먹이는 문맥의 본문. 파일마다 한 덩어리다. 파이썬 findingContext
|
|
836
|
+
/** Finding을 되먹이는 문맥의 본문. 파일마다 한 덩어리다. 파이썬 findingContext와 같다. @param {string} eventName @param {[string, import("../rules/finding.js").Finding[]][]} found */
|
|
837
837
|
function findingContext(eventName, found) {
|
|
838
838
|
const where = eventName === "Stop" ? "마지막 답변" : "방금 쓴 마크다운";
|
|
839
|
-
return `hanlint가 ${where}에서
|
|
839
|
+
return `hanlint가 ${where}에서 찾은 지적입니다.\n${found.map(([name, items]) => renderCompact(name, items)).join("\n")}`;
|
|
840
840
|
}
|
|
841
841
|
|
|
842
842
|
/** Codex apply_patch 본문에서 쓴 파일을 가리키는 머리 줄 (Codex 훅 문서, 2026-09-21 확인). */
|
|
@@ -891,7 +891,7 @@ function writtenFindings(payload, options) {
|
|
|
891
891
|
|
|
892
892
|
/** 명령 훅 JSON을 stdin에서 읽어 저장 파일이나 마지막 답변을 검사한다. 언제나 종료 코드 0이다. @param {string[]} args */
|
|
893
893
|
function runHook(args) {
|
|
894
|
-
if (args.includes("--prime") && args.includes("--reply")) throw new UsageError("--prime
|
|
894
|
+
if (args.includes("--prime") && args.includes("--reply")) throw new UsageError("--prime과 --reply는 함께 쓰지 않는다");
|
|
895
895
|
const { options, positionals } = parseArgs(args);
|
|
896
896
|
const agent = choose(/** @type {string} */ (options["--agent"] ?? "claude"), ["claude", "codex"], "--agent");
|
|
897
897
|
try {
|
|
@@ -929,13 +929,13 @@ function runHook(args) {
|
|
|
929
929
|
return 0;
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
-
/** 잠근 지적이 몇 건인지. baseline
|
|
932
|
+
/** 잠근 지적이 몇 건인지. baseline이 빚을 감추는 자리가 되지 않게 늘 보인다. @param {import("../config/settings.js").Config} config */
|
|
933
933
|
function baselineState(config) {
|
|
934
|
-
if (!config.baseline) return "
|
|
934
|
+
if (!config.baseline) return "없음 (만들려면 hanlint baseline 글들/)";
|
|
935
935
|
try {
|
|
936
|
-
return `${
|
|
936
|
+
return `${config.baseline}에 지적 ${loadBaseline(config.baseline).count}건이 들어 있다. 그 문장을 고치면 다시 나온다`;
|
|
937
937
|
} catch (error) {
|
|
938
|
-
return
|
|
938
|
+
return `읽지 못했다: ${config.baseline} (${/** @type {Error} */ (error).message})`;
|
|
939
939
|
}
|
|
940
940
|
}
|
|
941
941
|
|
|
@@ -952,22 +952,22 @@ function runDoctor(args) {
|
|
|
952
952
|
`설정 ${configLabel(config)}`,
|
|
953
953
|
`프리셋 ${config.preset} (${PRESET_NAMES.join(", ")} 가운데)`,
|
|
954
954
|
`규칙 ${names.length - off.length}개 켜짐, ${off.length}개 꺼짐`,
|
|
955
|
-
|
|
956
|
-
`폴더 점으로 시작하는
|
|
955
|
+
`baseline 파일 ${baselineState(config)}`,
|
|
956
|
+
`폴더 건너뛰는 것: 점으로 시작하는 폴더, ${SKIPPED_FOLDERS.join(", ")}. 직접 주면 검사한다`,
|
|
957
957
|
];
|
|
958
958
|
if (off.length) lines.push(`꺼진 규칙 ${off.join(", ")}`);
|
|
959
|
-
lines.push("", "다음: hanlint 글.md
|
|
959
|
+
lines.push("", "다음: hanlint 글.md로 검사한다. 설정 파일이 없으면 hanlint init으로 만든다");
|
|
960
960
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
961
961
|
return 0;
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
-
/** 주석 달린 hanlint.toml. 파이썬 `hanlint init
|
|
964
|
+
/** 주석 달린 hanlint.toml. 파이썬 `hanlint init`과 같은 글자다. @param {string} [preset] */
|
|
965
965
|
export function renderInit(preset = "blog") {
|
|
966
966
|
const defaults = defaultConfig();
|
|
967
967
|
const lines = [
|
|
968
|
-
"# hanlint 설정. 규칙을 끄려면 disable
|
|
968
|
+
"# hanlint 설정. 규칙을 끄려면 disable에 이름을 넣는다. 규칙 설명은 hanlint explain <규칙>으로 본다.",
|
|
969
969
|
"",
|
|
970
|
-
"# 글의 종류. 그 종류에 안 맞는 규칙을 처음부터 끈다. disable
|
|
970
|
+
"# 글의 종류. 그 종류에 안 맞는 규칙을 처음부터 끈다. disable은 그 위에 더한다.",
|
|
971
971
|
];
|
|
972
972
|
for (const [name, off] of Object.entries(PRESETS)) lines.push(`# ${name}: ${off.length ? off.join(", ") : "전부 켠다"}`);
|
|
973
973
|
lines.push(`preset = "${preset}"`, "", "# 규칙 목록과 한 줄 설명");
|
|
@@ -981,23 +981,23 @@ export function renderInit(preset = "blog") {
|
|
|
981
981
|
"",
|
|
982
982
|
"disable = []",
|
|
983
983
|
"",
|
|
984
|
-
"# 대표 검색어를 읽을 frontmatter 필드. 없으면 keywordMissing
|
|
984
|
+
"# 대표 검색어를 읽을 frontmatter 필드. 없으면 keywordMissing은 돌지 않는다",
|
|
985
985
|
'# keywordField = "primaryKeyword"',
|
|
986
986
|
"",
|
|
987
|
-
"# 도입과 마지막 절이 담아야 하는 frontmatter 필드. 비어 있으면 fieldEcho
|
|
987
|
+
"# 도입과 마지막 절이 담아야 하는 frontmatter 필드. 비어 있으면 fieldEcho는 돌지 않는다",
|
|
988
988
|
'# introFields = ["readerQuestion"]',
|
|
989
989
|
'# endingFields = ["readerTakeaway"]',
|
|
990
990
|
"",
|
|
991
|
-
"# hanlint profile build
|
|
991
|
+
"# hanlint profile build가 만든 파일. 있으면 글 종류의 기본 프로파일 대신 이 파일과 비교한다(outsideProfile)",
|
|
992
992
|
'# profile = "profile.json"',
|
|
993
993
|
"",
|
|
994
|
-
"# hanlint baseline
|
|
994
|
+
"# hanlint baseline이 만든 baseline 파일. 있으면 그 안의 지적은 보이지 않고 새로 생긴 지적만 막는다",
|
|
995
995
|
'# baseline = ".hanlint-baseline.json"',
|
|
996
996
|
"",
|
|
997
|
-
"# 코드도 산문도 아닌 펜스의 언어
|
|
997
|
+
"# 코드도 산문도 아닌 펜스의 언어 이름. 이 펜스는 코드 블록으로 세지 않고 검사에서도 뺀다",
|
|
998
998
|
'# ignoreFences = ["course-scene", "mermaid"]',
|
|
999
999
|
"",
|
|
1000
|
-
"#
|
|
1000
|
+
"# 기준값. 아래 값이 기본값이다",
|
|
1001
1001
|
);
|
|
1002
1002
|
for (const name of THRESHOLD_FIELDS) {
|
|
1003
1003
|
const value = /** @type {number} */ (defaults[name]);
|
|
@@ -1006,9 +1006,9 @@ export function renderInit(preset = "blog") {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
lines.push(
|
|
1008
1008
|
"",
|
|
1009
|
-
"# 용례.
|
|
1010
|
-
"#
|
|
1011
|
-
"# 종류는 프리셋이 정한다 (report
|
|
1009
|
+
"# 용례. 그 종류의 실제 글에 흔한 명사 연쇄(nounPile)와 사전 표현은 짚지 않는다. 명사 연쇄는 usageMin편 이상의",
|
|
1010
|
+
"# 문서에 나온 것, 사전 표현은 문서의 usageShare 비율 이상에 나온 것(사업보고서의 `에 대한`)이다.",
|
|
1011
|
+
"# 종류는 프리셋이 정한다 (report만 있다). 빈 문자열이면 어떤 프리셋에서도 용례를 보지 않는다",
|
|
1012
1012
|
'# usageKind = ""',
|
|
1013
1013
|
"# usageMin = 3",
|
|
1014
1014
|
"# usageShare = 0.9",
|
|
@@ -1019,31 +1019,31 @@ export function renderInit(preset = "blog") {
|
|
|
1019
1019
|
'# cliches = ["우리의 여정"]',
|
|
1020
1020
|
'# translationese = [{ pattern = "에 대한 이해", fix = "를 아는 것" }]',
|
|
1021
1021
|
"",
|
|
1022
|
-
"#
|
|
1022
|
+
"# 자동 고침이 건드리지 말아야 할 한국어 고유명사와 프로젝트 용어. 숫자와 영문 식별자는 저절로 지킨다",
|
|
1023
1023
|
'# protectedTerms = ["한린트", "김민지"]',
|
|
1024
|
-
'# enforceStyle = ["noQuestion", "nounPile"] #
|
|
1024
|
+
'# enforceStyle = ["noQuestion", "nounPile"] # 고른 문체 규칙만 오류로 낸다',
|
|
1025
1025
|
"",
|
|
1026
|
-
"# 사람이 승인한 프로젝트
|
|
1026
|
+
"# 사람이 승인한 프로젝트 예시. 같은 규칙과 프리셋의 기본 예시 대신 쓴다",
|
|
1027
1027
|
"# [[exemplars]]",
|
|
1028
1028
|
'# rule = "translationese"',
|
|
1029
1029
|
'# before = "설계에 대한 이해가 필요합니다."',
|
|
1030
1030
|
'# after = "설계를 알아야 합니다."',
|
|
1031
|
-
'# moved = "명사구를 서술어로 풀어
|
|
1031
|
+
'# moved = "명사구를 서술어로 풀어 썼다"',
|
|
1032
1032
|
'# presets = ["blog"]',
|
|
1033
1033
|
"",
|
|
1034
|
-
"# 사람이 승인한
|
|
1034
|
+
"# 사람이 승인한 그 자리만의 고침. 원문을 포함해 조건이 모두 맞을 때만 그대로 적용한다",
|
|
1035
1035
|
"# [[patches]]",
|
|
1036
1036
|
'# rule = "translationese"',
|
|
1037
1037
|
'# before = "설계에 대한 이해가 필요합니다."',
|
|
1038
1038
|
'# after = "설계를 알아야 합니다."',
|
|
1039
|
-
'# moved = "명사구를 서술어로 풀어
|
|
1039
|
+
'# moved = "명사구를 서술어로 풀어 썼다"',
|
|
1040
1040
|
'# sourceText = "설계에 대한 이해가 필요합니다." # before의 마크다운까지 보존한 선택용 원문',
|
|
1041
1041
|
'# sentence = "설계에 대한 이해가 필요합니다." # before에서 마크다운 표식을 걷은 선택용 원문',
|
|
1042
1042
|
'# cue = "에 대한"',
|
|
1043
1043
|
'# reader = "new"',
|
|
1044
1044
|
'# presets = ["blog"]',
|
|
1045
1045
|
"",
|
|
1046
|
-
"# 사람이 적용 범위까지 승인한
|
|
1046
|
+
"# 사람이 적용 범위까지 승인한 치환. 단어 경계가 맞고 숫자·링크·코드가 그대로일 때만 바꾼다",
|
|
1047
1047
|
"# [[operations]]",
|
|
1048
1048
|
'# before = "여러가지"',
|
|
1049
1049
|
'# after = "여러 가지"',
|
|
@@ -1058,15 +1058,15 @@ function runInit(args) {
|
|
|
1058
1058
|
const { options } = parseArgs(args);
|
|
1059
1059
|
const path = /** @type {string} */ (options["--output"] ?? "hanlint.toml");
|
|
1060
1060
|
const preset = choose(/** @type {string} */ (options["--preset"] ?? "blog"), PRESET_NAMES, "--preset");
|
|
1061
|
-
if (existsSync(path) && !options["--force"]) throw new Error(
|
|
1061
|
+
if (existsSync(path) && !options["--force"]) throw new Error(`이미 있는 파일: ${path}. 덮어쓰려면 --force를 붙인다`);
|
|
1062
1062
|
writeFileSync(path, renderInit(preset), "utf-8");
|
|
1063
1063
|
const off = PRESETS[preset];
|
|
1064
|
-
const tail = off.length ?
|
|
1065
|
-
process.stdout.write(
|
|
1064
|
+
const tail = off.length ? `${preset} 프리셋은 규칙 ${off.length}개를 끈다` : `${preset} 프리셋은 규칙을 전부 켠다`;
|
|
1065
|
+
process.stdout.write(`만들었다: ${path}. ${tail}. 더 끄려면 disable에 이름을 넣는다\n`);
|
|
1066
1066
|
return 0;
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
1069
|
-
/** 폴더 아래 소스 파일. 뜻은 파이썬 cli/commands/sheet.py
|
|
1069
|
+
/** 폴더 아래 소스 파일. 뜻은 파이썬 cli/commands/sheet.py의 sourceUnder가 소유한다. @param {string} folder @returns {string[]} */
|
|
1070
1070
|
function sourceUnder(folder) {
|
|
1071
1071
|
/** @type {string[]} */
|
|
1072
1072
|
const found = [];
|
|
@@ -1076,24 +1076,24 @@ function sourceUnder(folder) {
|
|
|
1076
1076
|
if (!isSkipped(name)) found.push(...sourceUnder(path));
|
|
1077
1077
|
} else if (SOURCE_SUFFIXES.includes(extname(name).toLowerCase())) found.push(path);
|
|
1078
1078
|
}
|
|
1079
|
-
// 구분자를 / 로 맞춘 뒤 정렬한다. 뜻은 파이썬 sourceUnder
|
|
1079
|
+
// 구분자를 / 로 맞춘 뒤 정렬한다. 뜻은 파이썬 sourceUnder가 소유한다.
|
|
1080
1080
|
const posix = (item) => item.split(sep).join("/");
|
|
1081
1081
|
return found.sort((a, b) => (posix(a) < posix(b) ? -1 : posix(a) > posix(b) ? 1 : 0));
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
|
-
/** 작업 폴더 기준 상대 경로, 구분자는 `/`. 파이썬 relativeLabel
|
|
1084
|
+
/** 작업 폴더 기준 상대 경로, 구분자는 `/`. 파이썬 relativeLabel과 같은 글자를 낸다. @param {string} path */
|
|
1085
1085
|
function relativeLabel(path) {
|
|
1086
1086
|
const rel = relative(process.cwd(), resolve(path));
|
|
1087
1087
|
const label = !rel || rel.startsWith("..") || isAbsolute(rel) ? path : rel;
|
|
1088
1088
|
return label.split(sep).join("/").replaceAll("\\", "/");
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
-
/** 표의 자리 글자. 뜻은 파이썬 SheetRow.place
|
|
1091
|
+
/** 표의 자리 글자. 뜻은 파이썬 SheetRow.place가 소유한다. @param {import("../report/sheet.js").SheetRow} row */
|
|
1092
1092
|
function placeOf(row) {
|
|
1093
1093
|
return row.column > 0 ? `${row.file}:${row.line}:${row.column}` : `${row.file}:${row.line}`;
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
|
-
/** 표의 고침을 파일에 쓴다. 뜻은 파이썬 applySheet
|
|
1096
|
+
/** 표의 고침을 파일에 쓴다. 뜻은 파이썬 applySheet가 소유한다. @param {string} sheetPath @param {boolean} dryRun @returns {[string[], string[]]} */
|
|
1097
1097
|
function applySheet(sheetPath, dryRun) {
|
|
1098
1098
|
const parsed = parseSheet(readFileSync(sheetPath, "utf-8"));
|
|
1099
1099
|
/** @type {string[]} */
|
|
@@ -1111,7 +1111,7 @@ function applySheet(sheetPath, dryRun) {
|
|
|
1111
1111
|
failed.push(...rows.map((row) => `${placeOf(row)}: 파일이 없다`));
|
|
1112
1112
|
continue;
|
|
1113
1113
|
}
|
|
1114
|
-
// 되돌려 쓰는 뜻은 report/sheet.js
|
|
1114
|
+
// 되돌려 쓰는 뜻은 report/sheet.js의 applyRows가 소유한다 (브라우저의 저장소 쓰기와 같은 함수).
|
|
1115
1115
|
const [rewritten, done, problems] = applyRows(readFileSync(file, "utf-8"), rows);
|
|
1116
1116
|
applied.push(...done);
|
|
1117
1117
|
failed.push(...problems);
|
|
@@ -1120,13 +1120,18 @@ function applySheet(sheetPath, dryRun) {
|
|
|
1120
1120
|
return [applied, failed];
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
|
-
/**
|
|
1123
|
+
/** 세 자리마다 쉼표를 찍은 수. 파이썬의 `f"{n:,}"`와 같은 글자다. @param {number} value */
|
|
1124
|
+
function comma(value) {
|
|
1125
|
+
return value.toLocaleString("en-US");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** 색인이 없을 때의 안내. 뜻은 파이썬 cli/commands/usage.py의 missingIndex가 소유한다. @param {string} kind @param {string} root */
|
|
1124
1129
|
function missingIndex(kind, root) {
|
|
1125
1130
|
const kinds = listUsageIndexes(root).map((meta) => String(meta.kind)).join(", ") || "없음";
|
|
1126
|
-
return `${kind} 색인이 없다 (있는 것: ${kinds}). hanlint usage build ${kind} <글
|
|
1131
|
+
return `${kind} 색인이 없다 (있는 것: ${kinds}). hanlint usage build ${kind} <글 폴더>로 만든다 (txt, md, jsonl. 하위 폴더 포함)`;
|
|
1127
1132
|
}
|
|
1128
1133
|
|
|
1129
|
-
/** 함께 쓰는 말
|
|
1134
|
+
/** 함께 쓰는 말 단어 수 상한. 뜻은 파이썬 QUERY_WORDS가 소유한다. */
|
|
1130
1135
|
const USAGE_QUERY_WORDS = 3;
|
|
1131
1136
|
|
|
1132
1137
|
/** @param {[string, number][]} pairs */
|
|
@@ -1135,8 +1140,8 @@ function pairText(pairs) {
|
|
|
1135
1140
|
}
|
|
1136
1141
|
|
|
1137
1142
|
/**
|
|
1138
|
-
* `hanlint usage "
|
|
1139
|
-
* cli/commands/usage.py
|
|
1143
|
+
* `hanlint usage "단어 …" --kind report --limit 5`, `hanlint usage build <종류> <글 폴더>`, `hanlint usage kinds`. 뜻은 파이썬
|
|
1144
|
+
* cli/commands/usage.py가 소유한다.
|
|
1140
1145
|
* @param {string[]} args
|
|
1141
1146
|
*/
|
|
1142
1147
|
function runUsage(args) {
|
|
@@ -1144,7 +1149,7 @@ function runUsage(args) {
|
|
|
1144
1149
|
const output = /** @type {string | undefined} */ (options["--output"]);
|
|
1145
1150
|
const root = /** @type {string} */ (options["--root"] ?? defaultUsageRoot());
|
|
1146
1151
|
const format = choose(/** @type {string} */ (options["--format"] ?? "text"), ["text", "json"], "--format");
|
|
1147
|
-
if (!positionals.length) throw new UsageError('hanlint usage "
|
|
1152
|
+
if (!positionals.length) throw new UsageError('hanlint usage "단어 …" 또는 hanlint usage build <종류> <글 폴더>');
|
|
1148
1153
|
if (positionals[0] === "kinds") {
|
|
1149
1154
|
const found = listUsageIndexes(root);
|
|
1150
1155
|
if (format === "json") {
|
|
@@ -1152,11 +1157,11 @@ function runUsage(args) {
|
|
|
1152
1157
|
return 0;
|
|
1153
1158
|
}
|
|
1154
1159
|
if (!found.length) {
|
|
1155
|
-
emit(`${root}
|
|
1160
|
+
emit(`${root}에 색인이 없다. hanlint usage build <종류> <글 폴더>로 만든다`, output);
|
|
1156
1161
|
return 0;
|
|
1157
1162
|
}
|
|
1158
1163
|
const lines = [`색인 ${found.length}개 (${root})`];
|
|
1159
|
-
for (const meta of found) lines.push(` ${meta.kind}: 문서 ${meta.documents}편, 문장 ${meta.sentences}개,
|
|
1164
|
+
for (const meta of found) lines.push(` ${meta.kind}: 문서 ${comma(meta.documents)}편, 문장 ${comma(meta.sentences)}개, 단어 ${comma(meta.terms)}종`);
|
|
1160
1165
|
emit(lines.join("\n"), output);
|
|
1161
1166
|
return 0;
|
|
1162
1167
|
}
|
|
@@ -1167,16 +1172,16 @@ function runUsage(args) {
|
|
|
1167
1172
|
}
|
|
1168
1173
|
const [, kind, folder] = positionals;
|
|
1169
1174
|
if (!existsSync(folder) || !statSync(folder).isDirectory()) {
|
|
1170
|
-
emit(
|
|
1175
|
+
emit(`폴더가 아니다: ${folder}`, output);
|
|
1171
1176
|
return 2;
|
|
1172
1177
|
}
|
|
1173
1178
|
const result = buildUsageIndex(kind, readUsageDocuments(folder), root);
|
|
1174
|
-
emit(`${join(root, kind)}: 문서 ${result.documents}편, 문장 ${result.sentences}개,
|
|
1179
|
+
emit(`${join(root, kind)}: 문서 ${comma(result.documents)}편, 문장 ${comma(result.sentences)}개, 단어 ${comma(result.terms)}종. hanlint usage "단어" --kind ${kind}로 찾는다`, output);
|
|
1175
1180
|
return 0;
|
|
1176
1181
|
}
|
|
1177
1182
|
const kind = /** @type {string} */ (options["--kind"] ?? USAGE_KINDS[0]);
|
|
1178
1183
|
const limit = Math.max(Number(options["--limit"] ?? 5), 0);
|
|
1179
|
-
if (!Number.isInteger(limit)) throw new UsageError("--limit
|
|
1184
|
+
if (!Number.isInteger(limit)) throw new UsageError("--limit은 정수다");
|
|
1180
1185
|
const query = positionals.join(" ");
|
|
1181
1186
|
const index = loadUsageIndex(kind, root);
|
|
1182
1187
|
if (!index) {
|
|
@@ -1201,7 +1206,7 @@ function runUsage(args) {
|
|
|
1201
1206
|
if (pairs.length) lines.push(` ${label}: ${pairText(/** @type {[string, number][]} */ (pairs))}`);
|
|
1202
1207
|
}
|
|
1203
1208
|
}
|
|
1204
|
-
if (!hits.length && limit > 0) lines.push("쓰인 문장이 없다.
|
|
1209
|
+
if (!hits.length && limit > 0) lines.push("쓰인 문장이 없다. 단어를 줄이거나 다른 단어로 찾는다");
|
|
1205
1210
|
hits.forEach((hit, index) => {
|
|
1206
1211
|
lines.push(`${index + 1}. ${hit.text}`);
|
|
1207
1212
|
lines.push(` 문서 ${hit.documents}편, 출처 ${hit.source}`);
|
|
@@ -1210,7 +1215,7 @@ function runUsage(args) {
|
|
|
1210
1215
|
return 0;
|
|
1211
1216
|
}
|
|
1212
1217
|
|
|
1213
|
-
/** `hanlint sheet 폴더/ --preset screen
|
|
1218
|
+
/** `hanlint sheet 폴더/ --preset screen`과 `hanlint sheet apply 시트.md`. 뜻은 파이썬 cli/commands/sheet.py가 소유한다. @param {string[]} args */
|
|
1214
1219
|
function runSheet(args) {
|
|
1215
1220
|
const { options, positionals } = parseArgs(args);
|
|
1216
1221
|
const output = /** @type {string | undefined} */ (options["--output"]);
|
|
@@ -1231,13 +1236,13 @@ function runSheet(args) {
|
|
|
1231
1236
|
/** @type {string[]} */
|
|
1232
1237
|
const files = [];
|
|
1233
1238
|
for (const target of positionals) {
|
|
1234
|
-
if (!existsSync(target)) throw new Error(
|
|
1239
|
+
if (!existsSync(target)) throw new Error(`없는 파일: ${target}`);
|
|
1235
1240
|
if (statSync(target).isDirectory()) files.push(...sourceUnder(target));
|
|
1236
1241
|
else files.push(target);
|
|
1237
1242
|
}
|
|
1238
1243
|
const config = configFrom(options, files);
|
|
1239
1244
|
const format = choose(/** @type {string} */ (options["--format"] ?? "markdown"), ["markdown", "json"], "--format");
|
|
1240
|
-
// 디스크의 파일을 읽어 report.sheetRows
|
|
1245
|
+
// 디스크의 파일을 읽어 report.sheetRows에 넘긴다. 행의 뜻은 그쪽이 소유한다.
|
|
1241
1246
|
const rows = sheetRows(files.map((file) => ({ label: relativeLabel(file), source: readFileSync(file, "utf-8") })), config, Boolean(options["--all"]));
|
|
1242
1247
|
emit(format === "json" ? renderSheetJson(rows, config.preset, files.length) : renderSheet(rows, config.preset, files.length), output);
|
|
1243
1248
|
return 0;
|
|
@@ -1258,8 +1263,14 @@ function dispatch(argv) {
|
|
|
1258
1263
|
process.stdout.write(`hanlint ${version}\n`);
|
|
1259
1264
|
return 0;
|
|
1260
1265
|
}
|
|
1266
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
1267
|
+
// npm에는 명령마다의 도움말이 없다. 도움말을 찾은 사람에게 '모르는 옵션' 대신 전체 도움말을 보인다
|
|
1268
|
+
process.stdout.write(`${USAGE}
|
|
1269
|
+
`);
|
|
1270
|
+
return 0;
|
|
1271
|
+
}
|
|
1261
1272
|
if (PYTHON_ONLY.includes(command)) {
|
|
1262
|
-
throw new Error(`${command}
|
|
1273
|
+
throw new Error(`${command} 명령은 파이썬 hanlint에만 있다(pip install hanlint). npm hanlint의 명령: ${COMMANDS.join(", ")}`);
|
|
1263
1274
|
}
|
|
1264
1275
|
if (command === "lint") return runLint(rest);
|
|
1265
1276
|
if (command === "contract") return runContract(rest);
|
|
@@ -1291,7 +1302,7 @@ export function main(argv) {
|
|
|
1291
1302
|
}
|
|
1292
1303
|
const failure = /** @type {NodeJS.ErrnoException} */ (error);
|
|
1293
1304
|
if (failure.code === "ENOENT") {
|
|
1294
|
-
process.stderr.write(
|
|
1305
|
+
process.stderr.write(`찾지 못한 파일: ${failure.path}. 경로를 확인하거나 hanlint --help를 본다\n`);
|
|
1295
1306
|
return 2;
|
|
1296
1307
|
}
|
|
1297
1308
|
process.stderr.write(`${failure.message}\n`);
|