hanlint 0.0.16 → 0.0.17

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/data/version.json CHANGED
@@ -1 +1 @@
1
- {"version": "0.0.16"}
1
+ {"version": "0.0.17"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanlint",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "한국어 글 검사기. Korean prose linter for Markdown: translationese, noun piles, double passives, weak structure. Zero dependencies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,6 @@ export const SOURCE_SUFFIXES = [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".
9
9
  const KOREAN = /[가-힣]/;
10
10
  // 문자열의 경계. 줄을 앞에서부터 훑어 여는 따옴표에서 같은 닫는 따옴표까지를 한 마디로 본다. 파이썬 sourceText.py 와 같다.
11
11
  const QUOTES = ["'", '"', "`"];
12
- const EXPRESSION = /\{[^{}]*\}/g;
13
12
  const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
14
13
  const DEVELOPER_LINE = /new Error\(|console\.|panic!\(|expect\(|assert/;
15
14
  const RUST_CONTINUATION = /\\\r?\n[ \t]*/g;
@@ -23,28 +22,53 @@ const RUST_TEST_MARKER = "#[cfg(test)]";
23
22
  * @property {number} column 1부터 세는 칸. 그 줄에서 text 가 시작하는 자리
24
23
  */
25
24
 
26
- /** 템플릿 리터럴의 `${ ... }` 식을 (중첩 괄호를 세어) 같은 길이의 빈칸으로 바꾼다. @param {string} source */
27
- export function withoutTemplateExpressions(source) {
28
- let output = "";
25
+ /** `start` 의 `{` 를 닫는 `}` 의 자리. 뜻은 파이썬 balancedEnd 가 소유한다. @param {string} line @param {number} start */
26
+ function balancedEnd(line, start) {
27
+ let depth = 0;
28
+ let index = start;
29
+ while (index < line.length) {
30
+ const char = line[index];
31
+ if (QUOTES.includes(char)) {
32
+ const end = closingQuote(line, index);
33
+ if (end < 0) return -1;
34
+ index = end + 1;
35
+ continue;
36
+ }
37
+ if (char === "{") depth += 1;
38
+ else if (char === "}") {
39
+ depth -= 1;
40
+ if (depth === 0) return index;
41
+ }
42
+ index += 1;
43
+ }
44
+ return -1;
45
+ }
46
+
47
+ /** 글에 낀 식 `{...}` 와 `${...}` 의 [시작, 끝 다음]. 뜻은 파이썬 expressionSpans 가 소유한다. @param {string} text @returns {[number, number][]} */
48
+ export function expressionSpans(text) {
49
+ /** @type {[number, number][]} */
50
+ const spans = [];
29
51
  let index = 0;
30
- while (index < source.length) {
31
- if (source[index] === "$" && source[index + 1] === "{") {
32
- let depth = 0;
33
- let cursor = index + 1;
34
- for (; cursor < source.length; cursor++) {
35
- if (source[cursor] === "{") depth += 1;
36
- else if (source[cursor] === "}") {
37
- depth -= 1;
38
- if (depth === 0) break;
39
- }
40
- }
41
- output += " ".repeat(cursor + 1 - index);
42
- index = cursor + 1;
52
+ while (index < text.length) {
53
+ if (text[index] === "{") {
54
+ const end = balancedEnd(text, index);
55
+ if (end < 0) break;
56
+ const start = index > 0 && text[index - 1] === "$" ? index - 1 : index;
57
+ spans.push([start, end + 1]);
58
+ index = end + 1;
43
59
  continue;
44
60
  }
45
- output += source[index];
46
61
  index += 1;
47
62
  }
63
+ return spans;
64
+ }
65
+
66
+ /** 템플릿 리터럴의 `${ ... }` 식을 같은 길이의 빈칸으로 바꾼다. 뜻은 파이썬 withoutTemplateExpressions 가 소유한다. @param {string} source */
67
+ export function withoutTemplateExpressions(source) {
68
+ let output = source;
69
+ for (const [start, end] of expressionSpans(source)) {
70
+ if (source[start] === "$") output = output.slice(0, start) + " ".repeat(end - start) + output.slice(end);
71
+ }
48
72
  return output;
49
73
  }
50
74
 
@@ -68,12 +92,17 @@ export function userFacingSource(source, path) {
68
92
 
69
93
  /** 검사에 쓰는 글. 식을 비우고 공백을 하나로 모은다. @param {string} text */
70
94
  export function plainText(text) {
71
- let copy = withoutTemplateExpressions(text);
72
- while (EXPRESSION.test(copy)) copy = copy.replace(EXPRESSION, " ");
73
- return copy.replace(/\s+/g, " ").trim();
95
+ let plain = "";
96
+ let cursor = 0;
97
+ for (const [start, end] of expressionSpans(text)) {
98
+ plain += text.slice(cursor, start) + " ";
99
+ cursor = end;
100
+ }
101
+ plain += text.slice(cursor);
102
+ return plain.replace(/\s+/g, " ").trim();
74
103
  }
75
104
 
76
- /** `start` 의 따옴표를 닫는 자리. 역슬래시 뒤 글자는 건너뛴다. 없으면 -1. @param {string} line @param {number} start */
105
+ /** `start` 의 따옴표를 닫는 자리. 백틱 안의 `${...}` 은 통째로 건너뛴다. 뜻은 파이썬 closingQuote 가 소유한다. @param {string} line @param {number} start */
77
106
  function closingQuote(line, start) {
78
107
  const quote = line[start];
79
108
  let index = start + 1;
@@ -83,6 +112,12 @@ function closingQuote(line, start) {
83
112
  index += 2;
84
113
  continue;
85
114
  }
115
+ if (quote === "`" && char === "$" && line[index + 1] === "{") {
116
+ const end = balancedEnd(line, index + 1);
117
+ if (end < 0) return -1;
118
+ index = end + 1;
119
+ continue;
120
+ }
86
121
  if (char === quote) return index;
87
122
  index += 1;
88
123
  }
@@ -96,8 +131,36 @@ function tagCloses(line, index) {
96
131
  return "\"'}".includes(before) || /[A-Za-z0-9]/.test(before);
97
132
  }
98
133
 
99
- /** 한 줄의 글 마디를 [시작 자리, 글] 로 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @returns {[number, string][]} */
100
- export function lineLiterals(line) {
134
+ /** `start` 부터의 JSX 글이 끝나는 `<` 의 자리. 식 안의 `<` 는 글의 끝이 아니다. 뜻은 파이썬 jsxTextEnd 가 소유한다. @param {string} line @param {number} start */
135
+ function jsxTextEnd(line, start) {
136
+ let index = start;
137
+ while (index < line.length) {
138
+ const char = line[index];
139
+ if (char === "<") return index;
140
+ if (char === "{") {
141
+ const end = balancedEnd(line, index);
142
+ if (end < 0) return -1;
143
+ index = end + 1;
144
+ continue;
145
+ }
146
+ index += 1;
147
+ }
148
+ return -1;
149
+ }
150
+
151
+ /** 글에 낀 식 안의 글 마디. 뜻은 파이썬 innerLiterals 가 소유한다. @param {string} text @param {number} offset @returns {[number, string][]} */
152
+ function innerLiterals(text, offset) {
153
+ /** @type {[number, string][]} */
154
+ const found = [];
155
+ for (const [start, end] of expressionSpans(text)) {
156
+ const brace = text[start] === "$" ? start + 1 : start;
157
+ found.push(...lineLiterals(text.slice(brace + 1, end - 1), offset + brace + 1));
158
+ }
159
+ return found;
160
+ }
161
+
162
+ /** 한 줄의 글 마디를 [시작 자리, 글] 로 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @param {number} [offset] @returns {[number, string][]} */
163
+ export function lineLiterals(line, offset = 0) {
101
164
  /** @type {[number, string][]} */
102
165
  const found = [];
103
166
  let index = 0;
@@ -106,23 +169,25 @@ export function lineLiterals(line) {
106
169
  if (QUOTES.includes(char)) {
107
170
  const end = closingQuote(line, index);
108
171
  if (end < 0) break;
109
- found.push([index + 1, line.slice(index + 1, end)]);
172
+ const raw = line.slice(index + 1, end);
173
+ found.push([offset + index + 1, raw]);
174
+ if (char === "`") found.push(...innerLiterals(raw, offset + index + 1));
110
175
  index = end + 1;
111
176
  continue;
112
177
  }
113
178
  if (char === ">" && tagCloses(line, index)) {
114
- const end = line.indexOf("<", index + 1);
179
+ const end = jsxTextEnd(line, index + 1);
115
180
  if (end > index) {
116
- const inner = line.slice(index + 1, end);
117
- found.push([index + 1, inner]);
118
- if (!inner.includes("{")) {
119
- index = end;
120
- continue;
121
- }
181
+ const raw = line.slice(index + 1, end);
182
+ found.push([offset + index + 1, raw]);
183
+ found.push(...innerLiterals(raw, offset + index + 1));
184
+ index = end;
185
+ continue;
122
186
  }
123
187
  }
124
188
  index += 1;
125
189
  }
190
+ found.sort((a, b) => a[0] - b[0]);
126
191
  return found;
127
192
  }
128
193