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.
Files changed (136) hide show
  1. package/README.md +23 -20
  2. package/data/calqueFrame.json +60 -0
  3. package/data/cliches.json +39 -29
  4. package/data/collocationStops.txt +28 -0
  5. package/data/confusable.json +20 -20
  6. package/data/easyWords.json +12 -246
  7. package/data/exemplars.json +50 -29
  8. package/data/hiddenDeps.txt +11 -11
  9. package/data/japaneseLoan.json +27 -27
  10. package/data/koreanProse.txt +64 -0
  11. package/data/patterns.json +26 -26
  12. package/data/phraseRepeat.json +11 -0
  13. package/data/platformApis.txt +11 -11
  14. package/data/redundantPair.json +16 -16
  15. package/data/ruleCategories.json +2 -0
  16. package/data/ruleDocs.json +56 -54
  17. package/data/ruleMechanisms.json +2 -0
  18. package/data/screenNarration.json +10 -10
  19. package/data/screenSentence.json +60 -12
  20. package/data/screenTone.json +6 -6
  21. package/data/screenWord.json +70 -110
  22. package/data/spacing.json +12 -12
  23. package/data/spelling.json +29 -29
  24. package/data/translationese.json +23 -24
  25. package/data/usageCounts.report.json +1676 -0
  26. package/data/version.json +1 -1
  27. package/package.json +1 -1
  28. package/src/analysis/grammar/ending.js +1 -1
  29. package/src/analysis/grammar/hangul.js +4 -4
  30. package/src/analysis/grammar/josa.js +1 -1
  31. package/src/analysis/grammar/register.js +1 -1
  32. package/src/analysis/grammar/voice.js +1 -1
  33. package/src/analysis/index.js +3 -3
  34. package/src/analysis/josa.js +1 -1
  35. package/src/analysis/splitSentences.js +1 -1
  36. package/src/analysis/tokenize.js +41 -26
  37. package/src/baseline/store.js +8 -8
  38. package/src/cli/help.js +11 -10
  39. package/src/cli/main.js +332 -182
  40. package/src/cli/walk.js +2 -2
  41. package/src/cli/welcome.js +8 -8
  42. package/src/config/loadConfig.js +3 -3
  43. package/src/config/patch.js +6 -6
  44. package/src/config/readerContract.js +26 -26
  45. package/src/config/settings.js +44 -25
  46. package/src/config/toml.js +9 -9
  47. package/src/data/exemplars.js +21 -21
  48. package/src/data/load.js +6 -6
  49. package/src/data/operations.js +14 -14
  50. package/src/data/patches.js +10 -10
  51. package/src/data/patterns.js +5 -5
  52. package/src/data/profiles.js +6 -6
  53. package/src/data/prose.js +27 -0
  54. package/src/document/model.js +4 -4
  55. package/src/document/parseMarkdown.js +7 -7
  56. package/src/document/plainText.js +3 -3
  57. package/src/document/sourceText.js +117 -19
  58. package/src/edit/applyFixes.js +0 -0
  59. package/src/fingerprint/build.js +18 -9
  60. package/src/fingerprint/codeBlocks.js +3 -3
  61. package/src/fingerprint/codeMarkers.js +4 -4
  62. package/src/fingerprint/dictionaries.js +34 -7
  63. package/src/fingerprint/markers.js +27 -5
  64. package/src/fingerprint/readerState.js +9 -9
  65. package/src/fingerprint/sourceSentence.js +1 -1
  66. package/src/fingerprint/topics.js +1 -1
  67. package/src/guard/contract.js +6 -6
  68. package/src/guard/editLocality.js +1 -1
  69. package/src/guard/outline.js +1 -1
  70. package/src/guard/receipt.js +13 -13
  71. package/src/guard/surface.js +4 -4
  72. package/src/index.js +9 -7
  73. package/src/learn/edits.js +2 -2
  74. package/src/learn/operations.js +1 -1
  75. package/src/regex.js +2 -2
  76. package/src/report/compactReport.js +7 -3
  77. package/src/report/fingerprintJson.js +2 -2
  78. package/src/report/githubReport.js +1 -1
  79. package/src/report/jsonReport.js +3 -3
  80. package/src/report/registerMatch.js +2 -2
  81. package/src/report/sheet.js +0 -0
  82. package/src/report/textReport.js +35 -16
  83. package/src/report/writingSpec.js +17 -17
  84. package/src/rules/code/inputFileSource.js +1 -1
  85. package/src/rules/code/installImport.js +2 -2
  86. package/src/rules/code/platformApi.js +1 -1
  87. package/src/rules/document/countMismatch.js +2 -1
  88. package/src/rules/document/enoughOnce.js +2 -1
  89. package/src/rules/document/fieldEcho.js +10 -6
  90. package/src/rules/document/keywordHeading.js +1 -1
  91. package/src/rules/document/keywordMissing.js +1 -1
  92. package/src/rules/document/noQuestion.js +2 -2
  93. package/src/rules/document/promiseRecall.js +1 -1
  94. package/src/rules/document/tableOddCell.js +1 -1
  95. package/src/rules/finding.js +4 -4
  96. package/src/rules/paragraph/factListParagraph.js +1 -1
  97. package/src/rules/paragraph/paraFragment.js +1 -1
  98. package/src/rules/readerDebt.js +1 -1
  99. package/src/rules/registry.js +30 -26
  100. package/src/rules/screen/screenWord.js +2 -1
  101. package/src/rules/sentence/calqueFrame.js +11 -0
  102. package/src/rules/sentence/cliche.js +7 -3
  103. package/src/rules/sentence/connectorRepeat.js +2 -1
  104. package/src/rules/sentence/danglingDeixis.js +1 -1
  105. package/src/rules/sentence/dash.js +1 -1
  106. package/src/rules/sentence/deixis.js +1 -1
  107. package/src/rules/sentence/doubleNegative.js +2 -1
  108. package/src/rules/sentence/doublePassive.js +2 -1
  109. package/src/rules/sentence/draftHistory.js +2 -1
  110. package/src/rules/sentence/endingRepeat.js +3 -2
  111. package/src/rules/sentence/euiChain.js +1 -1
  112. package/src/rules/sentence/fillerOpener.js +1 -1
  113. package/src/rules/sentence/hardWord.js +6 -3
  114. package/src/rules/sentence/imperativePeriod.js +1 -1
  115. package/src/rules/sentence/japaneseLoan.js +7 -3
  116. package/src/rules/sentence/longSentence.js +1 -1
  117. package/src/rules/sentence/negationRedefine.js +1 -1
  118. package/src/rules/sentence/nounPile.js +10 -4
  119. package/src/rules/sentence/numberOrphan.js +1 -1
  120. package/src/rules/sentence/outsideProfile.js +8 -3
  121. package/src/rules/sentence/phraseRepeat.js +54 -0
  122. package/src/rules/sentence/redundantPair.js +7 -3
  123. package/src/rules/sentence/translationese.js +7 -3
  124. package/src/rules/shared/candidates.js +2 -2
  125. package/src/rules/shared/dictionaryRule.js +46 -8
  126. package/src/rules/shared/repeat.js +2 -2
  127. package/src/rules/structure/bridgeRepeat.js +3 -3
  128. package/src/rules/structure/emojiBullet.js +1 -1
  129. package/src/rules/structure/headingSkip.js +1 -1
  130. package/src/rules/structure/headingUniform.js +2 -1
  131. package/src/rules/structure/loneSubheading.js +1 -1
  132. package/src/rules/structure/moreLater.js +1 -1
  133. package/src/text.js +2 -2
  134. package/src/usage/counts.js +54 -0
  135. package/src/usage/index.js +8 -0
  136. 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` 의 `{` 를 닫는 `}` 의 자리. 뜻은 파이썬 balancedEnd 가 소유한다. @param {string} line @param {number} 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
- /** 글에 낀 식 `{...}` 와 `${...}` 의 [시작, 끝 다음]. 뜻은 파이썬 expressionSpans 가 소유한다. @param {string} text @returns {[number, number][]} */
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 가 소유한다. @param {string} source */
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.replace(/\/\/.*$/, "");
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` 의 따옴표를 닫는 자리. 백틱 안의 `${...}` 은 통째로 건너뛴다. 뜻은 파이썬 closingQuote 가 소유한다. @param {string} line @param {number} 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` 의 `>` 가 JSX 여는 태그의 끝인가. 뜻은 파이썬 tagCloses 가 소유한다. @param {string} line @param {number} 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` 부터의 JSX 글이 끝나는 `<` 의 자리. 식 안의 `<` 는 글의 끝이 아니다. 뜻은 파이썬 jsxTextEnd 가 소유한다. @param {string} line @param {number} 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 가 소유한다. @param {string} text @param {number} offset @returns {[number, string][]} */
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
- /** 한 줄의 글 마디를 [시작 자리, 글] 로 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @param {number} [offset] @returns {[number, string][]} */
163
- export function lineLiterals(line, offset = 0) {
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
- const end = jsxTextEnd(line, index + 1);
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
- for (const [start, raw] of lineLiterals(line)) {
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` 를 `new` 로 바꾼다. 몇 번 나왔는지 같이 준다 (한 번일 때만 바꾼 것이 확정이다).
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) {
Binary file
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 문서 모델을 한 번 훑어 글 지문을 만든다. 텍스트를 읽는 유일한 자리다. 파이썬 fingerprint/build.py 와 같다. */
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 자리마다의 독자 상태. 약속과 회수 표지는 final 에 있다
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]) => !markers.insideAny(start, end, quoted))
139
+ .filter(([start, end]) => kept(start, end))
136
140
  .map(([, , found]) => found);
137
- const matches = matchesIn(text, entries).filter((m) => !markers.insideAny(m.start, m.end, quoted));
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]) => !markers.insideAny(start, end, quoted))
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
- // 만든 자리에서 바로 쌓는다. 다음 문장의 index 가 이 길이에서 나오므로 미루면 셈이 어긋난다.
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 = { entries: entriesFor(config), sentences: /** @type {SentencePrint[]} */ ([]), paragraphs: /** @type {ParagraphPrint[]} */ ([]) };
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
- * 코드 블록을 언어와 본문으로 나눈다. 지문이 한 번 만들고 (DocumentPrint.codeBlocks) code 부류 규칙과 독자 상태가
4
- * 읽는다. 파이썬 fingerprint/codeBlocks.py 와 같다.
5
- * 펜스 첫 줄의 언어 표기 (```python) 를 읽고 본문 줄에 원문 줄 번호를 붙인다. text 펜스는 출력이다.
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 의 모드에 이 글자가 있으면 쓰기다. 없으면 (기본 r) 읽기다. */
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 의 FROM 차례다. @param {string} code */
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
- /** 사전 넷을 한 번 컴파일하고 문장에서 맞는 자리를 찾는다. 설정의 dictionary 항목을 더한다. */
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
- // {조사} 자리표시자. 낱말 뒤에 조사가 붙었거나 낱말이 끝나는 자리. 파이썬 dictionaries.py 와 같은 글자다.
23
+ // {조사} 자리표시자. 단어 뒤에 조사가 붙었거나 단어가 끝나는 곳. 파이썬 dictionaries.py와 같은 글자다.
23
24
  const JOSA_TAIL = "(?=(?:에서는|으로는|에서|에게|까지|부터|보다|처럼|으로|이나|은|는|이|가|을|를|의|에|로|와|과|도|만)?(?![가-힣]))";
24
25
  const PLACEHOLDER = /\{(ㄴ|ㄹ|조사)\}/g;
25
26
 
26
- /** 받침으로 끝나는 음절 399개의 문자 부류. @param {"ㄴ" | "ㄹ"} final */
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
- * 낱말 목록은 전부 data/ 가 정본이다. 파이썬 fingerprint/markers.py 와 같다.
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
- /** 값으로 견주는 수. NUMBER 는 수의 개수를 세고 이것은 수의 정체를 센다. 천 단위 쉼표까지 한 수로 읽는다. */
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
- /** 수사 → 값. 정본은 data/koreanNumbers.txt. */
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 이 견준다. @param {string} text @returns {Set<string>} */
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 와 같다. reader 기제의 규칙은 어느 자리가 요구하는 것을 그 자리의 상태에
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
- * 자리마다의 독자 상태. 문장은 index 로, 블록은 index 로 찾는다.
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
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 평문 지문의 문장을 마크다운 표식을 보존한 원문 문장에 다시 맞춘다. */
2
+ /** 평문 분석 결과의 문장을 마크다운 표식을 보존한 원문 문장에 다시 맞춘다. */
3
3
  import { splitSentences } from "../analysis/splitSentences.js";
4
4
  import { plainText } from "../document/plainText.js";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- /** 화제어 집합. 조사를 뗀 명사 어절의 근사. 파이썬 fingerprint/topics.py 와 같다. */
2
+ /** 화제어 집합. 조사를 뗀 명사 어절의 어림. 파이썬 fingerprint/topics.py와 같다. */
3
3
  import { stripJosa, tailOf, words } from "../analysis/tokenize.js";
4
4
  import { loadLines } from "../data/load.js";
5
5