hanlint 0.0.17 → 0.0.18
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 +12 -10
- package/data/calqueFrame.json +60 -0
- package/data/cliches.json +10 -0
- package/data/collocationStops.txt +28 -0
- package/data/easyWords.json +0 -234
- package/data/exemplars.json +23 -2
- package/data/koreanProse.txt +64 -0
- package/data/patterns.json +2 -2
- package/data/phraseRepeat.json +11 -0
- package/data/ruleCategories.json +2 -0
- package/data/ruleDocs.json +18 -16
- package/data/ruleMechanisms.json +2 -0
- package/data/screenNarration.json +7 -7
- package/data/screenSentence.json +56 -8
- package/data/screenTone.json +4 -4
- package/data/screenWord.json +48 -88
- package/data/spacing.json +1 -1
- package/data/usageCounts.report.json +1676 -0
- package/data/version.json +1 -1
- package/package.json +1 -1
- package/src/analysis/index.js +1 -1
- package/src/analysis/tokenize.js +30 -15
- package/src/cli/help.js +3 -2
- package/src/cli/main.js +227 -88
- package/src/cli/walk.js +1 -1
- package/src/cli/welcome.js +1 -1
- package/src/config/settings.js +17 -0
- package/src/data/profiles.js +2 -2
- package/src/data/prose.js +27 -0
- package/src/document/sourceText.js +104 -6
- package/src/fingerprint/build.js +13 -4
- package/src/fingerprint/dictionaries.js +31 -4
- package/src/fingerprint/markers.js +22 -0
- package/src/index.js +3 -1
- package/src/report/compactReport.js +7 -3
- package/src/report/jsonReport.js +2 -2
- package/src/report/sheet.js +0 -0
- package/src/report/textReport.js +27 -8
- package/src/report/writingSpec.js +3 -3
- package/src/rules/registry.js +5 -1
- package/src/rules/screen/screenWord.js +2 -1
- package/src/rules/sentence/calqueFrame.js +11 -0
- package/src/rules/sentence/cliche.js +7 -3
- package/src/rules/sentence/hardWord.js +6 -3
- package/src/rules/sentence/japaneseLoan.js +7 -3
- package/src/rules/sentence/nounPile.js +10 -4
- package/src/rules/sentence/phraseRepeat.js +53 -0
- package/src/rules/sentence/redundantPair.js +7 -3
- package/src/rules/sentence/translationese.js +7 -3
- package/src/rules/shared/dictionaryRule.js +37 -6
- package/src/text.js +1 -1
- package/src/usage/counts.js +54 -0
- package/src/usage/index.js +8 -0
- package/src/usage/sentences.js +681 -0
package/src/config/settings.js
CHANGED
|
@@ -66,6 +66,7 @@ PRESETS.chat = [...CONVERSATION, ...SCREEN];
|
|
|
66
66
|
const SCREEN_OFF = [
|
|
67
67
|
"cliche",
|
|
68
68
|
"connectorRepeat",
|
|
69
|
+
"phraseRepeat",
|
|
69
70
|
"danglingDeixis",
|
|
70
71
|
"deixis",
|
|
71
72
|
"doubleNegative",
|
|
@@ -123,6 +124,11 @@ export const PROFILE_OF = {
|
|
|
123
124
|
screen: null,
|
|
124
125
|
};
|
|
125
126
|
|
|
127
|
+
/** 프리셋 → 용례 빈도표의 종류. 정본은 파이썬 config/settings.py 의 USAGE_OF 다. @type {Record<string, string>} */
|
|
128
|
+
export const USAGE_OF = { report: "report" };
|
|
129
|
+
/** 실린 빈도표의 종류. */
|
|
130
|
+
export const USAGE_KINDS = [...new Set(Object.values(USAGE_OF))].sort();
|
|
131
|
+
|
|
126
132
|
export const PRESET_NAMES = Object.keys(PRESETS);
|
|
127
133
|
/** 설정도 옵션도 없을 때의 종류. 이 이름일 때는 출력에 프리셋을 적지 않는다. */
|
|
128
134
|
export const DEFAULT_PRESET = PRESET_NAMES[0];
|
|
@@ -152,6 +158,9 @@ export const DEFAULT_PRESET = PRESET_NAMES[0];
|
|
|
152
158
|
* @property {number} headingSentenceMaxLevel
|
|
153
159
|
* @property {number} bridgeRepeatMin
|
|
154
160
|
* @property {number} nounPileMin
|
|
161
|
+
* @property {string | null} usageKind 용례 빈도표의 종류. null 이면 프리셋이 정하고 "" 면 보지 않는다
|
|
162
|
+
* @property {number} usageMin 명사 연쇄가 몇 편의 문서에 나와야 용례로 보는가
|
|
163
|
+
* @property {number} usageShare 사전 항목이 그 종류의 문서 몇 할에 나오면 관용으로 보는가
|
|
155
164
|
* @property {number} endingRun
|
|
156
165
|
* @property {number} factListMinSentences
|
|
157
166
|
* @property {number} factListMaxMeanLength
|
|
@@ -191,6 +200,9 @@ export function defaultConfig() {
|
|
|
191
200
|
headingSentenceMaxLevel: 6,
|
|
192
201
|
bridgeRepeatMin: 3,
|
|
193
202
|
nounPileMin: 5,
|
|
203
|
+
usageKind: null,
|
|
204
|
+
usageMin: 3,
|
|
205
|
+
usageShare: 0.9,
|
|
194
206
|
endingRun: 4,
|
|
195
207
|
factListMinSentences: 3,
|
|
196
208
|
factListMaxMeanLength: 8.0,
|
|
@@ -235,6 +247,11 @@ export function configFromMapping(data) {
|
|
|
235
247
|
throw new Error(`enforceStyle 은 ${ENFORCEABLE.join(", ")}의 배열이다`);
|
|
236
248
|
}
|
|
237
249
|
config.enforceStyle = [...value];
|
|
250
|
+
} else if (key === "usageKind") {
|
|
251
|
+
if (value !== null && value !== "" && !USAGE_KINDS.includes(/** @type {string} */ (value))) {
|
|
252
|
+
throw new Error(`usageKind 는 ${USAGE_KINDS.join(", ")} 가운데 하나이거나 빈 문자열이다: ${JSON.stringify(value)}`);
|
|
253
|
+
}
|
|
254
|
+
config.usageKind = /** @type {string | null} */ (value);
|
|
238
255
|
} else if (key === "dictionary") {
|
|
239
256
|
config.dictionary = { .../** @type {Record<string, unknown[]>} */ (value) };
|
|
240
257
|
} else if (key === "exemplars") {
|
package/src/data/profiles.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* 참조 분포 (프로파일) 의 자료형과 읽기. 파이썬 data/profiles.py 와 같다. 종류별 프로파일은 profiles.json (투영) 에서,
|
|
4
|
-
* 사용자 프로파일은 `hanlint profile build` 가 만든 파일에서 읽는다. 히스토그램은 정확한 계수라 두
|
|
4
|
+
* 사용자 프로파일은 `hanlint profile build` 가 만든 파일에서 읽는다. 히스토그램은 정확한 계수라 두 구현의 셈이 같다.
|
|
5
5
|
*/
|
|
6
6
|
import { readExternal } from "./read.js";
|
|
7
7
|
|
|
@@ -52,7 +52,7 @@ function histogramFromDict(data) {
|
|
|
52
52
|
return { total: Number(data.total), counts, percentiles };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** 값이 이 이상인 것의 몫을 천분율 정수로. 정수 셈이라 두
|
|
55
|
+
/** 값이 이 이상인 것의 몫을 천분율 정수로. 정수 셈이라 두 구현의 글자가 같다. @param {Histogram} hist @param {number} value */
|
|
56
56
|
export function shareAtOrAbove(hist, value) {
|
|
57
57
|
const capped = Math.min(value, CAP);
|
|
58
58
|
let above = 0;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 쓰기 전에 읽는 작법서. 파이썬 data/prose.py 의 투영이고 정본은 koreanProse.txt 하나다.
|
|
4
|
+
* primer 는 절마다 한 줄로 펴서 첫머리에 싣고, hook --prime 은 본문을 그대로 에이전트 문맥에 넣는다.
|
|
5
|
+
*/
|
|
6
|
+
import { readText } from "./load.js";
|
|
7
|
+
|
|
8
|
+
export const PROSE = "koreanProse.txt";
|
|
9
|
+
|
|
10
|
+
/** 작법서 본문. 끝의 줄바꿈 하나까지 파일 그대로다. @returns {string} */
|
|
11
|
+
export function proseGuide() {
|
|
12
|
+
return readText(PROSE);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* (제목, 본문) 짝. 첫 짝은 문서 제목과 첫머리이고 나머지는 일곱 줄이다. 제목의 `#` 는 뗀다.
|
|
17
|
+
* 파이썬 proseSections 와 같다. @returns {[string, string][]}
|
|
18
|
+
*/
|
|
19
|
+
export function proseSections() {
|
|
20
|
+
/** @type {[string, string[]][]} */
|
|
21
|
+
const sections = [];
|
|
22
|
+
for (const line of proseGuide().split(/\r?\n/)) {
|
|
23
|
+
if (line.startsWith("#")) sections.push([line.replace(/^#+/, "").trim(), []]);
|
|
24
|
+
else if (sections.length) sections[sections.length - 1][1].push(line);
|
|
25
|
+
}
|
|
26
|
+
return sections.map(([title, body]) => [title, body.join("\n").trim()]);
|
|
27
|
+
}
|
|
@@ -4,15 +4,29 @@
|
|
|
4
4
|
* 뜻과 규칙은 파이썬 document/sourceText.py 가 소유한다. 여기는 같은 값을 낸다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export const SOURCE_SUFFIXES = [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".rs", ".py"];
|
|
7
|
+
export const SOURCE_SUFFIXES = [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".rs", ".py", ".html", ".htm", ".vue", ".svelte"];
|
|
8
|
+
/** 태그 사이 글과 속성값을 JSX 와 같은 길로 읽는 파일. HTML 주석 `<!-- -->` 을 걷어낸다. */
|
|
9
|
+
const MARKUP_SUFFIXES = [".html", ".htm", ".vue", ".svelte"];
|
|
8
10
|
|
|
9
11
|
const KOREAN = /[가-힣]/;
|
|
10
12
|
// 문자열의 경계. 줄을 앞에서부터 훑어 여는 따옴표에서 같은 닫는 따옴표까지를 한 마디로 본다. 파이썬 sourceText.py 와 같다.
|
|
11
13
|
const QUOTES = ["'", '"', "`"];
|
|
12
14
|
const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
|
|
15
|
+
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
|
|
13
16
|
const DEVELOPER_LINE = /new Error\(|console\.|panic!\(|expect\(|assert/;
|
|
14
17
|
const RUST_CONTINUATION = /\\\r?\n[ \t]*/g;
|
|
15
18
|
const RUST_TEST_MARKER = "#[cfg(test)]";
|
|
19
|
+
/** 문장을 끊지 않는 HTML 인라인 태그. 같은 줄에서 이 태그만 사이에 둔 글 조각은 한 마디로 잇는다. 파이썬 INLINE_TAGS 와 같다. */
|
|
20
|
+
const INLINE_TAGS = new Set("a abbr b bdi bdo cite code data del dfn em i ins kbd mark q s samp small span strong sub sup time u var wbr br".split(" "));
|
|
21
|
+
const INLINE_TAG = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^<>]*>/g;
|
|
22
|
+
const INLINE_GAP = /^(?:\s*<\/?[a-zA-Z][a-zA-Z0-9]*[^<>]*>)+\s*$/;
|
|
23
|
+
const SIBLING_GAP = /<\/[a-zA-Z][a-zA-Z0-9]*\s*>\s*<[a-zA-Z]/;
|
|
24
|
+
const CODE_OPEN = /<(?:script|style)\b/i;
|
|
25
|
+
const CODE_TAG = /<\/?(?:script|style)\b[^<>]*>/gi;
|
|
26
|
+
const CODE_CLOSE = /<\/(?:script|style)\s*>/i;
|
|
27
|
+
/** 마크업 파일에서 태그도 코드 기호도 없는 줄. 여러 줄 문단의 이어지는 줄이다. */
|
|
28
|
+
const PROSE_LINE = /^(?![\s]*$)[^<>=;{}]*$/;
|
|
29
|
+
const HEAD_TEXT = /^\s*[^<\s][^<]*(?=<\/)/;
|
|
16
30
|
|
|
17
31
|
/**
|
|
18
32
|
* @typedef {object} SourceLiteral
|
|
@@ -79,17 +93,36 @@ export function userFacingSource(source, path) {
|
|
|
79
93
|
if (isRust && body.includes(RUST_TEST_MARKER)) body = body.slice(0, body.indexOf(RUST_TEST_MARKER));
|
|
80
94
|
if (isRust) body = body.replace(RUST_CONTINUATION, "");
|
|
81
95
|
body = body.replace(BLOCK_COMMENT, (block) => block.replace(/[^\n]/g, " "));
|
|
96
|
+
if (MARKUP_SUFFIXES.some((suffix) => path.endsWith(suffix))) body = body.replace(HTML_COMMENT, (block) => block.replace(/[^\n]/g, " "));
|
|
82
97
|
return body
|
|
83
98
|
.split("\n")
|
|
84
99
|
.map((line) => {
|
|
85
100
|
const trimmed = line.trim();
|
|
86
101
|
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#")) return "";
|
|
87
102
|
if (DEVELOPER_LINE.test(line)) return "";
|
|
88
|
-
return line
|
|
103
|
+
return withoutLineComment(line);
|
|
89
104
|
})
|
|
90
105
|
.join("\n");
|
|
91
106
|
}
|
|
92
107
|
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 따옴표 밖의 `//` 부터 자른다. `https://` 처럼 `:` 뒤의 `//` 는 주소라 두고, 문자열 안의 `//` 도 둔다.
|
|
111
|
+
* 뜻은 파이썬 document/sourceText.py 의 withoutLineComment 가 소유한다.
|
|
112
|
+
*/
|
|
113
|
+
export function withoutLineComment(line) {
|
|
114
|
+
let quote = null;
|
|
115
|
+
for (let i = 0; i < line.length; i++) {
|
|
116
|
+
const char = line[i];
|
|
117
|
+
if (quote) {
|
|
118
|
+
if (char === "\\") { i++; continue; }
|
|
119
|
+
if (char === quote) quote = null;
|
|
120
|
+
} else if (char === "'" || char === '"' || char === "`") quote = char;
|
|
121
|
+
else if (char === "/" && line[i + 1] === "/" && (i === 0 || line[i - 1] !== ":")) return line.slice(0, i);
|
|
122
|
+
}
|
|
123
|
+
return line;
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
/** 검사에 쓰는 글. 식을 비우고 공백을 하나로 모은다. @param {string} text */
|
|
94
127
|
export function plainText(text) {
|
|
95
128
|
let plain = "";
|
|
@@ -99,6 +132,8 @@ export function plainText(text) {
|
|
|
99
132
|
cursor = end;
|
|
100
133
|
}
|
|
101
134
|
plain += text.slice(cursor);
|
|
135
|
+
// 인라인 태그를 비운다. 이름이 영문자로 시작하는 것만이라 `<대상> 필요` 의 꺾쇠는 남는다.
|
|
136
|
+
plain = plain.replace(INLINE_TAG, (tag, name) => (INLINE_TAGS.has(name.toLowerCase()) ? "" : tag));
|
|
102
137
|
return plain.replace(/\s+/g, " ").trim();
|
|
103
138
|
}
|
|
104
139
|
|
|
@@ -160,7 +195,12 @@ function innerLiterals(text, offset) {
|
|
|
160
195
|
}
|
|
161
196
|
|
|
162
197
|
/** 한 줄의 글 마디를 [시작 자리, 글] 로 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @param {number} [offset] @returns {[number, string][]} */
|
|
163
|
-
|
|
198
|
+
/**
|
|
199
|
+
* openEnded (마크업 파일) 이면 여는 태그 뒤 줄 끝까지의 글과 줄 처음부터 닫는 태그 앞까지의 글도 마디다.
|
|
200
|
+
* 뜻은 파이썬 lineLiterals 가 소유한다.
|
|
201
|
+
* @param {string} line @param {number} [offset] @param {boolean} [openEnded]
|
|
202
|
+
*/
|
|
203
|
+
export function lineLiterals(line, offset = 0, openEnded = false) {
|
|
164
204
|
/** @type {[number, string][]} */
|
|
165
205
|
const found = [];
|
|
166
206
|
let index = 0;
|
|
@@ -176,7 +216,8 @@ export function lineLiterals(line, offset = 0) {
|
|
|
176
216
|
continue;
|
|
177
217
|
}
|
|
178
218
|
if (char === ">" && tagCloses(line, index)) {
|
|
179
|
-
|
|
219
|
+
let end = jsxTextEnd(line, index + 1);
|
|
220
|
+
if (end < 0 && openEnded && index + 1 < line.length) end = line.length;
|
|
180
221
|
if (end > index) {
|
|
181
222
|
const raw = line.slice(index + 1, end);
|
|
182
223
|
found.push([offset + index + 1, raw]);
|
|
@@ -187,16 +228,73 @@ export function lineLiterals(line, offset = 0) {
|
|
|
187
228
|
}
|
|
188
229
|
index += 1;
|
|
189
230
|
}
|
|
231
|
+
if (openEnded) {
|
|
232
|
+
const head = HEAD_TEXT.exec(line);
|
|
233
|
+
if (head && !found.some(([start]) => start < head[0].length)) found.push([offset, head[0]]);
|
|
234
|
+
else if (!found.length && PROSE_LINE.test(line)) found.push([offset, line]);
|
|
235
|
+
}
|
|
190
236
|
found.sort((a, b) => a[0] - b[0]);
|
|
191
|
-
return found;
|
|
237
|
+
return joinInline(line, found, offset);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 사이가 인라인 태그뿐인 태그 사이 글 조각을 한 마디로 잇는다. 태그 안의 속성 문자열은 잇기에 끼지 않고 제 마디로 남는다.
|
|
242
|
+
* 뜻은 파이썬 joinInline 이 소유한다.
|
|
243
|
+
* @param {string} line @param {[number, string][]} found @param {number} offset @returns {[number, string][]}
|
|
244
|
+
*/
|
|
245
|
+
function joinInline(line, found, offset) {
|
|
246
|
+
const pieces = found.filter(([, raw]) => raw.trim());
|
|
247
|
+
const attributes = pieces.filter(([start]) => insideTag(line, start - offset));
|
|
248
|
+
/** @type {[number, string][]} */
|
|
249
|
+
const joined = [];
|
|
250
|
+
for (const piece of pieces) {
|
|
251
|
+
if (attributes.includes(piece)) continue;
|
|
252
|
+
const [start, raw] = piece;
|
|
253
|
+
if (joined.length) {
|
|
254
|
+
const [previousStart, previousRaw] = joined[joined.length - 1];
|
|
255
|
+
const tail = previousStart - offset + previousRaw.length;
|
|
256
|
+
if (start - offset >= tail && inlineOnly(line.slice(tail, start - offset))) {
|
|
257
|
+
joined[joined.length - 1] = [previousStart, line.slice(previousStart - offset, start - offset + raw.length)];
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
joined.push([start, raw]);
|
|
262
|
+
}
|
|
263
|
+
return [...joined, ...attributes].sort((a, b) => a[0] - b[0]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** position 이 태그 안 (`<` 뒤, `>` 앞) 인가. @param {string} line @param {number} position */
|
|
267
|
+
function insideTag(line, position) {
|
|
268
|
+
const opened = line.lastIndexOf("<", position - 1);
|
|
269
|
+
return opened >= 0 && line.lastIndexOf(">", position - 1) < opened;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 사이가 인라인 태그뿐이고 형제 (`</a><a>`) 가 아니다. @param {string} gap */
|
|
273
|
+
function inlineOnly(gap) {
|
|
274
|
+
if (!INLINE_GAP.test(gap) || SIBLING_GAP.test(gap)) return false;
|
|
275
|
+
return [...gap.matchAll(INLINE_TAG)].every((match) => INLINE_TAGS.has(match[1].toLowerCase()));
|
|
192
276
|
}
|
|
193
277
|
|
|
194
278
|
/** 소스에서 글 마디를 줄 번호 순으로. @param {string} source @param {string} [path] @returns {SourceLiteral[]} */
|
|
195
279
|
export function sourceLiterals(source, path = "") {
|
|
196
280
|
/** @type {SourceLiteral[]} */
|
|
197
281
|
const found = [];
|
|
282
|
+
const markup = MARKUP_SUFFIXES.some((suffix) => path.endsWith(suffix));
|
|
283
|
+
// 마크업은 주석 지우기가 줄 수를 지키므로 원문 줄과 나란히 간다. script 블록 판정은 원문 줄로 한다 (`// 주석</script>` 가 지워져도).
|
|
284
|
+
const rawLines = markup ? source.split("\n") : [];
|
|
285
|
+
let inCode = false;
|
|
198
286
|
userFacingSource(source, path).split("\n").forEach((line, index) => {
|
|
199
|
-
|
|
287
|
+
// <script> 와 <style> 안은 코드다. 열린 조각 규칙을 끄고 따옴표 문자열만 잡는다. 태그 자체는 빈칸으로 바꿔 자리를 지킨다.
|
|
288
|
+
const rawLine = markup && index < rawLines.length ? rawLines[index] : line;
|
|
289
|
+
const opensCode = markup && CODE_OPEN.test(rawLine);
|
|
290
|
+
const closesCode = markup && CODE_CLOSE.test(rawLine);
|
|
291
|
+
const openEnded = markup && !inCode && !opensCode;
|
|
292
|
+
if (markup) {
|
|
293
|
+
if (opensCode && !closesCode) inCode = true;
|
|
294
|
+
else if (inCode && closesCode) inCode = false;
|
|
295
|
+
}
|
|
296
|
+
const scanned = opensCode || closesCode ? line.replace(CODE_TAG, (tag) => " ".repeat(tag.length)) : line;
|
|
297
|
+
for (const [start, raw] of lineLiterals(scanned, 0, openEnded)) {
|
|
200
298
|
const text = raw.trim();
|
|
201
299
|
const plain = plainText(text);
|
|
202
300
|
if (text && KOREAN.test(plain)) found.push({ line: index + 1, text, plain, column: start + (raw.length - raw.trimStart().length) + 1 });
|
package/src/fingerprint/build.js
CHANGED
|
@@ -130,11 +130,15 @@ function quotedIn(sentence, blockSpans) {
|
|
|
130
130
|
function makeSentencePrint(build, text, line, block, sectionIndex, quoted) {
|
|
131
131
|
const { entries } = build;
|
|
132
132
|
const ending = markers.endingOf(text);
|
|
133
|
+
const protectedSpans = markers.termSpans(text, build.protectedTerms);
|
|
134
|
+
// 인용 안의 것과 보호한 용어에 걸친 것은 뺀다. 뜻은 파이썬 fingerprint/build.py 의 kept 가 정한다.
|
|
135
|
+
/** @param {number} start @param {number} end */
|
|
136
|
+
const kept = (start, end) => !markers.insideAny(start, end, quoted) && !markers.overlapsAny(start, end, protectedSpans);
|
|
133
137
|
const deixis = markers
|
|
134
138
|
.matchedSpans(text, "deixis.txt")
|
|
135
|
-
.filter(([start, end]) =>
|
|
139
|
+
.filter(([start, end]) => kept(start, end))
|
|
136
140
|
.map(([, , found]) => found);
|
|
137
|
-
const matches = matchesIn(text, entries).filter((m) =>
|
|
141
|
+
const matches = matchesIn(text, entries).filter((m) => kept(m.start, m.end));
|
|
138
142
|
const mood = markers.moodOf(text, ending);
|
|
139
143
|
return {
|
|
140
144
|
index: build.sentences.length,
|
|
@@ -154,7 +158,7 @@ function makeSentencePrint(build, text, line, block, sectionIndex, quoted) {
|
|
|
154
158
|
euiCount: euiCount(text),
|
|
155
159
|
nounRun: longestNounRun(text),
|
|
156
160
|
passives: doublePassiveSpans(text)
|
|
157
|
-
.filter(([start, end]) =>
|
|
161
|
+
.filter(([start, end]) => kept(start, end))
|
|
158
162
|
.map(([, , surface]) => surface),
|
|
159
163
|
hedges: markers.countMatches(text, "hedges.txt"),
|
|
160
164
|
numbers: markers.countNumbers(text),
|
|
@@ -267,7 +271,12 @@ function buildSection(build, section, sectionIndex) {
|
|
|
267
271
|
export function buildFingerprint(doc, config = defaultConfig()) {
|
|
268
272
|
// 코드도 산문도 아닌 펜스는 지문에 들어오기 전에 뺀다. 모든 진입점이 여기를 지나므로 한 자리면 된다.
|
|
269
273
|
doc = dropFences(doc, config.ignoreFences);
|
|
270
|
-
const build = {
|
|
274
|
+
const build = {
|
|
275
|
+
entries: entriesFor(config),
|
|
276
|
+
protectedTerms: config.protectedTerms ?? [],
|
|
277
|
+
sentences: /** @type {SentencePrint[]} */ ([]),
|
|
278
|
+
paragraphs: /** @type {ParagraphPrint[]} */ ([]),
|
|
279
|
+
};
|
|
271
280
|
const { sentences, paragraphs } = build;
|
|
272
281
|
const sections = doc.sections.map((section, index) =>
|
|
273
282
|
buildSection(build, section, index),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/**
|
|
2
|
+
/** 사전들을 한 번 컴파일하고 문장에서 맞는 자리를 찾는다. 설정의 dictionary 항목을 더한다. */
|
|
3
3
|
import { loadEntries } from "../data/load.js";
|
|
4
4
|
import { compile } from "../regex.js";
|
|
5
5
|
|
|
@@ -16,6 +16,7 @@ export const DICTIONARY_FILES = {
|
|
|
16
16
|
screenNarration: "screenNarration.toml",
|
|
17
17
|
screenTone: "screenTone.toml",
|
|
18
18
|
screenWord: "screenWord.toml",
|
|
19
|
+
calqueFrame: "calqueFrame.toml",
|
|
19
20
|
};
|
|
20
21
|
const GROUP_REF = /\$(\d)/g;
|
|
21
22
|
const FINALS = { ㄴ: 4, ㄹ: 8 };
|
|
@@ -44,6 +45,9 @@ export function expandClasses(pattern) {
|
|
|
44
45
|
* @property {string} why
|
|
45
46
|
* @property {string} source
|
|
46
47
|
* @property {string | null} fix
|
|
48
|
+
* @property {{ pattern: import("../regex.js").Pattern, into: string }[]} to 문장 전체를 다시 쓰는 규칙. 앞의 것부터 시도한다
|
|
49
|
+
* @property {string} raw pattern 의 원문. 내장 항목만 갖고 설정 항목은 빈 글자라 관용으로 접히지 않는다
|
|
50
|
+
* @property {import("../regex.js").Pattern | null} unless 문장 전체에서 이것이 맞으면 그 문장에서는 항목이 맞지 않는다. 파이썬 Entry.unless 와 같다
|
|
47
51
|
*/
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -55,10 +59,12 @@ export function expandClasses(pattern) {
|
|
|
55
59
|
* @property {string} why
|
|
56
60
|
* @property {string} source
|
|
57
61
|
* @property {string | null} fix
|
|
62
|
+
* @property {string | null} rewrite 항목의 to 규칙으로 문장 전체를 다시 쓴 제안. 없거나 안 맞으면 null
|
|
63
|
+
* @property {string} pattern 맞은 항목의 pattern 원문. 용례 빈도표의 키다
|
|
58
64
|
*/
|
|
59
65
|
|
|
60
|
-
/** @param {string} dictionary @param {Record<string, unknown> | string} raw @returns {Entry} */
|
|
61
|
-
export function entryFrom(dictionary, raw) {
|
|
66
|
+
/** @param {string} dictionary @param {Record<string, unknown> | string} raw @param {boolean} [builtin] @returns {Entry} */
|
|
67
|
+
export function entryFrom(dictionary, raw, builtin = false) {
|
|
62
68
|
const data = typeof raw === "string" ? { pattern: raw } : raw;
|
|
63
69
|
return {
|
|
64
70
|
dictionary,
|
|
@@ -66,6 +72,9 @@ export function entryFrom(dictionary, raw) {
|
|
|
66
72
|
why: /** @type {string} */ (data.why ?? "설정에서 더한 항목"),
|
|
67
73
|
source: /** @type {string} */ (data.source ?? "설정"),
|
|
68
74
|
fix: /** @type {string | null} */ (data.fix ?? null),
|
|
75
|
+
to: (/** @type {[string, string][]} */ (data.to ?? [])).map(([source, into]) => ({ pattern: compile(expandClasses(source)), into })),
|
|
76
|
+
raw: builtin ? /** @type {string} */ (data.pattern) : "",
|
|
77
|
+
unless: data.unless ? compile(expandClasses(/** @type {string} */ (data.unless))) : null,
|
|
69
78
|
};
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -75,7 +84,7 @@ export function builtinEntries() {
|
|
|
75
84
|
if (!builtinCache) {
|
|
76
85
|
builtinCache = [];
|
|
77
86
|
for (const [dictionary, name] of Object.entries(DICTIONARY_FILES)) {
|
|
78
|
-
for (const raw of loadEntries(name)) builtinCache.push(entryFrom(dictionary, raw));
|
|
87
|
+
for (const raw of loadEntries(name)) builtinCache.push(entryFrom(dictionary, raw, true));
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
return builtinCache;
|
|
@@ -99,11 +108,27 @@ export function applyFix(match, fix) {
|
|
|
99
108
|
return fix.replace(GROUP_REF, (_, n) => match[Number(n)] ?? "");
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/**
|
|
112
|
+
* to 규칙으로 문장을 다시 쓴다. 빈 그룹이 남긴 겹 공백은 하나로 줄이고 양끝을 다듬는다. 원문과 같으면 null.
|
|
113
|
+
* 뜻은 파이썬 fingerprint/dictionaries.py 의 rewriteBy 가 소유한다.
|
|
114
|
+
* @param {string} text @param {Entry["to"]} rules
|
|
115
|
+
*/
|
|
116
|
+
export function rewriteBy(text, rules) {
|
|
117
|
+
for (const { pattern, into } of rules) {
|
|
118
|
+
const match = pattern.search(text);
|
|
119
|
+
if (!match) continue;
|
|
120
|
+
const rewritten = applyFix(match, into).split(/\s+/).filter(Boolean).join(" ");
|
|
121
|
+
return rewritten && rewritten !== text ? rewritten : null;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
/** @param {string} text @param {Entry[]} entries @returns {DictionaryMatch[]} */
|
|
103
127
|
export function matchesIn(text, entries) {
|
|
104
128
|
/** @type {DictionaryMatch[]} */
|
|
105
129
|
const found = [];
|
|
106
130
|
for (const entry of entries) {
|
|
131
|
+
if (entry.unless && entry.unless.search(text)) continue;
|
|
107
132
|
for (const match of entry.pattern.all(text)) {
|
|
108
133
|
const start = /** @type {number} */ (match.index);
|
|
109
134
|
found.push({
|
|
@@ -114,6 +139,8 @@ export function matchesIn(text, entries) {
|
|
|
114
139
|
why: entry.why,
|
|
115
140
|
source: entry.source,
|
|
116
141
|
fix: entry.fix ? applyFix(match, entry.fix) : null,
|
|
142
|
+
rewrite: entry.to.length ? rewriteBy(text, entry.to) : null,
|
|
143
|
+
pattern: entry.raw,
|
|
117
144
|
});
|
|
118
145
|
}
|
|
119
146
|
}
|
|
@@ -147,6 +147,28 @@ export function quoteSpans(text) {
|
|
|
147
147
|
return found.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 설정의 protectedTerms 가 글에 나온 [시작, 끝] 전부. 겹쳐 나온 자리도 따로 센다. 파이썬 termSpans 와 같다.
|
|
152
|
+
* @param {string} text @param {string[]} terms @returns {[number, number][]}
|
|
153
|
+
*/
|
|
154
|
+
export function termSpans(text, terms) {
|
|
155
|
+
/** @type {[number, number][]} */
|
|
156
|
+
const found = [];
|
|
157
|
+
for (const term of terms) {
|
|
158
|
+
let start = text.indexOf(term);
|
|
159
|
+
while (start !== -1) {
|
|
160
|
+
found.push([start, start + term.length]);
|
|
161
|
+
start = text.indexOf(term, start + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return found.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 두 구간이 한 글자라도 겹치는가. 파이썬 overlapsAny 와 같다. @param {number} start @param {number} end @param {[number, number][]} spans */
|
|
168
|
+
export function overlapsAny(start, end, spans) {
|
|
169
|
+
return spans.some(([spanStart, spanEnd]) => spanStart < end && start < spanEnd);
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
/** @param {number} start @param {number} end @param {[number, number][]} spans */
|
|
151
173
|
export function insideAny(start, end, spans) {
|
|
152
174
|
return spans.some(([spanStart, spanEnd]) => spanStart <= start && end <= spanEnd);
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { configFromMapping, defaultConfig } from "./config/settings.js";
|
|
|
26
26
|
import { loadVersion } from "./data/load.js";
|
|
27
27
|
import { parseMarkdown } from "./document/parseMarkdown.js";
|
|
28
28
|
import { SOURCE_SUFFIXES, replaceLiteral, sourceLiterals } from "./document/sourceText.js";
|
|
29
|
-
import { parseSheet, renderSheet, renderSheetJson } from "./report/sheet.js";
|
|
29
|
+
import { applyRows, parseSheet, renderSheet, renderSheetJson, sheetRows } from "./report/sheet.js";
|
|
30
30
|
import { headingsOf } from "./document/model.js";
|
|
31
31
|
import { applyFixes } from "./edit/applyFixes.js";
|
|
32
32
|
import { buildFingerprint } from "./fingerprint/build.js";
|
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
parseSheet,
|
|
55
55
|
renderSheet,
|
|
56
56
|
renderSheetJson,
|
|
57
|
+
sheetRows,
|
|
58
|
+
applyRows,
|
|
57
59
|
replaceLiteral,
|
|
58
60
|
sourceLiterals,
|
|
59
61
|
CONTRACT_VERSION,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 한 줄에 지적 하나. AI 와 스크립트가 읽는
|
|
2
|
+
/** 한 줄에 지적 하나. AI 와 스크립트가 읽는 형식이다. 파이썬 report/compactReport.py 와 같은 글자다. */
|
|
3
3
|
|
|
4
|
-
/** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이
|
|
4
|
+
/** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 형식의 계약이다. @param {string} text */
|
|
5
5
|
function flat(text) {
|
|
6
6
|
return text.split(/\s+/).filter(Boolean).join(" ");
|
|
7
7
|
}
|
|
@@ -9,6 +9,10 @@ function flat(text) {
|
|
|
9
9
|
/** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
|
|
10
10
|
export function renderCompact(path, findings) {
|
|
11
11
|
return findings
|
|
12
|
-
.map((f) =>
|
|
12
|
+
.map((f) => {
|
|
13
|
+
const label = f.severity === "notice" ? "notice: " : "";
|
|
14
|
+
const fixLabel = f.severity === "notice" ? "검토할 대안" : "고친 뒤";
|
|
15
|
+
return `${path}:${f.line} [${f.rule}] ${label}${flat(f.why)}` + (f.fix ? ` ${fixLabel}: ${flat(f.fix)}` : "");
|
|
16
|
+
})
|
|
13
17
|
.join("\n");
|
|
14
18
|
}
|
package/src/report/jsonReport.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 기계가 읽는
|
|
3
|
+
* 기계가 읽는 형식. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False) 와 같은 글자다.
|
|
4
4
|
* 지적마다 `exemplar` 를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
|
|
5
|
-
* 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory
|
|
5
|
+
* 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 실험 스크립트에서 따로 잰다.
|
|
6
6
|
*/
|
|
7
7
|
import { exemplarFor } from "../data/exemplars.js";
|
|
8
8
|
import { findingAsDict } from "../rules/finding.js";
|
package/src/report/sheet.js
CHANGED
|
Binary file
|
package/src/report/textReport.js
CHANGED
|
@@ -35,21 +35,40 @@ function candidateLines(findings) {
|
|
|
35
35
|
return lines;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** 접은 확인 권고. 규칙마다 개수와 줄 번호 한 줄. 뜻은 파이썬 textReport.py 의 noticeLines 가 소유한다. @param {import("../rules/finding.js").Finding[]} notices */
|
|
39
|
+
function noticeLines(notices) {
|
|
40
|
+
/** @type {Map<string, number[]>} */
|
|
41
|
+
const byRule = new Map();
|
|
42
|
+
for (const finding of notices) {
|
|
43
|
+
if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
|
|
44
|
+
/** @type {number[]} */ (byRule.get(finding.rule)).push(finding.line);
|
|
45
|
+
}
|
|
46
|
+
const lines = [`확인 권고 ${notices.length}건 (접음. 다 보려면 --notices)`];
|
|
47
|
+
for (const rule of [...byRule.keys()].sort()) {
|
|
48
|
+
const numbers = [...new Set(/** @type {number[]} */ (byRule.get(rule)))].sort((a, b) => a - b);
|
|
49
|
+
lines.push(` ${rule} ${/** @type {number[]} */ (byRule.get(rule)).length} ${numbers.join(", ")}줄`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] @param {boolean} [unfoldNotices] */
|
|
55
|
+
export function renderText(path, findings, register = null, preset = null, customExemplars = [], unfoldNotices = false) {
|
|
56
|
+
if (!findings.length) return `${path} 오류 없음`;
|
|
41
57
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
42
58
|
const notices = findings.length - errors;
|
|
43
|
-
const summary = `${path}
|
|
59
|
+
const summary = `${path} 오류 ${errors}건` + (notices ? `, 확인 권고 ${notices}건` : "");
|
|
44
60
|
const lines = [summary, ""];
|
|
45
|
-
|
|
61
|
+
const folded = unfoldNotices ? [] : findings.filter((f) => f.severity === "notice");
|
|
62
|
+
const shown = findings.filter((f) => unfoldNotices || f.severity === "error");
|
|
63
|
+
for (const f of shown) {
|
|
46
64
|
const tag = `[${f.rule}]` + (f.severity === "notice" ? " 확인" : "");
|
|
47
65
|
lines.push(`${path}:${f.line} ${tag}`, ` ${f.quote}`, ` ${f.why}`);
|
|
48
|
-
if (f.fix) lines.push(` 고친
|
|
66
|
+
if (f.fix) lines.push(` ${f.severity === "notice" ? "검토할 대안" : "고친 뒤"}: ${f.fix}`);
|
|
49
67
|
lines.push("");
|
|
50
68
|
}
|
|
51
|
-
lines.push(...
|
|
52
|
-
|
|
69
|
+
if (folded.length) lines.push(...noticeLines(folded), "");
|
|
70
|
+
lines.push(...exemplarLines(shown, register, preset, customExemplars));
|
|
71
|
+
const candidates = candidateLines(shown);
|
|
53
72
|
if (candidates.length) lines.push("", ...candidates);
|
|
54
73
|
return lines.join("\n").replace(/\n+$/, "");
|
|
55
74
|
}
|
|
@@ -34,7 +34,7 @@ function selectedProfile(config) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* 현재
|
|
37
|
+
* 현재 규칙 세트를 쓰기 전 숫자 사양으로 편다. 품질 점수나 새 임계는 만들지 않는다.
|
|
38
38
|
* @param {import("./settings.js").Config} [config]
|
|
39
39
|
* @param {string} [register]
|
|
40
40
|
* @param {number | null} [targetChars]
|
|
@@ -161,11 +161,11 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** 사람이 읽는
|
|
164
|
+
/** 사람이 읽는 형식. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
|
|
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}`);
|
package/src/rules/registry.js
CHANGED
|
@@ -24,9 +24,11 @@ import * as spacing from "./orthography/spacing.js";
|
|
|
24
24
|
import * as spelling from "./orthography/spelling.js";
|
|
25
25
|
import * as factListParagraph from "./paragraph/factListParagraph.js";
|
|
26
26
|
import * as paraFragment from "./paragraph/paraFragment.js";
|
|
27
|
+
import * as calqueFrame from "./sentence/calqueFrame.js";
|
|
27
28
|
import * as cliche from "./sentence/cliche.js";
|
|
28
29
|
import * as hardWord from "./sentence/hardWord.js";
|
|
29
30
|
import * as connectorRepeat from "./sentence/connectorRepeat.js";
|
|
31
|
+
import * as phraseRepeat from "./sentence/phraseRepeat.js";
|
|
30
32
|
import * as danglingDeixis from "./sentence/danglingDeixis.js";
|
|
31
33
|
import * as draftHistory from "./sentence/draftHistory.js";
|
|
32
34
|
import * as numberOrphan from "./sentence/numberOrphan.js";
|
|
@@ -70,8 +72,10 @@ import * as screenWord from "./screen/screenWord.js";
|
|
|
70
72
|
|
|
71
73
|
/** @type {Rule[]} */
|
|
72
74
|
export const RULES = [
|
|
75
|
+
calqueFrame,
|
|
73
76
|
cliche,
|
|
74
77
|
connectorRepeat,
|
|
78
|
+
phraseRepeat,
|
|
75
79
|
danglingDeixis,
|
|
76
80
|
dash,
|
|
77
81
|
draftHistory,
|
|
@@ -133,7 +137,7 @@ export const RULES = [
|
|
|
133
137
|
export const MECHANISMS = {
|
|
134
138
|
dictionary: "사전. 낱말 목록과 정규식이 맞는 자리",
|
|
135
139
|
repeat: "반복. 같은 모양이 창 안에서 N 번",
|
|
136
|
-
threshold: "셈.
|
|
140
|
+
threshold: "셈. 분석표의 값이 임계를 넘거나 모양이 계약과 다른 자리",
|
|
137
141
|
contrast: "대조. 두 자리를 맞대 어긋난 곳",
|
|
138
142
|
reader: "독자 상태. 문장 순서대로 독자가 손에 든 것과 본 것에 그 자리가 요구하는 것을 맞댄다",
|
|
139
143
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
3
4
|
|
|
4
5
|
export const name = "screenWord";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
8
|
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
9
|
export function run(doc) {
|
|
9
|
-
return overridingFindings(doc, "screenWord", name);
|
|
10
|
+
return overridingFindings(doc, "screenWord", name, undefined, NOTICE);
|
|
10
11
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { overridingFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
import { NOTICE } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "calqueFrame";
|
|
6
|
+
export const mechanism = "dictionary";
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
return overridingFindings(doc, "calqueFrame", name, undefined, NOTICE);
|
|
11
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { ERROR } from "../finding.js";
|
|
2
3
|
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
4
|
|
|
4
5
|
export const name = "cliche";
|
|
5
6
|
export const mechanism = "dictionary";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
return dictionaryFindings(doc, "cliches", name, ERROR, config);
|
|
10
14
|
}
|