hanlint 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -20
- package/data/calqueFrame.json +60 -0
- package/data/cliches.json +39 -29
- package/data/collocationStops.txt +28 -0
- package/data/confusable.json +20 -20
- package/data/easyWords.json +12 -246
- package/data/exemplars.json +50 -29
- package/data/hiddenDeps.txt +11 -11
- package/data/japaneseLoan.json +27 -27
- package/data/koreanProse.txt +64 -0
- package/data/patterns.json +26 -26
- package/data/phraseRepeat.json +11 -0
- package/data/platformApis.txt +11 -11
- package/data/redundantPair.json +16 -16
- package/data/ruleCategories.json +2 -0
- package/data/ruleDocs.json +56 -54
- package/data/ruleMechanisms.json +2 -0
- package/data/screenNarration.json +10 -10
- package/data/screenSentence.json +60 -12
- package/data/screenTone.json +6 -6
- package/data/screenWord.json +70 -110
- package/data/spacing.json +12 -12
- package/data/spelling.json +29 -29
- package/data/translationese.json +23 -24
- package/data/usageCounts.report.json +1676 -0
- package/data/version.json +1 -1
- package/package.json +1 -1
- package/src/analysis/grammar/ending.js +1 -1
- package/src/analysis/grammar/hangul.js +4 -4
- package/src/analysis/grammar/josa.js +1 -1
- package/src/analysis/grammar/register.js +1 -1
- package/src/analysis/grammar/voice.js +1 -1
- package/src/analysis/index.js +3 -3
- package/src/analysis/josa.js +1 -1
- package/src/analysis/splitSentences.js +1 -1
- package/src/analysis/tokenize.js +41 -26
- package/src/baseline/store.js +8 -8
- package/src/cli/help.js +11 -10
- package/src/cli/main.js +332 -182
- package/src/cli/walk.js +2 -2
- package/src/cli/welcome.js +8 -8
- package/src/config/loadConfig.js +3 -3
- package/src/config/patch.js +6 -6
- package/src/config/readerContract.js +26 -26
- package/src/config/settings.js +44 -25
- package/src/config/toml.js +9 -9
- package/src/data/exemplars.js +21 -21
- package/src/data/load.js +6 -6
- package/src/data/operations.js +14 -14
- package/src/data/patches.js +10 -10
- package/src/data/patterns.js +5 -5
- package/src/data/profiles.js +6 -6
- package/src/data/prose.js +27 -0
- package/src/document/model.js +4 -4
- package/src/document/parseMarkdown.js +7 -7
- package/src/document/plainText.js +3 -3
- package/src/document/sourceText.js +117 -19
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +18 -9
- package/src/fingerprint/codeBlocks.js +3 -3
- package/src/fingerprint/codeMarkers.js +4 -4
- package/src/fingerprint/dictionaries.js +34 -7
- package/src/fingerprint/markers.js +27 -5
- package/src/fingerprint/readerState.js +9 -9
- package/src/fingerprint/sourceSentence.js +1 -1
- package/src/fingerprint/topics.js +1 -1
- package/src/guard/contract.js +6 -6
- package/src/guard/editLocality.js +1 -1
- package/src/guard/outline.js +1 -1
- package/src/guard/receipt.js +13 -13
- package/src/guard/surface.js +4 -4
- package/src/index.js +9 -7
- package/src/learn/edits.js +2 -2
- package/src/learn/operations.js +1 -1
- package/src/regex.js +2 -2
- package/src/report/compactReport.js +7 -3
- package/src/report/fingerprintJson.js +2 -2
- package/src/report/githubReport.js +1 -1
- package/src/report/jsonReport.js +3 -3
- package/src/report/registerMatch.js +2 -2
- package/src/report/sheet.js +0 -0
- package/src/report/textReport.js +35 -16
- package/src/report/writingSpec.js +17 -17
- package/src/rules/code/inputFileSource.js +1 -1
- package/src/rules/code/installImport.js +2 -2
- package/src/rules/code/platformApi.js +1 -1
- package/src/rules/document/countMismatch.js +2 -1
- package/src/rules/document/enoughOnce.js +2 -1
- package/src/rules/document/fieldEcho.js +10 -6
- package/src/rules/document/keywordHeading.js +1 -1
- package/src/rules/document/keywordMissing.js +1 -1
- package/src/rules/document/noQuestion.js +2 -2
- package/src/rules/document/promiseRecall.js +1 -1
- package/src/rules/document/tableOddCell.js +1 -1
- package/src/rules/finding.js +4 -4
- package/src/rules/paragraph/factListParagraph.js +1 -1
- package/src/rules/paragraph/paraFragment.js +1 -1
- package/src/rules/readerDebt.js +1 -1
- package/src/rules/registry.js +30 -26
- package/src/rules/screen/screenWord.js +2 -1
- package/src/rules/sentence/calqueFrame.js +11 -0
- package/src/rules/sentence/cliche.js +7 -3
- package/src/rules/sentence/connectorRepeat.js +2 -1
- package/src/rules/sentence/danglingDeixis.js +1 -1
- package/src/rules/sentence/dash.js +1 -1
- package/src/rules/sentence/deixis.js +1 -1
- package/src/rules/sentence/doubleNegative.js +2 -1
- package/src/rules/sentence/doublePassive.js +2 -1
- package/src/rules/sentence/draftHistory.js +2 -1
- package/src/rules/sentence/endingRepeat.js +3 -2
- package/src/rules/sentence/euiChain.js +1 -1
- package/src/rules/sentence/fillerOpener.js +1 -1
- package/src/rules/sentence/hardWord.js +6 -3
- package/src/rules/sentence/imperativePeriod.js +1 -1
- package/src/rules/sentence/japaneseLoan.js +7 -3
- package/src/rules/sentence/longSentence.js +1 -1
- package/src/rules/sentence/negationRedefine.js +1 -1
- package/src/rules/sentence/nounPile.js +10 -4
- package/src/rules/sentence/numberOrphan.js +1 -1
- package/src/rules/sentence/outsideProfile.js +8 -3
- package/src/rules/sentence/phraseRepeat.js +54 -0
- package/src/rules/sentence/redundantPair.js +7 -3
- package/src/rules/sentence/translationese.js +7 -3
- package/src/rules/shared/candidates.js +2 -2
- package/src/rules/shared/dictionaryRule.js +46 -8
- package/src/rules/shared/repeat.js +2 -2
- package/src/rules/structure/bridgeRepeat.js +3 -3
- package/src/rules/structure/emojiBullet.js +1 -1
- package/src/rules/structure/headingSkip.js +1 -1
- package/src/rules/structure/headingUniform.js +2 -1
- package/src/rules/structure/loneSubheading.js +1 -1
- package/src/rules/structure/moreLater.js +1 -1
- package/src/text.js +2 -2
- package/src/usage/counts.js +54 -0
- package/src/usage/index.js +8 -0
- package/src/usage/sentences.js +681 -0
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* 소스 파일의 글 마디. 사람이 읽는 한국어 문자열 리터럴과 JSX 글을 줄 번호와 함께 뽑는다.
|
|
4
|
-
* 뜻과 규칙은 파이썬 document/sourceText.py
|
|
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
|
-
// 문자열의 경계. 줄을 앞에서부터 훑어 여는 따옴표에서 같은 닫는 따옴표까지를 한 마디로 본다. 파이썬 sourceText.py
|
|
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
|
|
19
33
|
* @property {number} line 1부터 세는 줄 번호. 여러 줄을 이은 러스트 문자열은 시작 줄이다
|
|
20
34
|
* @property {string} text 따옴표와 태그를 뺀 글. 파일에 있는 그대로라 되돌려 쓸 때 찾을 수 있다
|
|
21
|
-
* @property {string} plain 검사에 쓰는 글. 식
|
|
22
|
-
* @property {number} column 1부터 세는 칸. 그 줄에서 text
|
|
35
|
+
* @property {string} plain 검사에 쓰는 글. 식(`${...}`, `{...}`)을 비우고 공백을 하나로 모았다
|
|
36
|
+
* @property {number} column 1부터 세는 칸. 그 줄에서 text가 시작하는 위치
|
|
23
37
|
*/
|
|
24
38
|
|
|
25
|
-
/** `start
|
|
39
|
+
/** `start`의 `{`를 닫는 `}`의 위치. 뜻은 파이썬 balancedEnd가 정한다. @param {string} line @param {number} start */
|
|
26
40
|
function balancedEnd(line, start) {
|
|
27
41
|
let depth = 0;
|
|
28
42
|
let index = start;
|
|
@@ -44,7 +58,7 @@ function balancedEnd(line, start) {
|
|
|
44
58
|
return -1;
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
/** 글에 낀 식 `{...}
|
|
61
|
+
/** 글에 낀 식 `{...}`와 `${...}`의 [시작, 끝 다음]. 뜻은 파이썬 expressionSpans가 정한다. @param {string} text @returns {[number, number][]} */
|
|
48
62
|
export function expressionSpans(text) {
|
|
49
63
|
/** @type {[number, number][]} */
|
|
50
64
|
const spans = [];
|
|
@@ -63,7 +77,7 @@ export function expressionSpans(text) {
|
|
|
63
77
|
return spans;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
/** 템플릿 리터럴의 `${ ... }` 식을 같은 길이의 빈칸으로 바꾼다. 뜻은 파이썬 withoutTemplateExpressions
|
|
80
|
+
/** 템플릿 리터럴의 `${ ... }` 식을 같은 길이의 빈칸으로 바꾼다. 뜻은 파이썬 withoutTemplateExpressions가 정한다. @param {string} source */
|
|
67
81
|
export function withoutTemplateExpressions(source) {
|
|
68
82
|
let output = source;
|
|
69
83
|
for (const [start, end] of expressionSpans(source)) {
|
|
@@ -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,10 +132,12 @@ 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
|
|
|
105
|
-
/** `start
|
|
140
|
+
/** `start`의 따옴표를 닫는 위치. 백틱 안의 `${...}`은 통째로 건너뛴다. 뜻은 파이썬 closingQuote가 정한다. @param {string} line @param {number} start */
|
|
106
141
|
function closingQuote(line, start) {
|
|
107
142
|
const quote = line[start];
|
|
108
143
|
let index = start + 1;
|
|
@@ -124,14 +159,14 @@ function closingQuote(line, start) {
|
|
|
124
159
|
return -1;
|
|
125
160
|
}
|
|
126
161
|
|
|
127
|
-
/** `index
|
|
162
|
+
/** `index`의 `>`가 JSX 여는 태그의 끝인가. 뜻은 파이썬 tagCloses가 정한다. @param {string} line @param {number} index */
|
|
128
163
|
function tagCloses(line, index) {
|
|
129
164
|
if (index === 0 || line[index + 1] === "=") return false;
|
|
130
165
|
const before = line[index - 1];
|
|
131
166
|
return "\"'}".includes(before) || /[A-Za-z0-9]/.test(before);
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
/** `start
|
|
169
|
+
/** `start`부터의 JSX 글이 끝나는 `<`의 위치. 식 안의 `<`는 글의 끝이 아니다. 뜻은 파이썬 jsxTextEnd가 정한다. @param {string} line @param {number} start */
|
|
135
170
|
function jsxTextEnd(line, start) {
|
|
136
171
|
let index = start;
|
|
137
172
|
while (index < line.length) {
|
|
@@ -148,7 +183,7 @@ function jsxTextEnd(line, start) {
|
|
|
148
183
|
return -1;
|
|
149
184
|
}
|
|
150
185
|
|
|
151
|
-
/** 글에 낀 식 안의 글 마디. 뜻은 파이썬 innerLiterals
|
|
186
|
+
/** 글에 낀 식 안의 글 마디. 뜻은 파이썬 innerLiterals가 정한다. @param {string} text @param {number} offset @returns {[number, string][]} */
|
|
152
187
|
function innerLiterals(text, offset) {
|
|
153
188
|
/** @type {[number, string][]} */
|
|
154
189
|
const found = [];
|
|
@@ -159,8 +194,13 @@ function innerLiterals(text, offset) {
|
|
|
159
194
|
return found;
|
|
160
195
|
}
|
|
161
196
|
|
|
162
|
-
/** 한 줄의 글 마디를 [시작
|
|
163
|
-
|
|
197
|
+
/** 한 줄의 글 마디를 [시작 위치, 글]로 나온 차례로. 뜻은 파이썬 lineLiterals가 정한다. @param {string} line @param {number} [offset] @returns {[number, string][]} */
|
|
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 });
|
|
@@ -206,7 +304,7 @@ export function sourceLiterals(source, path = "") {
|
|
|
206
304
|
}
|
|
207
305
|
|
|
208
306
|
/**
|
|
209
|
-
* 한 줄 안에서 글 `old
|
|
307
|
+
* 한 줄 안에서 글 `old`를 `new`로 바꾼다. 몇 번 나왔는지 같이 준다 (한 번일 때만 바꾼 것이 확정이다).
|
|
210
308
|
* @param {string} lineText @param {string} oldText @param {string} newText @returns {[string, number]}
|
|
211
309
|
*/
|
|
212
310
|
export function replaceLiteral(lineText, oldText, newText) {
|
package/src/edit/applyFixes.js
CHANGED
|
Binary file
|
package/src/fingerprint/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 문서 모델을 한 번 훑어 글
|
|
2
|
+
/** 문서 모델을 한 번 훑어 글 분석 결과를 만든다. 텍스트를 읽는 곳은 여기 하나다. 파이썬 fingerprint/build.py와 같다. */
|
|
3
3
|
import { defaultConfig } from "../config/settings.js";
|
|
4
4
|
import { NONE as NO_REGISTER, documentRegister, lastWord, registerOfWord } from "../analysis/grammar/register.js";
|
|
5
5
|
import { doublePassiveSpans, euiCount, longestNounRun, splitSentences } from "../analysis/index.js";
|
|
@@ -88,9 +88,9 @@ import { overlap, topicsOf, unionOf } from "./topics.js";
|
|
|
88
88
|
* @property {number} readerCallCount
|
|
89
89
|
* @property {[number, string, number, string][]} countPromises (수, 단위, 줄, 원문)
|
|
90
90
|
* @property {import("./codeBlocks.js").CodeBlock[]} codeBlocks 코드 펜스를 언어와 본문 줄로 푼 것
|
|
91
|
-
* @property {import("./readerState.js").ReaderTrail} reader
|
|
91
|
+
* @property {import("./readerState.js").ReaderTrail} reader 위치마다의 독자 상태. 약속하는 말과 다시 다루는 말은 final에 있다
|
|
92
92
|
* @property {[string, number, number][]} disabled
|
|
93
|
-
* @property {import("../document/model.js").Block[]} ignored 설정이
|
|
93
|
+
* @property {import("../document/model.js").Block[]} ignored 설정이 글 분석에서 뺀 펜스
|
|
94
94
|
* @property {string} register
|
|
95
95
|
* @property {number} registerShare
|
|
96
96
|
* @property {SectionPrint} intro
|
|
@@ -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),
|
|
@@ -229,7 +233,7 @@ function buildSection(build, section, sectionIndex) {
|
|
|
229
233
|
const line = block.startLine + countIn(text, "\n", sentence.start);
|
|
230
234
|
const quoted = quotedIn(sentence, blockSpans);
|
|
231
235
|
const made = makeSentencePrint(build, sentence.text, line, block, sectionIndex, quoted);
|
|
232
|
-
// 만든
|
|
236
|
+
// 만든 곳에서 바로 쌓는다. 다음 문장의 index가 이 길이에서 나오므로 미루면 셈이 어긋난다.
|
|
233
237
|
sentences.push(made);
|
|
234
238
|
blockSentences.push(made);
|
|
235
239
|
}
|
|
@@ -265,9 +269,14 @@ function buildSection(build, section, sectionIndex) {
|
|
|
265
269
|
* @returns {DocumentPrint}
|
|
266
270
|
*/
|
|
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,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 코드 블록을 언어와 본문으로 나눈다.
|
|
4
|
-
* 읽는다. 파이썬 fingerprint/codeBlocks.py
|
|
5
|
-
* 펜스 첫 줄의 언어 표기
|
|
3
|
+
* 코드 블록을 언어와 본문으로 나눈다. 글 분석이 한 번 만들고(DocumentPrint.codeBlocks) code 묶음 규칙과 독자 상태가
|
|
4
|
+
* 읽는다. 파이썬 fingerprint/codeBlocks.py와 같다.
|
|
5
|
+
* 펜스 첫 줄의 언어 표기(```python)를 읽고 본문 줄에 원문 줄 번호를 붙인다. text 펜스는 출력이다.
|
|
6
6
|
*/
|
|
7
7
|
import { CODE } from "../document/model.js";
|
|
8
8
|
import { fenceLanguage } from "../document/parseMarkdown.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
3
|
* 코드 줄에서 파일의 공급과 수요를 읽는다. 어떤 줄이 파일을 만들고 (쓰기, 폴더 만들기) 어떤 줄이 파일을 읽는가.
|
|
4
|
-
* 파이썬 fingerprint/codeMarkers.py
|
|
4
|
+
* 파이썬 fingerprint/codeMarkers.py와 같다. 줄마다 대조하므로 여러 줄에 걸친 호출은 보지 않는다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const QUOTED = "\\(\\s*[\"']([^\"'\\n]+)[\"']";
|
|
@@ -19,7 +19,7 @@ const WRITES = new RegExp(
|
|
|
19
19
|
const SHELL_WRITES = /(?:>>?|-o|-O|--output|Out-File)\s*["']?([\w./\\-]+\.[A-Za-z0-9]+)/g;
|
|
20
20
|
const SQL_PATHS = /FROM\s+'([^']+\.(?:csv|parquet|json|xlsx))'/gi;
|
|
21
21
|
const MAKES_DIR = /(?:mkdir(?:\s+-p)?|makedirs|md)\s*\(?\s*["']?([\w./\\-]+)|Path\(\s*["']([^"']+)["']\s*\)\.mkdir/g;
|
|
22
|
-
/** open
|
|
22
|
+
/** open의 모드에 이 글자가 있으면 쓰기다. 없으면 (기본 r) 읽기다. */
|
|
23
23
|
const WRITE_MODES = [..."wax"];
|
|
24
24
|
|
|
25
25
|
/** @param {string} path */
|
|
@@ -41,7 +41,7 @@ function all(pattern, text, group) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* 코드 줄들이 만드는 [파일 이름, 폴더 이름]. 쓰기 함수, 셸 리다이렉션, 쓰기 모드의 open, mkdir
|
|
44
|
+
* 코드 줄들이 만드는 [파일 이름, 폴더 이름]. 쓰기 함수, 셸 리다이렉션, 쓰기 모드의 open, mkdir 호출을 본다.
|
|
45
45
|
* @param {string[]} lines
|
|
46
46
|
* @returns {[Set<string>, Set<string>]}
|
|
47
47
|
*/
|
|
@@ -59,7 +59,7 @@ export function createdIn(lines) {
|
|
|
59
59
|
return [files, dirs];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** 한 줄이 읽는 경로. 읽기 함수, 읽기 모드의 open, SQL
|
|
62
|
+
/** 한 줄이 읽는 경로. 읽기 함수, 읽기 모드의 open, SQL의 FROM 차례다. @param {string} code */
|
|
63
63
|
export function readsIn(code) {
|
|
64
64
|
const reads = all(READS, code, 1);
|
|
65
65
|
for (const match of code.matchAll(OPENS)) if (!isWriteMode(match[2])) reads.push(match[1]);
|
|
@@ -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,14 +16,15 @@ 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 };
|
|
22
|
-
// {조사} 자리표시자.
|
|
23
|
+
// {조사} 자리표시자. 단어 뒤에 조사가 붙었거나 단어가 끝나는 곳. 파이썬 dictionaries.py와 같은 글자다.
|
|
23
24
|
const JOSA_TAIL = "(?=(?:에서는|으로는|에서|에게|까지|부터|보다|처럼|으로|이나|은|는|이|가|을|를|의|에|로|와|과|도|만)?(?![가-힣]))";
|
|
24
25
|
const PLACEHOLDER = /\{(ㄴ|ㄹ|조사)\}/g;
|
|
25
26
|
|
|
26
|
-
/** 받침으로 끝나는 음절 399개의
|
|
27
|
+
/** 받침으로 끝나는 음절 399개의 글자 집합. @param {"ㄴ" | "ㄹ"} final */
|
|
27
28
|
function syllableClass(final) {
|
|
28
29
|
let chars = "";
|
|
29
30
|
for (let initial = 0; initial < 19; initial++) {
|
|
@@ -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;
|
|
@@ -87,7 +96,7 @@ export function entriesFor(config) {
|
|
|
87
96
|
const extra = [];
|
|
88
97
|
for (const [dictionary, items] of Object.entries(config.dictionary)) {
|
|
89
98
|
if (!(dictionary in DICTIONARY_FILES)) {
|
|
90
|
-
throw new Error(`모르는 사전: ${dictionary}. ${Object.keys(DICTIONARY_FILES).join(", ")}
|
|
99
|
+
throw new Error(`모르는 사전: ${dictionary}. 쓸 수 있는 이름: ${Object.keys(DICTIONARY_FILES).join(", ")}`);
|
|
91
100
|
}
|
|
92
101
|
for (const raw of items) extra.push(entryFrom(dictionary, /** @type {Record<string, unknown> | string} */ (raw)));
|
|
93
102
|
}
|
|
@@ -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
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 문장에서
|
|
4
|
-
*
|
|
3
|
+
* 문장에서 정해 둔 말을 센다. 종결어미 묶음, 서법, 접속부사, 인과 접속사, 지시어, 얼버무리는 말, 약속하는 말과 다시 다루는 말, 독자 호출, 수 약속.
|
|
4
|
+
* 단어 목록은 모두 data/에 있다. 파이썬 fingerprint/markers.py와 같다.
|
|
5
5
|
*/
|
|
6
6
|
import { loadLines, loadPatterns } from "../data/load.js";
|
|
7
7
|
import { compile, escape } from "../regex.js";
|
|
8
8
|
|
|
9
9
|
const TRAILING = /[\s.?!"'”’)\]]+$/;
|
|
10
10
|
const NUMBER = /\d+(?:[.,]\d+)*/g;
|
|
11
|
-
/** 값으로
|
|
11
|
+
/** 값으로 비교하는 수. NUMBER는 수의 개수를 세고 이것은 수의 정체를 센다. 천 단위 쉼표까지 한 수로 읽는다. */
|
|
12
12
|
const NUMERAL = /\d[\d,]*(?:\.\d+)?/g;
|
|
13
13
|
const COMMA = /,/g;
|
|
14
14
|
// 따옴표 쌍. 안은 사용이 아니라 인용이다. 작은따옴표는 앞이 문장 첫머리나 공백이나 여는 괄호일 때만.
|
|
@@ -27,7 +27,7 @@ const OPTIONAL_SPACE = /\s?/.source;
|
|
|
27
27
|
|
|
28
28
|
/** @type {Record<string, number> | null} */
|
|
29
29
|
let numberCache = null;
|
|
30
|
-
/** 수사 → 값.
|
|
30
|
+
/** 수사 → 값. 목록은 data/koreanNumbers.txt에 있다. */
|
|
31
31
|
export function koreanNumbers() {
|
|
32
32
|
if (!numberCache) {
|
|
33
33
|
numberCache = {};
|
|
@@ -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);
|
|
@@ -167,7 +189,7 @@ export function countPromisesIn(text) {
|
|
|
167
189
|
}
|
|
168
190
|
|
|
169
191
|
/** @param {string} text */
|
|
170
|
-
/** 글에 나온 수의 정체 (천 단위 쉼표를 뗀 꼴). 독자 상태가 쌓고 numberOrphan
|
|
192
|
+
/** 글에 나온 수의 정체 (천 단위 쉼표를 뗀 꼴). 독자 상태가 쌓고 numberOrphan이 비교한다. @param {string} text @returns {Set<string>} */
|
|
171
193
|
export function numeralsIn(text) {
|
|
172
194
|
const found = new Set();
|
|
173
195
|
for (const match of text.matchAll(NUMERAL)) found.add(match[0].replaceAll(",", ""));
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 독자 상태. 글을 위에서 아래로 읽는 독자가 어느
|
|
4
|
-
* 파이썬 fingerprint/readerState.py
|
|
5
|
-
*
|
|
3
|
+
* 독자 상태. 글을 위에서 아래로 읽는 독자가 어느 위치에서 무엇을 이미 알고 무엇을 이미 보았는가.
|
|
4
|
+
* 파이썬 fingerprint/readerState.py와 같다. 독자 상태를 보는 규칙(reader)은 한 위치가 필요로 하는 것을 그 위치의
|
|
5
|
+
* 상태와 대조한다. 상태는 블록과 문장 순서로 쌓이고 한 번 쌓은 것은 바뀌지 않는다. 집합은 고치지 않는다.
|
|
6
6
|
*/
|
|
7
7
|
import { CODE, PROSE } from "../document/model.js";
|
|
8
8
|
import { createdIn } from "./codeMarkers.js";
|
|
9
9
|
import { numeralsIn } from "./markers.js";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* 한
|
|
12
|
+
* 한 위치(문장 하나 또는 블록 하나)를 읽기 직전의 독자.
|
|
13
13
|
* @typedef {object} ReaderState
|
|
14
14
|
* @property {import("./build.js").SentencePrint | null} previous 바로 앞에 읽은 산문 문장
|
|
15
15
|
* @property {Set<string>} recent 손에 든 화제어. 바로 앞 문장의 것이고 앞 문장이 없으면 빈 집합
|
|
16
|
-
* @property {Set<string>} known 이
|
|
16
|
+
* @property {Set<string>} known 이 위치 앞까지 산문에 나온 화제어 전부
|
|
17
17
|
* @property {Set<string>} numerals 지금까지 산문과 블록에 나온 수 (천 단위 쉼표를 뗀 꼴)
|
|
18
18
|
* @property {Set<string>} files 앞선 코드 블록이 만든 파일 이름과 폴더
|
|
19
19
|
* @property {number} sentencesRead 지금까지 읽은 산문 문장 수
|
|
20
|
-
* @property {[number, string][]} promises 지금까지 뒤로 미룬
|
|
21
|
-
* @property {[number, string][]} recalls 지금까지
|
|
20
|
+
* @property {[number, string][]} promises 지금까지 뒤로 미룬 말(줄, 원문)
|
|
21
|
+
* @property {[number, string][]} recalls 지금까지 앞의 이야기를 다시 다룬 말(줄, 원문)
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 위치마다의 독자 상태. 문장은 index로, 블록은 index로 찾는다.
|
|
26
26
|
* @typedef {object} ReaderTrail
|
|
27
27
|
* @property {ReaderState[]} beforeSentence
|
|
28
28
|
* @property {ReaderState[]} beforeBlock
|
|
@@ -84,7 +84,7 @@ function afterBlock(state, block, code) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* 블록 순서로 한 번 지나며
|
|
87
|
+
* 블록 순서로 한 번 지나며 위치마다의 상태를 적는다. 산문 블록은 문장 하나씩, 나머지는 블록째 읽는다.
|
|
88
88
|
* @param {import("../document/model.js").Block[]} blocks
|
|
89
89
|
* @param {import("./codeBlocks.js").CodeBlock[]} codeBlocks
|
|
90
90
|
* @param {import("./build.js").SentencePrint[]} sentences
|