hanlint 0.0.13 → 0.0.15

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.13"}
1
+ {"version": "0.0.15"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hanlint",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "한국어 글 검사기. Korean prose linter for Markdown: translationese, noun piles, double passives, weak structure. Zero dependencies",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/main.js CHANGED
@@ -1005,6 +1005,11 @@ function relativeLabel(path) {
1005
1005
  return label.split(sep).join("/").replaceAll("\\", "/");
1006
1006
  }
1007
1007
 
1008
+ /** 표의 자리 글자. 뜻은 파이썬 SheetRow.place 가 소유한다. @param {import("../report/sheet.js").SheetRow} row */
1009
+ function placeOf(row) {
1010
+ return row.column > 0 ? `${row.file}:${row.line}:${row.column}` : `${row.file}:${row.line}`;
1011
+ }
1012
+
1008
1013
  /** 표의 고침을 파일에 쓴다. 뜻은 파이썬 applySheet 가 소유한다. @param {string} sheetPath @param {boolean} dryRun @returns {[string[], string[]]} */
1009
1014
  function applySheet(sheetPath, dryRun) {
1010
1015
  const parsed = parseSheet(readFileSync(sheetPath, "utf-8"));
@@ -1020,21 +1025,33 @@ function applySheet(sheetPath, dryRun) {
1020
1025
  for (const file of [...byFile.keys()].sort()) {
1021
1026
  const rows = /** @type {import("../report/sheet.js").SheetRow[]} */ (byFile.get(file));
1022
1027
  if (!existsSync(file)) {
1023
- failed.push(...rows.map((row) => `${row.file}:${row.line}: 파일이 없다`));
1028
+ failed.push(...rows.map((row) => `${placeOf(row)}: 파일이 없다`));
1024
1029
  continue;
1025
1030
  }
1026
1031
  const lines = readFileSync(file, "utf-8").split("\n");
1027
1032
  let changed = false;
1028
- for (const row of rows) {
1029
- const place = `${row.file}:${row.line}`;
1033
+ // 한 줄에 고침이 여럿이면 뒤 칸부터. 앞 칸을 먼저 바꾸면 길이가 달라져 뒤 칸의 자리가 어긋난다.
1034
+ for (const row of [...rows].sort((a, b) => a.line - b.line || b.column - a.column)) {
1035
+ const place = placeOf(row);
1030
1036
  if (row.line < 1 || row.line > lines.length) {
1031
1037
  failed.push(`${place}: 그 줄이 없다`);
1032
1038
  continue;
1033
1039
  }
1034
- const [newLine, count] = replaceLiteral(lines[row.line - 1], row.text, row.fix);
1035
- if (count !== 1) {
1036
- failed.push(`${place}: 글이 그 줄에 ${count}번 있다. 한 번이어야 바꾼다`);
1037
- continue;
1040
+ let newLine = lines[row.line - 1];
1041
+ if (row.column > 0) {
1042
+ const start = row.column - 1;
1043
+ if (newLine.slice(start, start + row.text.length) !== row.text) {
1044
+ failed.push(`${place}: 그 칸에 그 글이 없다. 표를 다시 뽑는다`);
1045
+ continue;
1046
+ }
1047
+ newLine = newLine.slice(0, start) + row.fix + newLine.slice(start + row.text.length);
1048
+ } else {
1049
+ const [replaced, count] = replaceLiteral(newLine, row.text, row.fix);
1050
+ if (count !== 1) {
1051
+ failed.push(`${place}: 글이 그 줄에 ${count}번 있다. 한 번이어야 바꾼다`);
1052
+ continue;
1053
+ }
1054
+ newLine = replaced;
1038
1055
  }
1039
1056
  lines[row.line - 1] = newLine;
1040
1057
  changed = true;
@@ -1078,7 +1095,7 @@ function runSheet(args) {
1078
1095
  const label = relativeLabel(file);
1079
1096
  for (const literal of sourceLiterals(readFileSync(file, "utf-8"), label)) {
1080
1097
  const findings = runAll(fingerprint(literal.plain, config), config).filter((finding) => finding.severity === "error");
1081
- if (findings.length || options["--all"]) rows.push({ file: label, line: literal.line, text: literal.text, findings, fix: "" });
1098
+ if (findings.length || options["--all"]) rows.push({ file: label, line: literal.line, text: literal.text, findings, fix: "", column: literal.column });
1082
1099
  }
1083
1100
  }
1084
1101
  emit(format === "json" ? renderSheetJson(rows, config.preset, files.length) : renderSheet(rows, config.preset, files.length), output);
@@ -20,6 +20,7 @@ const RUST_TEST_MARKER = "#[cfg(test)]";
20
20
  * @property {number} line 1부터 세는 줄 번호. 여러 줄을 이은 러스트 문자열은 시작 줄이다
21
21
  * @property {string} text 따옴표와 태그를 뺀 글. 파일에 있는 그대로라 되돌려 쓸 때 찾을 수 있다
22
22
  * @property {string} plain 검사에 쓰는 글. 식 (`${...}`, `{...}`) 을 비우고 공백을 하나로 모았다
23
+ * @property {number} column 1부터 세는 칸. 그 줄에서 text 가 시작하는 자리
23
24
  */
24
25
 
25
26
  /** 템플릿 리터럴의 `${ ... }` 식을 (중첩 괄호를 세어) 같은 길이의 빈칸으로 바꾼다. @param {string} source */
@@ -88,9 +89,16 @@ function closingQuote(line, start) {
88
89
  return -1;
89
90
  }
90
91
 
91
- /** 한 줄의 글 마디를 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @returns {string[]} */
92
+ /** `index` 의 `>` 가 JSX 여는 태그의 끝인가. 뜻은 파이썬 tagCloses 가 소유한다. @param {string} line @param {number} index */
93
+ function tagCloses(line, index) {
94
+ if (index === 0 || line[index + 1] === "=") return false;
95
+ const before = line[index - 1];
96
+ return "\"'}".includes(before) || /[A-Za-z0-9]/.test(before);
97
+ }
98
+
99
+ /** 한 줄의 글 마디를 [시작 자리, 글] 로 나온 차례로. 뜻은 파이썬 lineLiterals 가 소유한다. @param {string} line @returns {[number, string][]} */
92
100
  export function lineLiterals(line) {
93
- /** @type {string[]} */
101
+ /** @type {[number, string][]} */
94
102
  const found = [];
95
103
  let index = 0;
96
104
  while (index < line.length) {
@@ -98,15 +106,15 @@ export function lineLiterals(line) {
98
106
  if (QUOTES.includes(char)) {
99
107
  const end = closingQuote(line, index);
100
108
  if (end < 0) break;
101
- found.push(line.slice(index + 1, end));
109
+ found.push([index + 1, line.slice(index + 1, end)]);
102
110
  index = end + 1;
103
111
  continue;
104
112
  }
105
- if (char === ">" && (index === 0 || !"=-".includes(line[index - 1]))) {
113
+ if (char === ">" && tagCloses(line, index)) {
106
114
  const end = line.indexOf("<", index + 1);
107
115
  if (end > index) {
108
116
  const inner = line.slice(index + 1, end);
109
- found.push(inner);
117
+ found.push([index + 1, inner]);
110
118
  if (!inner.includes("{")) {
111
119
  index = end;
112
120
  continue;
@@ -123,10 +131,10 @@ export function sourceLiterals(source, path = "") {
123
131
  /** @type {SourceLiteral[]} */
124
132
  const found = [];
125
133
  userFacingSource(source, path).split("\n").forEach((line, index) => {
126
- for (const raw of lineLiterals(line)) {
134
+ for (const [start, raw] of lineLiterals(line)) {
127
135
  const text = raw.trim();
128
136
  const plain = plainText(text);
129
- if (text && KOREAN.test(plain)) found.push({ line: index + 1, text, plain });
137
+ if (text && KOREAN.test(plain)) found.push({ line: index + 1, text, plain, column: start + (raw.length - raw.trimStart().length) + 1 });
130
138
  }
131
139
  });
132
140
  return found;
Binary file