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.
- package/README.md +23 -20
- package/data/calqueFrame.json +60 -0
- package/data/cliches.json +39 -29
- package/data/collocationStops.txt +28 -0
- package/data/confusable.json +20 -20
- package/data/easyWords.json +12 -246
- package/data/exemplars.json +50 -29
- package/data/hiddenDeps.txt +11 -11
- package/data/japaneseLoan.json +27 -27
- package/data/koreanProse.txt +64 -0
- package/data/patterns.json +26 -26
- package/data/phraseRepeat.json +11 -0
- package/data/platformApis.txt +11 -11
- package/data/redundantPair.json +16 -16
- package/data/ruleCategories.json +2 -0
- package/data/ruleDocs.json +56 -54
- package/data/ruleMechanisms.json +2 -0
- package/data/screenNarration.json +10 -10
- package/data/screenSentence.json +60 -12
- package/data/screenTone.json +6 -6
- package/data/screenWord.json +70 -110
- package/data/spacing.json +12 -12
- package/data/spelling.json +29 -29
- package/data/translationese.json +23 -24
- package/data/usageCounts.report.json +1676 -0
- 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 +3 -3
- package/src/analysis/josa.js +1 -1
- package/src/analysis/splitSentences.js +1 -1
- package/src/analysis/tokenize.js +41 -26
- package/src/baseline/store.js +8 -8
- package/src/cli/help.js +11 -10
- package/src/cli/main.js +332 -182
- package/src/cli/walk.js +2 -2
- 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 +44 -25
- 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 +6 -6
- package/src/data/prose.js +27 -0
- 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 +117 -19
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +18 -9
- package/src/fingerprint/codeBlocks.js +3 -3
- package/src/fingerprint/codeMarkers.js +4 -4
- package/src/fingerprint/dictionaries.js +34 -7
- package/src/fingerprint/markers.js +27 -5
- 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 +9 -7
- 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 +7 -3
- package/src/report/fingerprintJson.js +2 -2
- package/src/report/githubReport.js +1 -1
- package/src/report/jsonReport.js +3 -3
- package/src/report/registerMatch.js +2 -2
- package/src/report/sheet.js +0 -0
- package/src/report/textReport.js +35 -16
- package/src/report/writingSpec.js +17 -17
- 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 +30 -26
- package/src/rules/screen/screenWord.js +2 -1
- package/src/rules/sentence/calqueFrame.js +11 -0
- package/src/rules/sentence/cliche.js +7 -3
- package/src/rules/sentence/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/hardWord.js +6 -3
- package/src/rules/sentence/imperativePeriod.js +1 -1
- package/src/rules/sentence/japaneseLoan.js +7 -3
- package/src/rules/sentence/longSentence.js +1 -1
- package/src/rules/sentence/negationRedefine.js +1 -1
- package/src/rules/sentence/nounPile.js +10 -4
- package/src/rules/sentence/numberOrphan.js +1 -1
- package/src/rules/sentence/outsideProfile.js +8 -3
- package/src/rules/sentence/phraseRepeat.js +54 -0
- package/src/rules/sentence/redundantPair.js +7 -3
- package/src/rules/sentence/translationese.js +7 -3
- package/src/rules/shared/candidates.js +2 -2
- package/src/rules/shared/dictionaryRule.js +46 -8
- 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 +54 -0
- package/src/usage/index.js +8 -0
- package/src/usage/sentences.js +681 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { matchedTexts } from "../../fingerprint/markers.js";
|
|
3
4
|
import { SENTENCE, finding } from "../finding.js";
|
|
4
5
|
|
|
@@ -20,6 +21,6 @@ export function run(doc, config) {
|
|
|
20
21
|
if (breaks.length < 2) return [];
|
|
21
22
|
const first = breaks[0][0];
|
|
22
23
|
return breaks.slice(1).map(([sentence, text]) =>
|
|
23
|
-
finding(name, sentence.line, sentence.text, `\`${text}
|
|
24
|
+
finding(name, sentence.line, sentence.text, `\`${text}\`${fitJosa(text, "로")} 끊는 문장이 ${first.line}번째 줄에 이미 있다. 글 전체에 한 번만 쓴다. 여러 번 쓰면 끝난다고 해 놓고 글이 계속 이어진다`, null, "error", SENTENCE, sentence.index),
|
|
24
25
|
);
|
|
25
26
|
}
|
|
@@ -5,22 +5,26 @@ import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
|
5
5
|
export const name = "fieldEcho";
|
|
6
6
|
export const mechanism = "contrast";
|
|
7
7
|
|
|
8
|
+
/** 글의 끝으로 보는 절의 수. 결말 뒤에 `더 해 볼 것` 목록이 붙는 경우가 흔하다. 파이썬과 같다. */
|
|
9
|
+
const ENDING_SECTIONS = 2;
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
-
* @param {import("../../fingerprint/build.js").SectionPrint}
|
|
13
|
+
* @param {import("../../fingerprint/build.js").SectionPrint[]} sections
|
|
11
14
|
* @param {string[]} fields
|
|
12
15
|
* @param {string} where
|
|
13
16
|
*/
|
|
14
|
-
function check(doc,
|
|
17
|
+
function check(doc, sections, fields, where) {
|
|
15
18
|
const findings = [];
|
|
19
|
+
const section = sections[0];
|
|
16
20
|
for (const name_ of fields) {
|
|
17
21
|
const value = doc.frontmatter[name_];
|
|
18
22
|
if (!value) continue;
|
|
19
23
|
const promised = topicsOf(value);
|
|
20
24
|
if (!promised.size) continue;
|
|
21
|
-
if ([...promised].some((word) =>
|
|
25
|
+
if ([...promised].some((word) => sections.some((spoken) => spoken.topics.has(word)))) continue;
|
|
22
26
|
findings.push(
|
|
23
|
-
finding(name, section.startLine, value, `frontmatter
|
|
27
|
+
finding(name, section.startLine, value, `frontmatter의 \`${name_}\` 필드가 약속한 말이 ${where}에 하나도 없다. 약속한 것을 ${where}에서 그 말로 답한다`, null, NOTICE, DOCUMENT, section.index),
|
|
24
28
|
);
|
|
25
29
|
}
|
|
26
30
|
return findings;
|
|
@@ -32,7 +36,7 @@ function check(doc, section, fields, where) {
|
|
|
32
36
|
*/
|
|
33
37
|
export function run(doc, config) {
|
|
34
38
|
if (!config.introFields.length && !config.endingFields.length) return [];
|
|
35
|
-
const findings = check(doc, doc.intro, config.introFields, "도입");
|
|
36
|
-
if (doc.sections.length > 1) findings.push(...check(doc, doc.sections
|
|
39
|
+
const findings = check(doc, [doc.intro], config.introFields, "도입");
|
|
40
|
+
if (doc.sections.length > 1) findings.push(...check(doc, doc.sections.slice(-ENDING_SECTIONS), config.endingFields, "글의 끝"));
|
|
37
41
|
return findings;
|
|
38
42
|
}
|
|
@@ -27,6 +27,6 @@ export function run(doc, config) {
|
|
|
27
27
|
.filter((word) => word.length >= MIN_WORD && promised.includes(word) && !joined.includes(word));
|
|
28
28
|
if (!missing.length) return [];
|
|
29
29
|
return [
|
|
30
|
-
finding(name, headings[0][2], joined, `글이 내건 \`${missing[0]}
|
|
30
|
+
finding(name, headings[0][2], joined, `글이 내건 말이 절 제목 어디에도 없다: \`${missing[0]}\`. 목차를 훑는 독자는 이 글이 그것을 다루는지 알 수 없다`, null, NOTICE, DOCUMENT, -1),
|
|
31
31
|
];
|
|
32
32
|
}
|
|
@@ -17,6 +17,6 @@ export function run(doc, config) {
|
|
|
17
17
|
const head = `${title}\n${first ? first.text : ""}`;
|
|
18
18
|
if (head.includes(keyword)) return [];
|
|
19
19
|
return [
|
|
20
|
-
finding(name, first ? first.startLine : 1, title || (first ? first.text : ""), `대표
|
|
20
|
+
finding(name, first ? first.startLine : 1, title || (first ? first.text : ""), `대표 검색어가 제목에도 첫 문단에도 없다: \`${keyword}\`. 검색해 들어온 독자는 자기가 친 이름을 보지 못한다`, null, "error", DOCUMENT, -1),
|
|
21
21
|
];
|
|
22
22
|
}
|
|
@@ -9,7 +9,7 @@ export function run(doc, config) {
|
|
|
9
9
|
if (doc.bodySections.length < 2 || doc.questionCount > 0) return [];
|
|
10
10
|
const first = doc.sentences.length ? doc.sentences[0] : null;
|
|
11
11
|
const why = doc.readerCallCount === 0
|
|
12
|
-
? "물음표가 한 번도 없고
|
|
13
|
-
: "물음표가 한 번도 없다. 질문이 필요한 글인지
|
|
12
|
+
? "물음표가 한 번도 없고 독자에게 말을 거는 문장도 없다. 질문이나 독자에게 말을 거는 문장이 필요한 글인지 판단하고, 설명만으로 충분하면 그대로 둔다"
|
|
13
|
+
: "물음표가 한 번도 없다. 질문이 필요한 글인지 판단하고, 설명만으로 충분하면 그대로 둔다";
|
|
14
14
|
return [finding(name, first ? first.line : 1, first ? first.text : "", why, null, config.enforceStyle.includes(name) ? "error" : "notice", DOCUMENT, -1)];
|
|
15
15
|
}
|
|
@@ -9,6 +9,6 @@ export function run(doc) {
|
|
|
9
9
|
const reader = doc.reader.final;
|
|
10
10
|
if (!reader.promises.length || reader.recalls.length) return [];
|
|
11
11
|
return reader.promises.map(([line, text]) =>
|
|
12
|
-
finding(name, line, text, "뒤로 미룬
|
|
12
|
+
finding(name, line, text, "뒤로 미룬 이야기를 `앞에서 미룬 X`처럼 다시 언급하는 문장이 글 어디에도 없다. 뒤에서 다루거나 처음부터 미루지 않는다", null, NOTICE, DOCUMENT, -1),
|
|
13
13
|
);
|
|
14
14
|
}
|
|
@@ -53,7 +53,7 @@ export function run(doc, config) {
|
|
|
53
53
|
const unit = [...kinds][0];
|
|
54
54
|
const [offset, cell] = odd[0];
|
|
55
55
|
findings.push(
|
|
56
|
-
finding(name, block.startLine + offset, cell, `이 열의
|
|
56
|
+
finding(name, block.startLine + offset, cell, `이 열의 다른 ${values.length - 1}칸은 모두 \`${unit}\` 단위인데 이 칸만 다르다. 같은 단위로 다시 재거나 그 줄을 본문 문장으로 옮긴다`, null, NOTICE, DOCUMENT, block.index),
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
}
|
package/src/rules/finding.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 지적 하나의
|
|
2
|
+
/** 지적 하나의 형식. 파이썬 rules/finding.py와 필드도 JSON 순서도 같다. */
|
|
3
3
|
|
|
4
4
|
export const ERROR = "error";
|
|
5
5
|
export const NOTICE = "notice";
|
|
@@ -18,10 +18,10 @@ const MIN_LOCAL_CUE = 2;
|
|
|
18
18
|
* @property {string} quote
|
|
19
19
|
* @property {string} why
|
|
20
20
|
* @property {string | null} fix 기계가 낼 수 있을 때만
|
|
21
|
-
* @property {string} severity error
|
|
21
|
+
* @property {string} severity error는 규칙 위반, notice는 확인이 필요한 것
|
|
22
22
|
* @property {string} scope sentence, paragraph, section, document
|
|
23
|
-
* @property {number} at scope
|
|
24
|
-
* @property {string | null} fragment 원문에서 바꿀 조각. hanlint fix
|
|
23
|
+
* @property {number} at scope가 가리키는 분석 결과의 index
|
|
24
|
+
* @property {string | null} fragment 원문에서 바꿀 조각. hanlint fix가 replacement로 바꾼다
|
|
25
25
|
* @property {string | null} replacement
|
|
26
26
|
* @property {Candidate[]} candidates 뜻을 정하지 않고 형태로 좁힐 수 있을 때만. 순위와 점수는 없다
|
|
27
27
|
*/
|
|
@@ -20,7 +20,7 @@ export function run(doc, config) {
|
|
|
20
20
|
if (paragraph.sentenceCount < config.factListMinSentences || isLinked(paragraph)) continue;
|
|
21
21
|
if (paragraph.meanLength <= config.factListMaxMeanLength) {
|
|
22
22
|
findings.push(
|
|
23
|
-
finding(name, paragraph.startLine, paragraph.sentences[0].text, `문장 ${paragraph.sentenceCount}개에 인과
|
|
23
|
+
finding(name, paragraph.startLine, paragraph.sentences[0].text, `문장 ${paragraph.sentenceCount}개에 인과 접속사가 하나도 없다. 사실만 나열한 문단일 수 있다. \`그래서\`, \`때문에\`로 잇는다`, null, NOTICE, PARAGRAPH, paragraph.index),
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -31,7 +31,7 @@ export function run(doc, config) {
|
|
|
31
31
|
for (const [start] of runsOf(keys, config.fragmentRun)) {
|
|
32
32
|
const first = paragraphs[start];
|
|
33
33
|
findings.push(
|
|
34
|
-
finding(name, first.startLine, first.sentences.length ? first.sentences[0].text : "", `${config.fragmentRun}개 문단이 연달아 한두 문장씩이다. 화제가 같으면 한 문단으로 묶는다. 줄바꿈은 화제가 바뀌는
|
|
34
|
+
finding(name, first.startLine, first.sentences.length ? first.sentences[0].text : "", `${config.fragmentRun}개 문단이 연달아 한두 문장씩이다. 화제가 같으면 한 문단으로 묶는다. 줄바꿈은 화제가 바뀌는 곳에만 둔다`, null, "error", PARAGRAPH, first.index),
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
}
|
package/src/rules/readerDebt.js
CHANGED
package/src/rules/registry.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 규칙
|
|
4
|
-
* 여기는 목록으로 든다. test/rules.test.js
|
|
5
|
-
*
|
|
3
|
+
* 규칙 목록. 규칙 하나는 파일 하나이고 `name`과 `run(doc, config)`을 내보낸다. 파이썬은 폴더를 걸어 찾고
|
|
4
|
+
* 여기는 목록으로 든다. test/rules.test.js가 목록과 폴더와 ruleDocs.json이 같은지 본다.
|
|
5
|
+
* 규칙 설명(왜, 어디서, 고치기, 안 잡는 것)은 파이썬 docstring을 옮긴 data/ruleDocs.json에서 읽는다.
|
|
6
6
|
*/
|
|
7
7
|
import { enabled } from "../config/settings.js";
|
|
8
8
|
import { loadRuleCategories, loadRuleDocs } from "../data/load.js";
|
|
@@ -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";
|
|
@@ -64,14 +66,16 @@ import * as screenWord from "./screen/screenWord.js";
|
|
|
64
66
|
/**
|
|
65
67
|
* @typedef {object} Rule
|
|
66
68
|
* @property {string} name
|
|
67
|
-
* @property {string} mechanism 세는 방법. MECHANISMS
|
|
69
|
+
* @property {string} mechanism 세는 방법. MECHANISMS의 키 하나
|
|
68
70
|
* @property {(doc: import("../fingerprint/build.js").DocumentPrint, config: import("../config/settings.js").Config) => import("./finding.js").Finding[]} run
|
|
69
71
|
*/
|
|
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,
|
|
@@ -127,19 +131,19 @@ export const RULES = [
|
|
|
127
131
|
];
|
|
128
132
|
|
|
129
133
|
/**
|
|
130
|
-
* 규칙이
|
|
134
|
+
* 규칙이 방식의 닫힌 목록. 파이썬 rules/registry.py의 MECHANISMS와 같은 값과 같은 순서다.
|
|
131
135
|
* @type {Record<string, string>}
|
|
132
136
|
*/
|
|
133
137
|
export const MECHANISMS = {
|
|
134
|
-
dictionary: "사전.
|
|
135
|
-
repeat: "반복. 같은 모양이
|
|
136
|
-
threshold: "
|
|
137
|
-
contrast: "
|
|
138
|
-
reader: "
|
|
138
|
+
dictionary: "사전. 단어 목록과 정규식이 맞는 곳",
|
|
139
|
+
repeat: "반복. 같은 모양이 가까이에서 여러 번 나오는 곳",
|
|
140
|
+
threshold: "수치. 글에서 잰 값이 기준값을 넘거나 정해 둔 형식과 다른 곳",
|
|
141
|
+
contrast: "비교. 두 곳을 비교해 어긋난 곳",
|
|
142
|
+
reader: "독자. 앞에서 읽은 내용만으로 그 문장을 이해할 수 있는지 문장 순서대로 본다",
|
|
139
143
|
};
|
|
140
144
|
for (const rule of RULES) {
|
|
141
145
|
if (!(rule.mechanism in MECHANISMS)) {
|
|
142
|
-
throw new Error(`규칙 ${rule.name}
|
|
146
|
+
throw new Error(`규칙 ${rule.name}의 방식이 닫힌 집합 밖이다: ${rule.mechanism} (있는 것: ${Object.keys(MECHANISMS).join(", ")}). 새 방법은 규칙 하나 때문에 더하지 않는다. 멈추고 묻는다`);
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -153,7 +157,7 @@ export function ruleNames() {
|
|
|
153
157
|
/** @param {string} name */
|
|
154
158
|
export function ruleDoc(name) {
|
|
155
159
|
const docs = loadRuleDocs();
|
|
156
|
-
if (!(name in docs) || !BY_NAME.has(name)) throw new Error(`모르는 규칙: ${name}. hanlint rules
|
|
160
|
+
if (!(name in docs) || !BY_NAME.has(name)) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
|
|
157
161
|
return docs[name];
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -162,10 +166,10 @@ export function ruleSummary(name) {
|
|
|
162
166
|
return ruleDoc(name).split("\n")[0];
|
|
163
167
|
}
|
|
164
168
|
|
|
165
|
-
/** 기술서의 네 절 표. 파이썬 rules/registry.py
|
|
169
|
+
/** 기술서의 네 절 표. 파이썬 rules/registry.py의 REQUIRED_SECTIONS와 같다. */
|
|
166
170
|
export const REQUIRED_SECTIONS = ["왜:", "어디서:", "고치기:", "안 잡는 것:"];
|
|
167
171
|
|
|
168
|
-
/** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection
|
|
172
|
+
/** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection과 같다. @param {string} doc @param {string} label */
|
|
169
173
|
export function docSection(doc, label) {
|
|
170
174
|
const collected = [];
|
|
171
175
|
let inside = false;
|
|
@@ -184,41 +188,41 @@ export function docSection(doc, label) {
|
|
|
184
188
|
return collected.filter(Boolean).join(" ");
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
/** 규칙의 고치기 절 한 줄. primer
|
|
191
|
+
/** 규칙의 고치기 절 한 줄. primer가 쓰기 전에 읽히는 그 문장이다. @param {string} name */
|
|
188
192
|
export function ruleFix(name) {
|
|
189
193
|
return docSection(ruleDoc(name), "고치기:");
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
/**
|
|
193
|
-
*
|
|
197
|
+
* 묶음의 사람 이름. 파이썬 rules/registry.py의 CATEGORY_TITLES와 같은 값과 같은 순서다.
|
|
194
198
|
* @type {Record<string, string>}
|
|
195
199
|
*/
|
|
196
200
|
export const CATEGORY_TITLES = {
|
|
197
|
-
sentence: "문장
|
|
198
|
-
paragraph: "문단
|
|
199
|
-
structure: "글의
|
|
200
|
-
document: "
|
|
201
|
+
sentence: "문장",
|
|
202
|
+
paragraph: "문단",
|
|
203
|
+
structure: "글의 짜임",
|
|
204
|
+
document: "문서",
|
|
201
205
|
orthography: "표기와 띄어쓰기",
|
|
202
|
-
code: "코드 블록
|
|
203
|
-
screen: "화면의
|
|
206
|
+
code: "코드 블록",
|
|
207
|
+
screen: "화면의 글",
|
|
204
208
|
};
|
|
205
209
|
|
|
206
210
|
/** 규칙이 세는 방법. @param {string} name */
|
|
207
211
|
export function ruleMechanism(name) {
|
|
208
212
|
const rule = BY_NAME.get(name);
|
|
209
|
-
if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules
|
|
213
|
+
if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
|
|
210
214
|
return rule.mechanism;
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
/** 규칙이 사는
|
|
217
|
+
/** 규칙이 사는 묶음. 정본은 파이썬 쪽 폴더이고 여기는 그 투영을 읽는다. @param {string} name */
|
|
214
218
|
export function ruleCategory(name) {
|
|
215
219
|
const categories = loadRuleCategories();
|
|
216
|
-
if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules
|
|
220
|
+
if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
|
|
217
221
|
return categories[name];
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
/**
|
|
221
|
-
* 인라인 제어가 그 줄에서 그 규칙을 껐는가.
|
|
225
|
+
* 인라인 제어가 그 줄에서 그 규칙을 껐는가. `*`는 전부다.
|
|
222
226
|
* @param {import("./finding.js").Finding} f
|
|
223
227
|
* @param {[string, number, number][]} disabled
|
|
224
228
|
*/
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
3
4
|
|
|
4
5
|
export const name = "screenWord";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
8
|
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
9
|
export function run(doc) {
|
|
9
|
-
return overridingFindings(doc, "screenWord", name);
|
|
10
|
+
return overridingFindings(doc, "screenWord", name, undefined, NOTICE);
|
|
10
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "calqueFrame";
|
|
6
|
+
export const mechanism = "dictionary";
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
return overridingFindings(doc, "calqueFrame", name, undefined, NOTICE);
|
|
11
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "cliche";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "cliches", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { SENTENCE, finding } from "../finding.js";
|
|
3
4
|
import { runsOf } from "../shared/repeat.js";
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ export function run(doc) {
|
|
|
15
16
|
for (const [start, length, connector] of runsOf(keys, 2)) {
|
|
16
17
|
for (const sentence of sentences.slice(start + 1, start + length)) {
|
|
17
18
|
findings.push(
|
|
18
|
-
finding(name, sentence.line, sentence.text, `\`${connector}
|
|
19
|
+
finding(name, sentence.line, sentence.text, `\`${connector}\`${fitJosa(connector, "로")} 시작하는 문장이 연달아 온다. 앞에서 만든 것의 이름을 불러 잇는다`, null, "error", SENTENCE, sentence.index),
|
|
19
20
|
);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -32,7 +32,7 @@ export function run(doc) {
|
|
|
32
32
|
if (overlap(reader.recent, sentence.topics) > 0) continue;
|
|
33
33
|
if (isPrompt(reader.previous)) continue;
|
|
34
34
|
findings.push(
|
|
35
|
-
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}
|
|
35
|
+
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\`${fitJosa(sentence.deixis[0], "이")} 가리킬 것이 앞 문장에 없다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index, null, null, danglingDeixisCandidates(sentence, reader.previous)),
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
return findings;
|
|
@@ -13,7 +13,7 @@ export function run(doc) {
|
|
|
13
13
|
block.text.split("\n").forEach((lineText, offset) => {
|
|
14
14
|
if (DASHES.test(lineText)) {
|
|
15
15
|
findings.push(
|
|
16
|
-
finding(name, block.startLine + offset, lineText.trim(), "긴 줄표다.
|
|
16
|
+
finding(name, block.startLine + offset, lineText.trim(), "긴 줄표다. 덧붙이는 말은 마침표로 끊거나 괄호에 넣고, 범위는 물결표(~)로 쓴다", null, "error", DOCUMENT, block.index),
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
});
|
|
@@ -16,7 +16,7 @@ export function run(doc) {
|
|
|
16
16
|
const reader = doc.reader.beforeSentence[sentence.index];
|
|
17
17
|
if (overlap(reader.recent, sentence.topics) === 0) continue;
|
|
18
18
|
findings.push(
|
|
19
|
-
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}
|
|
19
|
+
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\`${fitJosa(sentence.deixis[0], "은")} 독자가 스크롤을 되돌려야 하는 지시어다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index),
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
22
|
return findings;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { SENTENCE, finding } from "../finding.js";
|
|
3
4
|
|
|
4
5
|
export const name = "doubleNegative";
|
|
@@ -24,7 +25,7 @@ export function run(doc) {
|
|
|
24
25
|
const start = /** @type {number} */ (match.index);
|
|
25
26
|
const fix = replacement ? sentence.text.slice(0, start) + replacement + sentence.text.slice(start + match[0].length) : null;
|
|
26
27
|
findings.push(
|
|
27
|
-
finding(name, sentence.line, sentence.text, `\`${match[1]}
|
|
28
|
+
finding(name, sentence.line, sentence.text, `\`${match[1]}\`${fitJosa(match[1], "는")} 이중 부정이다. 긍정으로 쓴다`, fix, "error", SENTENCE, sentence.index, replacement ? match[1] : null, replacement ?? null),
|
|
28
29
|
);
|
|
29
30
|
}
|
|
30
31
|
return findings;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { SENTENCE, finding } from "../finding.js";
|
|
3
4
|
import { doublePassiveCandidates } from "../shared/candidates.js";
|
|
4
5
|
|
|
@@ -38,7 +39,7 @@ export function run(doc) {
|
|
|
38
39
|
name,
|
|
39
40
|
sentence.line,
|
|
40
41
|
sentence.text,
|
|
41
|
-
`\`${sentence.passives[0]}
|
|
42
|
+
`\`${sentence.passives[0]}\`${fitJosa(sentence.passives[0], "는")} 피동에 \`-어지다\`를 또 붙인 이중 피동이다. 하나만 남긴다`,
|
|
42
43
|
approved ? candidates[0].text : null,
|
|
43
44
|
"error",
|
|
44
45
|
SENTENCE,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { insideAny, matchedSpans } from "../../fingerprint/markers.js";
|
|
3
4
|
import { SENTENCE, finding } from "../finding.js";
|
|
4
5
|
|
|
@@ -16,7 +17,7 @@ export function run(doc, config) {
|
|
|
16
17
|
for (const [start, end, text] of matchedSpans(sentence.text, "draftHistory.txt")) {
|
|
17
18
|
if (insideAny(start, end, sentence.quoted)) continue;
|
|
18
19
|
findings.push(
|
|
19
|
-
finding(name, sentence.line, sentence.text, `\`${text}
|
|
20
|
+
finding(name, sentence.line, sentence.text, `\`${text}\`${fitJosa(text, "는")} 글쓴이의 과정이다. 독자는 초고를 본 적이 없다. 알아낸 사실만 결과 문장으로 남긴다`, null, "error", SENTENCE, sentence.index),
|
|
20
21
|
);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { NOTICE, SENTENCE, finding } from "../finding.js";
|
|
3
4
|
import { runsOf } from "../shared/repeat.js";
|
|
4
5
|
|
|
@@ -16,12 +17,12 @@ export function run(doc, config) {
|
|
|
16
17
|
const sentences = section.paragraphs.flatMap((p) => p.sentences);
|
|
17
18
|
const endings = sentences.map((s) => (COUNTED_ENDINGS.has(s.ending) ? s.ending : `__${s.index}`));
|
|
18
19
|
for (const [start, length, ending] of runsOf(endings, config.endingRun)) {
|
|
19
|
-
// 구간 어딘가에
|
|
20
|
+
// 구간 어딘가에 인과 접속사나 독자에게 말을 거는 문장이 있으면 리듬이 산 것이다. 구간을 쪼개면
|
|
20
21
|
// 조각마다 지적이 되어 오히려 늘어난다. 구간 전체를 하나로 보고 살린다.
|
|
21
22
|
if (sentences.slice(start, start + length).some((s) => s.causal || s.readerCall)) continue;
|
|
22
23
|
const first = sentences[start];
|
|
23
24
|
findings.push(
|
|
24
|
-
finding(name, first.line, first.text, `\`${ending}
|
|
25
|
+
finding(name, first.line, first.text, `\`${ending}\`${fitJosa(ending, "로")} 끝나는 문장 ${length}개에 인과 접속사도 질문도 독자를 부르는 말도 없다. 질문, 인과 접속사, 행동 동사로 리듬을 바꾼다`, null, NOTICE, SENTENCE, first.index),
|
|
25
26
|
);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -11,7 +11,7 @@ export function run(doc) {
|
|
|
11
11
|
for (const sentence of doc.sentences) {
|
|
12
12
|
if (sentence.euiCount >= 3 || (sentence.euiCount >= 2 && euiAdjacent(sentence.text))) {
|
|
13
13
|
findings.push(
|
|
14
|
-
finding(name, sentence.line, sentence.text, `한 문장에
|
|
14
|
+
finding(name, sentence.line, sentence.text, `한 문장에 \`의\`가 ${sentence.euiCount}번이다. 명사를 쌓은 문장이라 관계가 표시되지 않는다. 동사로 편다`, null, "error", SENTENCE, sentence.index),
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -12,7 +12,7 @@ export function run(doc) {
|
|
|
12
12
|
const match = FILLER.exec(sentence.text);
|
|
13
13
|
if (!match) continue;
|
|
14
14
|
findings.push(
|
|
15
|
-
finding(name, sentence.line, sentence.text, `\`${match[1]}
|
|
15
|
+
finding(name, sentence.line, sentence.text, `\`${match[1]}\`만 붙여 잇는 문장이다. 앞에서 만든 파일이나 값을 이름으로 다시 언급하고 다음 행동을 붙인다`, null, "error", SENTENCE, sentence.index),
|
|
16
16
|
);
|
|
17
17
|
}
|
|
18
18
|
return findings;
|
|
@@ -5,7 +5,10 @@ import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
|
5
5
|
export const name = "hardWord";
|
|
6
6
|
export const mechanism = "dictionary";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "easyWords", name, NOTICE, config);
|
|
11
14
|
}
|
|
@@ -14,7 +14,7 @@ export function run(doc) {
|
|
|
14
14
|
const start = /** @type {number} */ (match.index);
|
|
15
15
|
const fix = sentence.text.slice(0, start) + match[1] + sentence.text.slice(start + match[0].length);
|
|
16
16
|
findings.push(
|
|
17
|
-
finding(name, sentence.line, sentence.text,
|
|
17
|
+
finding(name, sentence.line, sentence.text, `명령·청유 문장 끝(\`${match[1]}\`)에 마침표를 붙이지 않는 표기 방침이다`, fix, "error", SENTENCE, sentence.index, match[0], match[1]),
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
return findings;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "japaneseLoan";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "japaneseLoan", name, ERROR, config);
|
|
10
14
|
}
|
|
@@ -14,7 +14,7 @@ export function run(doc, config) {
|
|
|
14
14
|
for (const sentence of doc.sentences) {
|
|
15
15
|
if (sentence.length > config.longSentenceMax) {
|
|
16
16
|
findings.push(
|
|
17
|
-
finding(name, sentence.line, sentence.text,
|
|
17
|
+
finding(name, sentence.line, sentence.text, `어절이 ${sentence.length}개로 상한 ${config.longSentenceMax}개를 넘는다. 마침표로 끊거나 나열이면 목록으로 꺼낸다`, null, NOTICE, SENTENCE, sentence.index, null, null, longSentenceCandidates(sentence.text)),
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -24,7 +24,7 @@ export function run(doc) {
|
|
|
24
24
|
const current = sentences[i];
|
|
25
25
|
const following = sentences[i + 1];
|
|
26
26
|
if (NEGATION.test(current.text) && isDefinition(following.text)) {
|
|
27
|
-
findings.push(finding(name, current.line, current.text, "
|
|
27
|
+
findings.push(finding(name, current.line, current.text, "`단순한 X가 아닙니다` 뒤에 `Y입니다`로 다시 정의하는 공식이다. 앞 문장을 지우고 Y를 바로 쓴다", null, "error", SENTENCE, current.index));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { nounRuns } from "../../analysis/index.js";
|
|
3
|
+
import { attested, usageKindOf } from "../../usage/counts.js";
|
|
2
4
|
import { SENTENCE, finding } from "../finding.js";
|
|
3
5
|
|
|
4
6
|
export const name = "nounPile";
|
|
@@ -10,12 +12,16 @@ export const mechanism = "threshold";
|
|
|
10
12
|
*/
|
|
11
13
|
export function run(doc, config) {
|
|
12
14
|
const findings = [];
|
|
15
|
+
const kind = usageKindOf(config);
|
|
13
16
|
for (const sentence of doc.sentences) {
|
|
14
|
-
if (sentence.nounRun
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
17
|
+
if (sentence.nounRun < config.nounPileMin) continue;
|
|
18
|
+
if (kind) {
|
|
19
|
+
const piles = nounRuns(sentence.text).filter(([, length]) => length >= config.nounPileMin).map(([chain]) => chain);
|
|
20
|
+
if (piles.length && piles.every((chain) => attested(chain, kind, config.usageMin))) continue;
|
|
18
21
|
}
|
|
22
|
+
findings.push(
|
|
23
|
+
finding(name, sentence.line, sentence.text, `명사 ${sentence.nounRun}개가 조사 없이 이어진다. 관계가 표시되지 않아 독자가 조사를 끼워 넣는다. 동사로 되돌린다`, null, config.enforceStyle.includes(name) ? "error" : "notice", SENTENCE, sentence.index),
|
|
24
|
+
);
|
|
19
25
|
}
|
|
20
26
|
return findings;
|
|
21
27
|
}
|
|
@@ -37,7 +37,7 @@ export function run(doc, config) {
|
|
|
37
37
|
if (reader.numerals.has(base) || numeralsIn(sentence.text.slice(0, start)).has(base)) continue;
|
|
38
38
|
if (isAnchored(sentence.text, start)) continue;
|
|
39
39
|
findings.push(
|
|
40
|
-
finding(name, sentence.line, sentence.text, `\`${base}
|
|
40
|
+
finding(name, sentence.line, sentence.text, `\`${base}\`${fitJosa(base, "가")} 여기서 처음 나오는 값이다. 무엇에서 올라간 것인지 알 수 없다. 그 값이 어디서 나왔는지 밝힌다`, null, NOTICE, SENTENCE, sentence.index),
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
}
|