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