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/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";
|
|
@@ -66,7 +66,7 @@ import * as screenWord from "./screen/screenWord.js";
|
|
|
66
66
|
/**
|
|
67
67
|
* @typedef {object} Rule
|
|
68
68
|
* @property {string} name
|
|
69
|
-
* @property {string} mechanism 세는 방법. MECHANISMS
|
|
69
|
+
* @property {string} mechanism 세는 방법. MECHANISMS의 키 하나
|
|
70
70
|
* @property {(doc: import("../fingerprint/build.js").DocumentPrint, config: import("../config/settings.js").Config) => import("./finding.js").Finding[]} run
|
|
71
71
|
*/
|
|
72
72
|
|
|
@@ -131,19 +131,19 @@ export const RULES = [
|
|
|
131
131
|
];
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* 규칙이
|
|
134
|
+
* 규칙이 방식의 닫힌 목록. 파이썬 rules/registry.py의 MECHANISMS와 같은 값과 같은 순서다.
|
|
135
135
|
* @type {Record<string, string>}
|
|
136
136
|
*/
|
|
137
137
|
export const MECHANISMS = {
|
|
138
|
-
dictionary: "사전.
|
|
139
|
-
repeat: "반복. 같은 모양이
|
|
140
|
-
threshold: "
|
|
141
|
-
contrast: "
|
|
142
|
-
reader: "
|
|
138
|
+
dictionary: "사전. 단어 목록과 정규식이 맞는 곳",
|
|
139
|
+
repeat: "반복. 같은 모양이 가까이에서 여러 번 나오는 곳",
|
|
140
|
+
threshold: "수치. 글에서 잰 값이 기준값을 넘거나 정해 둔 형식과 다른 곳",
|
|
141
|
+
contrast: "비교. 두 곳을 비교해 어긋난 곳",
|
|
142
|
+
reader: "독자. 앞에서 읽은 내용만으로 그 문장을 이해할 수 있는지 문장 순서대로 본다",
|
|
143
143
|
};
|
|
144
144
|
for (const rule of RULES) {
|
|
145
145
|
if (!(rule.mechanism in MECHANISMS)) {
|
|
146
|
-
throw new Error(`규칙 ${rule.name}
|
|
146
|
+
throw new Error(`규칙 ${rule.name}의 방식이 닫힌 집합 밖이다: ${rule.mechanism} (있는 것: ${Object.keys(MECHANISMS).join(", ")}). 새 방법은 규칙 하나 때문에 더하지 않는다. 멈추고 묻는다`);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -157,7 +157,7 @@ export function ruleNames() {
|
|
|
157
157
|
/** @param {string} name */
|
|
158
158
|
export function ruleDoc(name) {
|
|
159
159
|
const docs = loadRuleDocs();
|
|
160
|
-
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로 목록을 본다`);
|
|
161
161
|
return docs[name];
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -166,10 +166,10 @@ export function ruleSummary(name) {
|
|
|
166
166
|
return ruleDoc(name).split("\n")[0];
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/** 기술서의 네 절 표. 파이썬 rules/registry.py
|
|
169
|
+
/** 기술서의 네 절 표. 파이썬 rules/registry.py의 REQUIRED_SECTIONS와 같다. */
|
|
170
170
|
export const REQUIRED_SECTIONS = ["왜:", "어디서:", "고치기:", "안 잡는 것:"];
|
|
171
171
|
|
|
172
|
-
/** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection
|
|
172
|
+
/** 기술서에서 절 하나의 본문을 한 줄로. 파이썬 docSection과 같다. @param {string} doc @param {string} label */
|
|
173
173
|
export function docSection(doc, label) {
|
|
174
174
|
const collected = [];
|
|
175
175
|
let inside = false;
|
|
@@ -188,41 +188,41 @@ export function docSection(doc, label) {
|
|
|
188
188
|
return collected.filter(Boolean).join(" ");
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/** 규칙의 고치기 절 한 줄. primer
|
|
191
|
+
/** 규칙의 고치기 절 한 줄. primer가 쓰기 전에 읽히는 그 문장이다. @param {string} name */
|
|
192
192
|
export function ruleFix(name) {
|
|
193
193
|
return docSection(ruleDoc(name), "고치기:");
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
*
|
|
197
|
+
* 묶음의 사람 이름. 파이썬 rules/registry.py의 CATEGORY_TITLES와 같은 값과 같은 순서다.
|
|
198
198
|
* @type {Record<string, string>}
|
|
199
199
|
*/
|
|
200
200
|
export const CATEGORY_TITLES = {
|
|
201
|
-
sentence: "문장
|
|
202
|
-
paragraph: "문단
|
|
203
|
-
structure: "글의
|
|
204
|
-
document: "
|
|
201
|
+
sentence: "문장",
|
|
202
|
+
paragraph: "문단",
|
|
203
|
+
structure: "글의 짜임",
|
|
204
|
+
document: "문서",
|
|
205
205
|
orthography: "표기와 띄어쓰기",
|
|
206
|
-
code: "코드 블록
|
|
207
|
-
screen: "화면의
|
|
206
|
+
code: "코드 블록",
|
|
207
|
+
screen: "화면의 글",
|
|
208
208
|
};
|
|
209
209
|
|
|
210
210
|
/** 규칙이 세는 방법. @param {string} name */
|
|
211
211
|
export function ruleMechanism(name) {
|
|
212
212
|
const rule = BY_NAME.get(name);
|
|
213
|
-
if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules
|
|
213
|
+
if (!rule) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
|
|
214
214
|
return rule.mechanism;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
/** 규칙이 사는
|
|
217
|
+
/** 규칙이 사는 묶음. 정본은 파이썬 쪽 폴더이고 여기는 그 투영을 읽는다. @param {string} name */
|
|
218
218
|
export function ruleCategory(name) {
|
|
219
219
|
const categories = loadRuleCategories();
|
|
220
|
-
if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules
|
|
220
|
+
if (!(name in categories)) throw new Error(`모르는 규칙: ${name}. hanlint rules로 목록을 본다`);
|
|
221
221
|
return categories[name];
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
/**
|
|
225
|
-
* 인라인 제어가 그 줄에서 그 규칙을 껐는가.
|
|
225
|
+
* 인라인 제어가 그 줄에서 그 규칙을 껐는가. `*`는 전부다.
|
|
226
226
|
* @param {import("./finding.js").Finding} f
|
|
227
227
|
* @param {[string, number, number][]} disabled
|
|
228
228
|
*/
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -10,9 +10,14 @@ const FEATURES = [
|
|
|
10
10
|
["length", "문장 길이", "어절"],
|
|
11
11
|
["commas", "쉼표", "개"],
|
|
12
12
|
["newTopics", "처음 나온 화제어", "개"],
|
|
13
|
-
["hedges", "
|
|
13
|
+
["hedges", "얼버무리는 말", "개"],
|
|
14
14
|
];
|
|
15
15
|
|
|
16
|
+
/** 세 자리마다 쉼표를 찍는다. 파이썬 `f"{n:,}"`와 같은 글을 낸다. @param {number} count */
|
|
17
|
+
function grouped(count) {
|
|
18
|
+
return String(count).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
/** @param {import("../../config/settings.js").Config} config */
|
|
17
22
|
function profileFor(config) {
|
|
18
23
|
if (config.profile) return userProfile(config.profile);
|
|
@@ -22,7 +27,7 @@ function profileFor(config) {
|
|
|
22
27
|
|
|
23
28
|
/** @param {number} permille */
|
|
24
29
|
function shareText(permille) {
|
|
25
|
-
return permille === 0 ? "0.1%
|
|
30
|
+
return permille === 0 ? "0.1% 안" : `${Math.floor(permille / 10)}.${permille % 10}%`;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/** @param {Set<string>} topics @param {Set<string>} known */
|
|
@@ -55,7 +60,7 @@ export function run(doc, config) {
|
|
|
55
60
|
name,
|
|
56
61
|
sentence.line,
|
|
57
62
|
sentence.text,
|
|
58
|
-
`${label} ${value}${unit}
|
|
63
|
+
`${label} ${value}${unit}인 문장이다. ${profile.label} ${grouped(profile.sentences)}문장 가운데 상위 ${shareText(shareAtOrAbove(hist, value))}에 든다. ${p}%는 ${limit}${unit} 이하다`,
|
|
59
64
|
null,
|
|
60
65
|
NOTICE,
|
|
61
66
|
SENTENCE,
|
|
@@ -36,12 +36,13 @@ export function run(doc, config) {
|
|
|
36
36
|
if (hit.length < MIN_HITS || hit.length / prose.length <= share) continue;
|
|
37
37
|
const lines = [...new Set(hit.map((one) => one.line))].sort((a, b) => a - b).join(", ");
|
|
38
38
|
const share100 = `${Math.round(share * 100)}%`;
|
|
39
|
+
const cited = source ? ` (${source})` : "";
|
|
39
40
|
findings.push(
|
|
40
41
|
finding(
|
|
41
42
|
name,
|
|
42
43
|
hit[0].line,
|
|
43
44
|
hit[0].text,
|
|
44
|
-
`\`${label}\`
|
|
45
|
+
`\`${label}\` 문형이 산문 ${prose.length}문장 가운데 ${hit.length}번(${lines}줄) 나와 상한 ${share100}를 넘는다. ${why}${cited}`,
|
|
45
46
|
null,
|
|
46
47
|
NOTICE,
|
|
47
48
|
DOCUMENT,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 파이썬 rules/shared/candidates.py
|
|
2
|
+
/** 파이썬 rules/shared/candidates.py와 같은 일을 한다. 후보에는 점수와 순위가 없다. */
|
|
3
3
|
import { HAEYO, HANDA, HAPNIDA, REGISTERS, conjugate, fitJosa } from "../../analysis/grammar/index.js";
|
|
4
4
|
import * as hangul from "../../analysis/grammar/hangul.js";
|
|
5
5
|
import { decomposePassive } from "../../analysis/grammar/voice.js";
|
|
@@ -123,7 +123,7 @@ export function doublePassiveCandidates(text, passives) {
|
|
|
123
123
|
const candidateText = reducedPassiveText(text, surface, voice.reduced);
|
|
124
124
|
if (candidateText === null || seen.has(candidateText)) continue;
|
|
125
125
|
seen.add(candidateText);
|
|
126
|
-
candidates.push({ text: candidateText, why: `\`${surface}
|
|
126
|
+
candidates.push({ text: candidateText, why: `\`${surface}\`에서 겹친 피동을 하나로 줄인다` });
|
|
127
127
|
}
|
|
128
128
|
return candidates;
|
|
129
129
|
}
|
|
@@ -18,6 +18,13 @@ function countNewlines(text) {
|
|
|
18
18
|
return count;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** 사전 지적의 글. 찾은 말 뒤에 받침에 맞는 은/는을 붙인다. 뜻은 파이썬 rules/shared/dictionaryRule.py 의 message 가 소유한다.
|
|
22
|
+
* @param {import("../../fingerprint/dictionaries.js").DictionaryMatch} match */
|
|
23
|
+
function message(match) {
|
|
24
|
+
const source = match.source ? ` (${match.source})` : "";
|
|
25
|
+
return `\`${match.text}\`${fitJosa(match.text, "는")} ${match.why}${source}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
29
|
* @param {import("../../fingerprint/build.js").SentencePrint} sentence
|
|
23
30
|
* @param {import("../../fingerprint/dictionaries.js").DictionaryMatch} match
|
|
@@ -32,7 +39,7 @@ function matchFinding(sentence, match, ruleName, severity) {
|
|
|
32
39
|
ruleName,
|
|
33
40
|
sentence.line + countNewlines(sentence.text.slice(0, match.start)),
|
|
34
41
|
sentence.text,
|
|
35
|
-
|
|
42
|
+
message(match),
|
|
36
43
|
fix,
|
|
37
44
|
severity,
|
|
38
45
|
SENTENCE,
|
|
@@ -108,7 +115,7 @@ export function firstMatchFindings(doc, dictionary, ruleName, severity = ERROR)
|
|
|
108
115
|
ruleName,
|
|
109
116
|
sentence.line + countNewlines(sentence.text.slice(0, match.start)),
|
|
110
117
|
sentence.text,
|
|
111
|
-
|
|
118
|
+
message(match),
|
|
112
119
|
match.rewrite,
|
|
113
120
|
severity,
|
|
114
121
|
SENTENCE,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* 파이썬 rules/shared/repeat.py
|
|
3
|
+
* 반복을 세는 방법. 같은 값이 이어지는 구간(runsOf)과 한 값이 차지하는 비율(shareOf)을 센다.
|
|
4
|
+
* 파이썬 rules/shared/repeat.py와 같다. 규칙은 열쇠와 기준값만 정하고 세는 일은 여기서 한다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -6,8 +6,8 @@ export const name = "bridgeRepeat";
|
|
|
6
6
|
export const mechanism = "repeat";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* 절을 닫는 문장이
|
|
10
|
-
* 파이썬 rules/structure/bridgeRepeat.py
|
|
9
|
+
* 절을 닫는 문장이 접속사(이번에는, 이제, 다음으로)로 시작하는 절이 bridgeRepeatMin개 이상인 글.
|
|
10
|
+
* 파이썬 rules/structure/bridgeRepeat.py와 같다. 접속사 목록은 data/bridgeOpeners.txt에 있다.
|
|
11
11
|
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
12
12
|
* @param {import("../../config/settings.js").Config} config
|
|
13
13
|
*/
|
|
@@ -30,7 +30,7 @@ export function run(doc, config) {
|
|
|
30
30
|
name,
|
|
31
31
|
first.line,
|
|
32
32
|
first.text,
|
|
33
|
-
`절 ${closers.length}개가 \`${markers.join(", ")}\` 같은
|
|
33
|
+
`절 ${closers.length}개가 \`${markers.join(", ")}\` 같은 접속사로 시작하는 문장으로 끝난다. 예고가 절마다 되풀이되면 틀에 박힌 글이 된다. 방금 만든 결과를 이름으로 부르고 아직 못 하는 일을 적는다`,
|
|
34
34
|
null,
|
|
35
35
|
"error",
|
|
36
36
|
DOCUMENT,
|
|
@@ -26,7 +26,7 @@ export function run(doc) {
|
|
|
26
26
|
const lines = block.text.split("\n");
|
|
27
27
|
for (let offset = 0; offset < lines.length; offset++) {
|
|
28
28
|
if (BULLET_WITH_EMOJI.test(lines[offset])) {
|
|
29
|
-
findings.push(finding(name, block.startLine + offset, lines[offset].trim(), "목록 항목이 이모지로 시작한다. 이모지를 지우고 첫
|
|
29
|
+
findings.push(finding(name, block.startLine + offset, lines[offset].trim(), "목록 항목이 이모지로 시작한다. 이모지를 지우고 첫 단어가 내용을 말하게 한다", null, "error", DOCUMENT, block.index));
|
|
30
30
|
break;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -11,7 +11,7 @@ export function run(doc) {
|
|
|
11
11
|
let previous = doc.frontmatter.title ? 1 : null;
|
|
12
12
|
for (const [level, text, line] of doc.headings) {
|
|
13
13
|
if (previous !== null && level > previous + 1) {
|
|
14
|
-
findings.push(finding(name, line, text, `H${previous} 다음에 H${level}
|
|
14
|
+
findings.push(finding(name, line, text, `H${previous} 다음에 바로 H${level} 제목이 온다. 한 수준씩 내려간다`, null, "error", DOCUMENT, -1));
|
|
15
15
|
}
|
|
16
16
|
previous = level;
|
|
17
17
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { DOCUMENT, finding } from "../finding.js";
|
|
3
4
|
import { shareOf } from "../shared/repeat.js";
|
|
4
5
|
|
|
@@ -27,7 +28,7 @@ export function run(doc, config) {
|
|
|
27
28
|
);
|
|
28
29
|
if (count / total >= config.headingUniformRatio) {
|
|
29
30
|
return [
|
|
30
|
-
finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개
|
|
31
|
+
finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 가운데 ${count}개가 \`${best}\`${fitJosa(best, "로")} 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 그 문서의 관례라면 그대로 둔다`, null, "notice", DOCUMENT, -1),
|
|
31
32
|
];
|
|
32
33
|
}
|
|
33
34
|
return [];
|
|
@@ -5,7 +5,7 @@ import { NOTICE, SECTION, finding } from "../finding.js";
|
|
|
5
5
|
export const name = "loneSubheading";
|
|
6
6
|
export const mechanism = "contrast";
|
|
7
7
|
|
|
8
|
-
/** (부모 H2 제목,
|
|
8
|
+
/** (부모 H2 제목, 하나뿐인 H3 제목, 그 줄). H2 하나 아래 H3이 정확히 하나일 때만. @param {[number, string, number][]} headings */
|
|
9
9
|
function onlyChild(headings) {
|
|
10
10
|
/** @type {[string, string, number][]} */
|
|
11
11
|
const found = [];
|
|
@@ -43,6 +43,6 @@ export function run(doc, config) {
|
|
|
43
43
|
let best = over[0];
|
|
44
44
|
for (const pair of over) if (pair[1].length > best[1].length) best = pair;
|
|
45
45
|
return [
|
|
46
|
-
finding(name, best[0], best[1], `마지막 절의 목록 항목 ${over.length}개가 ${config.moreLaterMaxChars}자를
|
|
46
|
+
finding(name, best[0], best[1], `마지막 절의 목록 항목 ${over.length}개가 ${config.moreLaterMaxChars}자를 넘는다. 가장 긴 항목은 ${best[1].length}자다. 미룬 것이 아니라 옮겨 적은 것이다. 한 줄과 링크로 줄인다`, null, NOTICE, SECTION, last.index),
|
|
47
47
|
];
|
|
48
48
|
}
|
package/src/text.js
CHANGED
|
@@ -64,8 +64,8 @@ export function countIn(text, needle, end = text.length) {
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* 파이썬 round(value, digits). 정확한 이진 값을 십진으로 풀어 반올림하고 정확히 절반이면 짝수로 간다.
|
|
67
|
-
* Math.round
|
|
68
|
-
* toFixed(100)
|
|
67
|
+
* Math.round는 절반을 위로 올려 13/16을 0.813으로 만들고 파이썬은 0.812다. 두 구현의 글자가 같으려면 파이썬 쪽이다.
|
|
68
|
+
* toFixed(100)은 1e21 아래의 수를 정확한 십진 전개로 준다. 분석 결과의 비율과 평균이 있는 범위다.
|
|
69
69
|
* @param {number} value
|
|
70
70
|
* @param {number} digits
|
|
71
71
|
*/
|
package/src/usage/counts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* 명사 연쇄 빈도표. 글 종류마다 data/usageCounts.<종류>.json 하나이고 값은 그 연쇄가 나온 문서 수다.
|
|
4
|
-
* 뜻은 파이썬 usage/counts.py
|
|
4
|
+
* 뜻은 파이썬 usage/counts.py가 정한다. 표의 키는 nounRuns의 어절들을 빈칸 하나로 이은 것이다.
|
|
5
5
|
*/
|
|
6
6
|
import { USAGE_OF } from "../config/settings.js";
|
|
7
7
|
import { readText } from "../data/load.js";
|
|
@@ -12,7 +12,7 @@ import { readText } from "../data/load.js";
|
|
|
12
12
|
const tables = new Map();
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* 이 설정이 볼 빈도표의 종류. usageKind
|
|
15
|
+
* 이 설정이 볼 빈도표의 종류. usageKind가 null이면 프리셋이 정하고 ""면 보지 않는다.
|
|
16
16
|
* @param {import("../config/settings.js").Config} config @returns {string | null}
|
|
17
17
|
*/
|
|
18
18
|
export function usageKindOf(config) {
|
|
@@ -46,7 +46,7 @@ export function patternDocuments(dictionary, pattern, kind) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* 사전 항목이 그 종류의 문서 share 이상에 나오면 관용이다. 뜻은 파이썬 usage/counts.py
|
|
49
|
+
* 사전 항목이 그 종류의 문서 share 이상에 나오면 관용이다. 뜻은 파이썬 usage/counts.py의 conventional이 정한다.
|
|
50
50
|
* @param {string} dictionary @param {string} pattern @param {string} kind @param {number} share
|
|
51
51
|
*/
|
|
52
52
|
export function conventional(dictionary, pattern, kind, share) {
|
package/src/usage/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 용례
|
|
4
|
-
* counts
|
|
5
|
-
* fingerprint
|
|
3
|
+
* 용례(usage). 실제 글에서 단어가 어떻게 쓰였는지를 든다. 뜻은 파이썬 usage/가 정한다.
|
|
4
|
+
* counts는 실리는 빈도표, sentences는 사용자 기계의 문장 역인덱스다.
|
|
5
|
+
* fingerprint와 같은 단계다. rules와 cli가 이 부분을 쓰고, 이 부분은 analysis와 config와 data만 쓴다.
|
|
6
6
|
*/
|
|
7
7
|
export { attested, chainDocuments, conventional, patternDocuments, usageKindOf, usageTable } from "./counts.js";
|
|
8
8
|
export { UsageIndex, buildIndex, defaultRoot as defaultUsageRoot, indexTokens, listIndexes, loadIndex, queryCores, readDocuments } from "./sentences.js";
|