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/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* hanlint: 한국어 글에서
|
|
3
|
+
* hanlint: 한국어 글에서 규칙으로 잡을 수 있는 잘못을 찾는 검사기. 공개 API는 이 파일 한 곳에 있다.
|
|
4
4
|
*
|
|
5
5
|
* ```js
|
|
6
6
|
* import { lintText, lintFile } from "hanlint";
|
|
7
7
|
* for (const f of lintFile("글.md")) console.log(f.line, f.rule, f.why);
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
-
* 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은
|
|
10
|
+
* 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은 규칙으로 잡을 수 있는 잘못이 없다는 뜻이다.
|
|
11
11
|
* 같은 공개 API를 Node와 브라우저가 공유한다. 파일 경로를 읽는 API만 Node 전용이다.
|
|
12
12
|
*/
|
|
13
13
|
import { readExternal } from "./data/read.js";
|
|
@@ -84,7 +84,7 @@ export {
|
|
|
84
84
|
export const version = loadVersion();
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* 글을 한 번 읽어
|
|
87
|
+
* 글을 한 번 읽어 분석 결과를 만든다.
|
|
88
88
|
* @param {string} text
|
|
89
89
|
* @param {import("./config/settings.js").Config} [config]
|
|
90
90
|
* @param {string | null} [path]
|
|
@@ -104,7 +104,7 @@ export function lintText(text, config = defaultConfig(), path = null) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* 파일을 UTF-8
|
|
107
|
+
* 파일을 UTF-8로 읽어 검사한다.
|
|
108
108
|
* @param {string} path
|
|
109
109
|
* @param {import("./config/settings.js").Config} [config]
|
|
110
110
|
*/
|
|
@@ -112,7 +112,7 @@ export function lintFile(path, config = defaultConfig()) {
|
|
|
112
112
|
return lintText(readExternal(path), config, path);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/** 지적, 문체
|
|
115
|
+
/** 지적, 문체 예시, 승인 고침과 분석 결과를 같은 분석에서 준다. @param {string} text @param {import("./config/settings.js").Config} config */
|
|
116
116
|
export function inspectText(text, config = defaultConfig()) {
|
|
117
117
|
const document = fingerprint(text, config);
|
|
118
118
|
const findings = runAll(document, config);
|
|
@@ -132,7 +132,7 @@ export function learnText(before, after, config = defaultConfig()) {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** 자유 원고의
|
|
135
|
+
/** 자유 원고의 숫자·링크·코드와 H2 순서를 비교한다. 빈 글과 반복 제목도 관찰하며 승인 계약을 만들지 않는다. @param {string} before @param {string} after @param {import("./config/settings.js").Config} config */
|
|
136
136
|
export function compareRevision(before, after, config = defaultConfig()) {
|
|
137
137
|
const original = before.normalize("NFC");
|
|
138
138
|
const outline = { level: 2, headings: headingsOf(parseMarkdown(original), 2).map((heading) => heading.text) };
|
package/src/learn/edits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Python learn/edits.py와 같은
|
|
2
|
+
/** Python learn/edits.py와 같은 방법으로 문장을 짝짓고, 지적이 사라진 문장 짝을 후보로 낸다. */
|
|
3
3
|
import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
|
|
4
4
|
import { readerKind } from "../fingerprint/readerState.js";
|
|
5
5
|
import { localCue, SENTENCE } from "../rules/finding.js";
|
|
@@ -21,7 +21,7 @@ export function learnExemplars(beforeDoc, afterDoc, beforeFindings, afterFinding
|
|
|
21
21
|
if (!afterText || before.text.trim() === afterPlain) continue;
|
|
22
22
|
seen.add(key);
|
|
23
23
|
learned.push({ rule: finding.rule, before: (rawBefore.get(before.index) ?? before.text).trim(), after: afterText,
|
|
24
|
-
moved: "
|
|
24
|
+
moved: "실제로 고친 문장으로 바꿨다", why: finding.why, beforeLine: before.line,
|
|
25
25
|
afterLines: [...new Set(after.map((item) => item.line))], sentence: before.text.trim(),
|
|
26
26
|
cue: localCue(finding), reader: readerKind(before, beforeDoc.reader.beforeSentence[finding.at]),
|
|
27
27
|
...(preset ? { presets: [preset] } : {}),
|
package/src/learn/operations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Python learn/operations.py의
|
|
2
|
+
/** Python learn/operations.py의 치환 후보와 증거 묶음. */
|
|
3
3
|
import { operationFromApproval } from "../data/operations.js";
|
|
4
4
|
import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
|
|
5
5
|
import { changedSentencePairs } from "./pairs.js";
|
package/src/regex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 파이썬 re
|
|
3
|
+
* 파이썬 re와 같은 뜻으로 쓰는 얇은 껍질. search는 첫 일치, at은 문자열 첫머리에서만, all은 전부.
|
|
4
4
|
* 정규식 문법은 데이터 파일과 규칙에서 파이썬과 같은 부분집합만 쓴다 (lookbehind 포함, 이름 그룹 없음).
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -41,7 +41,7 @@ export function compile(source, flags = "") {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape
|
|
44
|
+
/** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape의 쓰임새). @param {string} text */
|
|
45
45
|
export function escape(text) {
|
|
46
46
|
return text.replace(SPECIALS, (c) => BACKSLASH + c);
|
|
47
47
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 한 줄에 지적 하나. AI
|
|
2
|
+
/** 한 줄에 지적 하나. AI와 스크립트가 읽는 형식이다. 파이썬 report/compactReport.py와 같은 글자다. */
|
|
3
3
|
|
|
4
4
|
/** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 형식의 계약이다. @param {string} text */
|
|
5
5
|
function flat(text) {
|
|
@@ -10,8 +10,8 @@ function flat(text) {
|
|
|
10
10
|
export function renderCompact(path, findings) {
|
|
11
11
|
return findings
|
|
12
12
|
.map((f) => {
|
|
13
|
-
const label = f.severity === "notice" ? "
|
|
14
|
-
const fixLabel = f.severity === "notice" ? "
|
|
13
|
+
const label = f.severity === "notice" ? "경고: " : "";
|
|
14
|
+
const fixLabel = f.severity === "notice" ? "제안" : "고친 뒤";
|
|
15
15
|
return `${path}:${f.line} [${f.rule}] ${label}${flat(f.why)}` + (f.fix ? ` ${fixLabel}: ${flat(f.fix)}` : "");
|
|
16
16
|
})
|
|
17
17
|
.join("\n");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py
|
|
4
|
-
* 아래층을 index
|
|
3
|
+
* 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py와 같은 키 순서다. 층마다 한 번씩만 적고 위층은
|
|
4
|
+
* 아래층을 index로 가리킨다. 실수는 소수 여섯째 자리로 맞춰 두 구현이 같은 글자를 낸다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { roundHalfEven } from "../text.js";
|
package/src/report/jsonReport.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 기계가 읽는 형식. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False)
|
|
4
|
-
* 지적마다 `exemplar
|
|
3
|
+
* 기계가 읽는 형식. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False)와 같은 글자다.
|
|
4
|
+
* 지적마다 `exemplar`를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
|
|
5
5
|
* 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 실험 스크립트에서 따로 잰다.
|
|
6
6
|
*/
|
|
7
7
|
import { exemplarFor } from "../data/exemplars.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 한 문체로 저장한
|
|
2
|
+
/** 한 문체로 저장한 예시와 문형을 검사한 글의 문체에 맞춘다. */
|
|
3
3
|
import { HAPNIDA, REGISTERS, convertTemplate } from "../analysis/grammar/index.js";
|
|
4
4
|
|
|
5
5
|
/** 섞임과 없음은 데이터 정본의 합니다체를 유지한다. @param {string | null | undefined} register */
|
|
@@ -14,7 +14,7 @@ export function exemplarInRegister(exemplar, register) {
|
|
|
14
14
|
const after = convertTemplate(exemplar.after, target);
|
|
15
15
|
if (before.skipped || after.skipped) {
|
|
16
16
|
throw new Error(
|
|
17
|
-
exemplar.rule + "
|
|
17
|
+
exemplar.rule + " 예시의 서술어를 못 풀었다: " + (before.skipped + after.skipped),
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
return { rule: exemplar.rule, before: before.text, after: after.text, moved: exemplar.moved, presets: exemplar.presets };
|
package/src/report/sheet.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* 글 시트. 소스에서 뽑은 글 마디와 지적을 마크다운 표 하나로 내고, 사람이 고친 표를 다시 읽는다.
|
|
4
|
-
* 뜻과 칸의 정본은 파이썬 report/sheet.py
|
|
4
|
+
* 뜻과 칸의 정본은 파이썬 report/sheet.py다. 여기는 같은 글자를 낸다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { parseMarkdown } from "../document/parseMarkdown.js";
|
|
@@ -19,12 +19,12 @@ const ROW_PATTERN = /^\|(.*)\|$/;
|
|
|
19
19
|
* @property {string} text
|
|
20
20
|
* @property {import("../rules/finding.js").Finding[]} findings
|
|
21
21
|
* @property {string} fix 사람이 표에 적은 고친 글. 뽑을 때는 비어 있다
|
|
22
|
-
* @property {number} column 1부터 세는 칸. 0
|
|
22
|
+
* @property {number} column 1부터 세는 칸. 0이면 모른다
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/** @param {string} text */
|
|
26
26
|
/**
|
|
27
|
-
* 지적이 하나이고 그 지적에 고침이 있으면 고침 칸에 미리 적는다. 뜻은 파이썬 report/sheet.py
|
|
27
|
+
* 지적이 하나이고 그 지적에 고침이 있으면 고침 칸에 미리 적는다. 뜻은 파이썬 report/sheet.py의 prefilledFix가 소유한다.
|
|
28
28
|
* @param {import("../document/sourceText.js").SourceLiteral} literal
|
|
29
29
|
* @param {import("../rules/finding.js").Finding[]} findings
|
|
30
30
|
*/
|
|
@@ -35,8 +35,8 @@ function prefilledFix(literal, findings) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* (라벨, 소스)
|
|
39
|
-
* 소스를 읽는 쪽 (CLI
|
|
38
|
+
* (라벨, 소스)마다 글을 뽑아 규칙을 돌리고 표의 행을 만든다. 지적은 `error`만 남긴다.
|
|
39
|
+
* 소스를 읽는 쪽 (CLI는 디스크, 브라우저는 저장소)이 다르므로 읽기는 밖에 두고 문자열만 받는다.
|
|
40
40
|
* @param {Iterable<{ label: string, source: string }>} sources
|
|
41
41
|
* @param {import("../config/settings.js").Config} config
|
|
42
42
|
* @param {boolean} [everything] 지적이 없는 글도 넣는다
|
|
@@ -63,7 +63,7 @@ export function unescapeCell(text) {
|
|
|
63
63
|
return text.replaceAll("\\|", "|").replaceAll("\\\\", "\\");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/** `| a | b
|
|
66
|
+
/** `| a | b |`를 칸으로. `\|`는 칸 경계가 아니다. @param {string} line @returns {string[] | null} */
|
|
67
67
|
export function splitRow(line) {
|
|
68
68
|
const match = ROW_PATTERN.exec(line.trim());
|
|
69
69
|
if (!match) return null;
|
|
@@ -91,7 +91,7 @@ const CUE = /^`[^`]+`/;
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* `column` (1부터)
|
|
94
|
+
* `column` (1부터)에 `old`가 그대로 있으면 그 자리만 `new`로 바꾼다.
|
|
95
95
|
* @param {string} lineText @param {number} column @param {string} old @param {string} replacement @returns {[string, boolean]}
|
|
96
96
|
*/
|
|
97
97
|
function replaceAt(lineText, column, old, replacement) {
|
|
@@ -101,8 +101,8 @@ function replaceAt(lineText, column, old, replacement) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
|
-
* 한 파일의 소스에 표의 행들을 되돌려 쓴다. [새 소스, 적용한 자리, 실패한 자리와 이유]. 뜻은 파이썬 report/sheet.py
|
|
105
|
-
* applyRows
|
|
104
|
+
* 한 파일의 소스에 표의 행들을 되돌려 쓴다. [새 소스, 적용한 자리, 실패한 자리와 이유]. 뜻은 파이썬 report/sheet.py의
|
|
105
|
+
* applyRows가 소유한다. 한 줄에 고침이 여럿이면 뒤 칸부터 바꾼다. 줄은 `\n` 으로만 나누므로 `\r`은 남아 CRLF 파일이 그대로다.
|
|
106
106
|
* @param {string} source @param {SheetRow[]} rows @returns {[string, string[], string[]]}
|
|
107
107
|
*/
|
|
108
108
|
export function applyRows(source, rows) {
|
|
@@ -140,8 +140,8 @@ export function applyRows(source, rows) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
* 규칙마다 한 번. 첫 지적의 이유는 온전히, 같은 규칙의 나머지는 단서 (이유 앞의 `…`)
|
|
144
|
-
* 뜻은 파이썬 report/sheet.py
|
|
143
|
+
* 규칙마다 한 번. 첫 지적의 이유는 온전히, 같은 규칙의 나머지는 단서 (이유 앞의 `…`)만 `(또 …)`로 잇는다.
|
|
144
|
+
* 뜻은 파이썬 report/sheet.py의 findingCell이 소유한다.
|
|
145
145
|
* @param {import("../rules/finding.js").Finding[]} findings
|
|
146
146
|
*/
|
|
147
147
|
function findingCell(findings) {
|
|
@@ -168,7 +168,7 @@ function placeOf(row) {
|
|
|
168
168
|
return row.column > 0 ? `${row.file}:${row.line}:${row.column}` : `${row.file}:${row.line}`;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/**
|
|
171
|
+
/** `경로:줄:칸`이나 `경로:줄`을 [경로, 줄, 칸]으로. 칸이 없으면 0. 꼴이 아니면 경로가 null. @param {string} place @returns {[string | null, number, number]} */
|
|
172
172
|
export function splitPlace(place) {
|
|
173
173
|
const last = place.lastIndexOf(":");
|
|
174
174
|
if (last < 0 || !/^[0-9]+$/.test(place.slice(last + 1))) return [null, 0, 0];
|
|
@@ -185,7 +185,7 @@ export function renderSheet(rows, preset, fileCount) {
|
|
|
185
185
|
"# hanlint sheet",
|
|
186
186
|
"",
|
|
187
187
|
`프리셋 ${preset}, 파일 ${fileCount}개, 글 ${rows.length}개, 지적 있는 글 ${flagged}개. ` +
|
|
188
|
-
"고침 칸에 새 글을 적고 `hanlint sheet apply 이파일.md
|
|
188
|
+
"고침 칸에 새 글을 적고 `hanlint sheet apply 이파일.md`로 되돌려 쓴다. 다른 칸은 고치지 않는다.",
|
|
189
189
|
"",
|
|
190
190
|
`| ${HEADER.join(" | ")} |`,
|
|
191
191
|
`| ${HEADER.map(() => "---").join(" | ")} |`,
|
|
@@ -216,7 +216,7 @@ export function renderSheetJson(rows, preset, fileCount) {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* 사람이 고친 표를 읽는다. 머리글과 구분선은 넘기고
|
|
219
|
+
* 사람이 고친 표를 읽는다. 머리글과 구분선은 넘기고 `고침`이 있는 줄만 남긴다.
|
|
220
220
|
* @param {string} markdown @returns {{ rows: SheetRow[], problems: string[] }}
|
|
221
221
|
*/
|
|
222
222
|
export function parseSheet(markdown) {
|
|
@@ -230,13 +230,13 @@ export function parseSheet(markdown) {
|
|
|
230
230
|
if (cells === null) return;
|
|
231
231
|
if (cells.slice(0, HEADER.length).join("") === HEADER.join("") || cells.every((cell) => cell.replace(/-/g, "") === "")) return;
|
|
232
232
|
if (cells.length !== HEADER.length) {
|
|
233
|
-
problems.push(`${lineNumber}
|
|
233
|
+
problems.push(`${lineNumber}번째 줄: 칸이 ${cells.length}개다. ${HEADER.length}개여야 한다`);
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
236
|
const place = unescapeCell(cells[1]);
|
|
237
237
|
const [file, line, column] = splitPlace(place);
|
|
238
238
|
if (file === null) {
|
|
239
|
-
problems.push(`${lineNumber}
|
|
239
|
+
problems.push(`${lineNumber}번째 줄: 경로:줄이나 경로:줄:칸 형식이 아닌 자리: \`${place}\``);
|
|
240
240
|
return;
|
|
241
241
|
}
|
|
242
242
|
const fix = unescapeCell(cells[4]);
|
package/src/report/textReport.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 사람이 읽는 지적 목록.
|
|
4
|
-
* 끝에
|
|
3
|
+
* 사람이 읽는 지적 목록. `파일:줄`로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py와 같은 글자다.
|
|
4
|
+
* 끝에 예시를 붙인다. 전과 후를 제 줄에 둔다. 왜 한 줄이 아닌지는 파이썬 report/textReport.py가 소유한다.
|
|
5
5
|
*/
|
|
6
6
|
import { exemplarFor, shortened, twoLines } from "../data/exemplars.js";
|
|
7
7
|
import { exemplarInRegister } from "./registerMatch.js";
|
|
8
8
|
|
|
9
|
-
/** 그 글에 나온 규칙의
|
|
9
|
+
/** 그 글에 나온 규칙의 예시. 이름 순이고 규칙 하나에 세 줄이다. @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
|
|
10
10
|
function exemplarLines(findings, register, preset, customExemplars) {
|
|
11
11
|
const lines = [];
|
|
12
12
|
let cut = false;
|
|
@@ -19,15 +19,15 @@ function exemplarLines(findings, register, preset, customExemplars) {
|
|
|
19
19
|
lines.push(` [${name}]`, ` 전 ${before}`, ` 후 ${after}`);
|
|
20
20
|
}
|
|
21
21
|
if (!lines.length) return [];
|
|
22
|
-
const head = "
|
|
23
|
-
return [cut ? `${head}. 잘린 것은 hanlint explain
|
|
22
|
+
const head = "예시 (고치기 전, 고친 뒤)";
|
|
23
|
+
return [cut ? `${head}. 잘린 것은 hanlint explain <규칙>으로 본다` : head, ...lines];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/** 예시 아래에 붙이는 선택지. 지적 자리별이며 순위는 없다. @param {import("../rules/finding.js").Finding[]} findings */
|
|
27
27
|
function candidateLines(findings) {
|
|
28
28
|
const chosen = findings.filter((finding) => finding.candidates.length);
|
|
29
29
|
if (!chosen.length) return [];
|
|
30
|
-
const lines = ["후보 (
|
|
30
|
+
const lines = ["후보 (hanlint가 하나를 고르지 않았다)"];
|
|
31
31
|
for (const finding of chosen) {
|
|
32
32
|
lines.push(` ${finding.line}줄 [${finding.rule}]`);
|
|
33
33
|
for (const candidate of finding.candidates) lines.push(` - ${candidate.text} (${candidate.why})`);
|
|
@@ -35,7 +35,7 @@ function candidateLines(findings) {
|
|
|
35
35
|
return lines;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** 접은
|
|
38
|
+
/** 접은 경고. 규칙마다 개수와 줄 번호 한 줄. 뜻은 파이썬 textReport.py의 noticeLines가 소유한다. @param {import("../rules/finding.js").Finding[]} notices */
|
|
39
39
|
function noticeLines(notices) {
|
|
40
40
|
/** @type {Map<string, number[]>} */
|
|
41
41
|
const byRule = new Map();
|
|
@@ -43,7 +43,7 @@ function noticeLines(notices) {
|
|
|
43
43
|
if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
|
|
44
44
|
/** @type {number[]} */ (byRule.get(finding.rule)).push(finding.line);
|
|
45
45
|
}
|
|
46
|
-
const lines = [
|
|
46
|
+
const lines = [`경고 ${notices.length}건 (접어 두었다. 다 보려면 --notices)`];
|
|
47
47
|
for (const rule of [...byRule.keys()].sort()) {
|
|
48
48
|
const numbers = [...new Set(/** @type {number[]} */ (byRule.get(rule)))].sort((a, b) => a - b);
|
|
49
49
|
lines.push(` ${rule} ${/** @type {number[]} */ (byRule.get(rule)).length} ${numbers.join(", ")}줄`);
|
|
@@ -56,14 +56,14 @@ export function renderText(path, findings, register = null, preset = null, custo
|
|
|
56
56
|
if (!findings.length) return `${path} 오류 없음`;
|
|
57
57
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
58
58
|
const notices = findings.length - errors;
|
|
59
|
-
const summary = `${path} 오류 ${errors}건` + (notices ? `,
|
|
59
|
+
const summary = `${path} 오류 ${errors}건` + (notices ? `, 경고 ${notices}건` : "");
|
|
60
60
|
const lines = [summary, ""];
|
|
61
61
|
const folded = unfoldNotices ? [] : findings.filter((f) => f.severity === "notice");
|
|
62
62
|
const shown = findings.filter((f) => unfoldNotices || f.severity === "error");
|
|
63
63
|
for (const f of shown) {
|
|
64
|
-
const tag = `[${f.rule}]` + (f.severity === "notice" ? "
|
|
64
|
+
const tag = `[${f.rule}]` + (f.severity === "notice" ? " 경고" : "");
|
|
65
65
|
lines.push(`${path}:${f.line} ${tag}`, ` ${f.quote}`, ` ${f.why}`);
|
|
66
|
-
if (f.fix) lines.push(` ${f.severity === "notice" ? "
|
|
66
|
+
if (f.fix) lines.push(` ${f.severity === "notice" ? "제안" : "고친 뒤"}: ${f.fix}`);
|
|
67
67
|
lines.push("");
|
|
68
68
|
}
|
|
69
69
|
if (folded.length) lines.push(...noticeLines(folded), "");
|
|
@@ -27,7 +27,7 @@ function percent(value) {
|
|
|
27
27
|
function selectedProfile(config) {
|
|
28
28
|
if (config.profile) return userProfile(config.profile);
|
|
29
29
|
const kind = PROFILE_OF[config.preset];
|
|
30
|
-
if (kind === null) throw new Error(`${config.preset}
|
|
30
|
+
if (kind === null) throw new Error(`${config.preset} 프리셋은 비교할 프로파일이 없어 스펙을 낼 수 없다`);
|
|
31
31
|
const profile = profileOf(kind);
|
|
32
32
|
if (profile === null) throw new Error(`${kind} 프로파일을 찾지 못했다`);
|
|
33
33
|
return profile;
|
|
@@ -40,8 +40,8 @@ function selectedProfile(config) {
|
|
|
40
40
|
* @param {number | null} [targetChars]
|
|
41
41
|
*/
|
|
42
42
|
export function writingSpec(config = defaultConfig(), register = HAPNIDA, targetChars = null) {
|
|
43
|
-
if (!REGISTERS.includes(register)) throw new Error(`register
|
|
44
|
-
if (targetChars !== null && targetChars <= 0) throw new Error(`chars
|
|
43
|
+
if (!REGISTERS.includes(register)) throw new Error(`register는 ${REGISTERS.join(", ")} 가운데 하나여야 한다. 받은 값: ${register}`);
|
|
44
|
+
if (targetChars !== null && targetChars <= 0) throw new Error(`chars는 1 이상이어야 한다. 받은 값: ${targetChars}`);
|
|
45
45
|
const profile = selectedProfile(config);
|
|
46
46
|
const sentence = profile.sentence;
|
|
47
47
|
const paragraph = profile.paragraph.get("sentenceCount");
|
|
@@ -62,7 +62,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
62
62
|
}
|
|
63
63
|
rows.push({ id: "amount", label: "분량", guidance: amount, basis: [`profile.${profile.kind}.paragraph.sentenceCount`] });
|
|
64
64
|
|
|
65
|
-
let length =
|
|
65
|
+
let length = `중앙값 ${lengthP50}어절, 문장 90%가 ${percentile(lengthHistogram, 90)}어절 이하`;
|
|
66
66
|
const lengthBasis = [`profile.${profile.kind}.sentence.length`];
|
|
67
67
|
if (enabled(config, "longSentence")) {
|
|
68
68
|
length += `. ${config.longSentenceMax}어절을 넘으면 longSentence가 지적`;
|
|
@@ -71,7 +71,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
71
71
|
rows.push({ id: "sentenceLength", label: "문장 길이", guidance: length, basis: lengthBasis });
|
|
72
72
|
|
|
73
73
|
if (profile.endingRuns !== null) {
|
|
74
|
-
let endingText = `같은 끝맺음 연속
|
|
74
|
+
let endingText = `같은 끝맺음 연속 중앙값 ${percentile(profile.endingRuns, 50)}개, 90%가 ${percentile(profile.endingRuns, 90)}개 이하`;
|
|
75
75
|
const endingBasis = [`profile.${profile.kind}.endingRuns`];
|
|
76
76
|
if (enabled(config, "endingRepeat")) {
|
|
77
77
|
endingText += `. 인과나 독자 호출 없이 ${config.endingRun}개부터 endingRepeat가 지적`;
|
|
@@ -85,13 +85,13 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
85
85
|
rows.push({
|
|
86
86
|
id: "commas",
|
|
87
87
|
label: "쉼표",
|
|
88
|
-
guidance: `문장
|
|
88
|
+
guidance: `문장 중앙값 ${percentile(commas, 50)}개, 90%가 ${percentile(commas, 90)}개 이하, 99%가 ${percentile(commas, 99)}개 이하`,
|
|
89
89
|
basis: [`profile.${profile.kind}.sentence.commas`],
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
const eui = sentence.get("euiCount");
|
|
93
93
|
if (!eui) throw new Error("프로파일에 sentence.euiCount가 없다");
|
|
94
|
-
let euiText = `문장
|
|
94
|
+
let euiText = `문장 중앙값 ${percentile(eui, 50)}개, 90%가 ${percentile(eui, 90)}개 이하`;
|
|
95
95
|
const euiBasis = [`profile.${profile.kind}.sentence.euiCount`];
|
|
96
96
|
if (enabled(config, "euiChain")) {
|
|
97
97
|
euiText += ". 3개부터, 붙은 2개부터 euiChain이 지적";
|
|
@@ -114,7 +114,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
114
114
|
rows.push({
|
|
115
115
|
id: "newTopics",
|
|
116
116
|
label: "새 화제",
|
|
117
|
-
guidance: `문장
|
|
117
|
+
guidance: `문장 중앙값 ${percentile(newTopics, 50)}개, 90%가 ${percentile(newTopics, 90)}개 이하`,
|
|
118
118
|
basis: [`profile.${profile.kind}.sentence.newTopics`],
|
|
119
119
|
});
|
|
120
120
|
|
|
@@ -122,7 +122,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
122
122
|
rows.push({
|
|
123
123
|
id: "connector",
|
|
124
124
|
label: "문두 접속",
|
|
125
|
-
guidance: `문서
|
|
125
|
+
guidance: `문서 중앙값은 문장의 ${percent(connector.p50)}%, 문서 90%가 ${percent(connector.p90)}% 이하`,
|
|
126
126
|
basis: [`profile.${profile.kind}.rates.connector`],
|
|
127
127
|
});
|
|
128
128
|
|
|
@@ -132,8 +132,8 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
132
132
|
const numberP90 = percentile(numbers, 90);
|
|
133
133
|
const numberText =
|
|
134
134
|
numberP50 === 0 && numberP90 === 0
|
|
135
|
-
? "문장
|
|
136
|
-
: `문장
|
|
135
|
+
? "문장 중앙값과 90%에서 숫자 없음"
|
|
136
|
+
: `문장 중앙값 ${numberP50}개, 90%가 ${numberP90}개 이하`;
|
|
137
137
|
rows.push({ id: "numbers", label: "숫자", guidance: numberText, basis: [`profile.${profile.kind}.sentence.numbers`] });
|
|
138
138
|
|
|
139
139
|
const question = profile.rates.question;
|
|
@@ -142,10 +142,10 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
142
142
|
if (enabled(config, "noQuestion")) {
|
|
143
143
|
questionText = config.enforceStyle.includes("noQuestion")
|
|
144
144
|
? "선택한 정책: 절이 2개 이상이면 글 전체에 물음표 최소 1개"
|
|
145
|
-
: "
|
|
145
|
+
: "물음표가 없으면 경고다. 질문 없이 충분히 설명했다면 그대로 둔다";
|
|
146
146
|
questionBasis = [`profile.${profile.kind}.rates.question`, "rule.noQuestion"];
|
|
147
147
|
} else {
|
|
148
|
-
questionText = `규칙 요구 없음. 문서
|
|
148
|
+
questionText = `규칙 요구 없음. 문서 중앙값은 문장의 ${percent(question.p50)}%, 문서 90%가 ${percent(question.p90)}% 이하`;
|
|
149
149
|
questionBasis = [`profile.${profile.kind}.rates.question`];
|
|
150
150
|
}
|
|
151
151
|
rows.push({ id: "question", label: "물음표", guidance: questionText, basis: questionBasis });
|
|
@@ -165,10 +165,10 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
165
165
|
export function renderWritingSpec(spec) {
|
|
166
166
|
const target = spec.targetChars !== null ? `, ${spec.targetChars}자` : "";
|
|
167
167
|
const lines = [
|
|
168
|
-
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은
|
|
168
|
+
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은 규칙의 기준값과 ${spec.profile} 글의 평소 수치를 쓰기 전에 보여 준다`,
|
|
169
169
|
];
|
|
170
170
|
const width = Math.max(...spec.rows.map((row) => row.label.length));
|
|
171
171
|
for (const row of spec.rows) lines.push(` ${row.label.padEnd(width)} ${row.guidance}`);
|
|
172
|
-
lines.push("", "이
|
|
172
|
+
lines.push("", "이 스펙은 품질 점수가 아니다. 다 쓴 뒤 같은 설정으로 hanlint <글.md>를 실행해 지적을 확인한다");
|
|
173
173
|
return lines.join("\n");
|
|
174
174
|
}
|
|
@@ -42,7 +42,7 @@ export function run(doc) {
|
|
|
42
42
|
if (!isDataFile(path)) continue;
|
|
43
43
|
const name2 = fileName(path);
|
|
44
44
|
if (have.has(name2) || doc.reader.mentionedBefore(block.index, name2)) continue;
|
|
45
|
-
findings.push(finding(name, line, code.trim(), `\`${name2}\`
|
|
45
|
+
findings.push(finding(name, line, code.trim(), `\`${name2}\` 파일을 읽는데 글 어디에서도 만들지 않았다. 독자는 여기서 파일 없음 오류로 멈춘다`, null, "error", DOCUMENT, block.index));
|
|
46
46
|
}
|
|
47
47
|
for (const path of [...writeTargets(code), ...reads]) {
|
|
48
48
|
if (NOT_A_PATH.some((mark) => path.includes(mark))) continue;
|
|
@@ -108,7 +108,7 @@ export function run(doc) {
|
|
|
108
108
|
const acceptable = packageOf().get(top) ?? new Set([normalize(top)]);
|
|
109
109
|
if (![...acceptable].some((pkg) => packages.has(pkg))) {
|
|
110
110
|
const packageNames = [...acceptable].sort().join("`, `");
|
|
111
|
-
findings.push(finding(name, line, code.trim(), `\`${top}\`
|
|
111
|
+
findings.push(finding(name, line, code.trim(), `\`${top}\` 모듈을 import하는데 설치 줄에 그 모듈이 든 패키지(\`${packageNames}\`)가 없다. 독자는 ModuleNotFoundError에서 멈춘다`, null, "error", DOCUMENT, block.index));
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -120,7 +120,7 @@ export function run(doc) {
|
|
|
120
120
|
const name2 = normalize(parsed[1]);
|
|
121
121
|
const extra = parsed[2] ? normalize(parsed[2]) : null;
|
|
122
122
|
if (packages.has(name2) && (extra === null || (packages.get(name2)?.has(extra) ?? false))) continue;
|
|
123
|
-
findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\`
|
|
123
|
+
findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\` 패키지가 있어야 실행되는데 설치 줄에 없다. ${why}`, null, NOTICE, DOCUMENT, block.index));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -27,7 +27,7 @@ export function run(doc) {
|
|
|
27
27
|
for (const [pattern, source, platform, alternative] of platformApis()) {
|
|
28
28
|
pattern.lastIndex = 0;
|
|
29
29
|
if (pattern.test(code)) {
|
|
30
|
-
findings.push(finding(name, line, code.trim(), `\`${source}\`
|
|
30
|
+
findings.push(finding(name, line, code.trim(), `\`${source}\` 부분은 ${platform} 전용이다. 다른 운영체제의 독자는 여기서 멈춘다. ${alternative}`, null, NOTICE, DOCUMENT, block.index));
|
|
31
31
|
break;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -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
|
|
|
4
5
|
export const name = "countMismatch";
|
|
@@ -50,7 +51,7 @@ export function run(doc, config) {
|
|
|
50
51
|
const conflict = promises.slice(i + 1).find((p) => p[1] !== numberA && comparable(doc, lineA, p[0], config.countMismatchSpan));
|
|
51
52
|
if (!conflict) continue;
|
|
52
53
|
const [lineB, , textB] = conflict;
|
|
53
|
-
findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}
|
|
54
|
+
findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}\`${fitJosa(textA, "이라고")} 했는데 여기서는 \`${textB}\`${fitJosa(textB, "이라고")} 한다. 같은 단위로 다른 수를 약속하면 독자가 어느 쪽을 믿을지 모른다`, null, "error", DOCUMENT, -1));
|
|
54
55
|
break;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -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
|
}
|