prooflint 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Higashiyama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # prooflint
2
+
3
+ Declarative text linter for Japanese/Markdown. Define custom rules in YAML — no JavaScript required.
4
+
5
+ ## textlint との棲み分け
6
+
7
+ [textlint](https://textlint.github.io/) は汎用的な日本語ルール(ですます混在検出、二重否定、助詞の重複など)を提供する。prooflint はそれを置き換えるものではなく、**プロジェクト固有のルールを YAML で宣言的に定義する**ことに特化している。
8
+
9
+ | | textlint | prooflint |
10
+ |---|---|---|
11
+ | 汎用日本語ルール | ✅ | — |
12
+ | カスタムルール定義 | JS プラグイン必要 | **YAML のみ** |
13
+ | 用語集管理 | 別途プラグイン | **組み込み** |
14
+ | プロジェクト固有設定 | `.textlintrc` | `.prooflint.yml` |
15
+
16
+ 推奨: 汎用ルールは textlint、プロジェクト固有ルールは prooflint で併用する。
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install --save-dev prooflint
22
+ # または
23
+ npm install -g prooflint
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # 設定ファイルを生成
30
+ prooflint init
31
+
32
+ # Markdown ファイルを lint
33
+ prooflint check "docs/**/*.md"
34
+
35
+ # JSON 出力 (CI 用)
36
+ prooflint check --format json "docs/**/*.md"
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ `.prooflint.yml` に 3 種類のルールを定義できる。
42
+
43
+ ### pattern — 正規表現マッチング
44
+
45
+ ```yaml
46
+ rules:
47
+ - id: no-desu-masu-mix
48
+ type: pattern
49
+ description: "である調とですます調の混在を検出"
50
+ severity: error
51
+ patterns:
52
+ - regex: "です。"
53
+ message: "である調で統一してください"
54
+ - regex: "ます。"
55
+ message: "である調で統一してください"
56
+ ```
57
+
58
+ ### dictionary — 用語の表記ゆれ統一
59
+
60
+ ```yaml
61
+ rules:
62
+ - id: term-consistency
63
+ type: dictionary
64
+ description: "用語の表記ゆれを統一"
65
+ severity: warning
66
+ terms:
67
+ - prefer: "サーバー"
68
+ avoid: ["サーバ"]
69
+ - prefer: "インターフェース"
70
+ avoid: ["インタフェース", "インターフェイス"]
71
+ ```
72
+
73
+ ### structure — 文構造のチェック
74
+
75
+ ```yaml
76
+ rules:
77
+ # 一文の長さを制限
78
+ - id: sentence-length
79
+ type: structure
80
+ severity: warning
81
+ target: sentence
82
+ max_chars: 120
83
+
84
+ # 見出しのレベルと末尾ピリオドを制限
85
+ - id: heading-style
86
+ type: structure
87
+ severity: error
88
+ target: heading
89
+ max_level: 3
90
+ no_period: true
91
+ ```
92
+
93
+ ## Severity
94
+
95
+ | 値 | 意味 | 終了コード |
96
+ |---|---|---|
97
+ | `error` | 重大な問題。CI で失敗させる | 1 |
98
+ | `warning` | 推奨事項の違反 | 0 |
99
+ | `info` | 情報提供のみ | 0 |
100
+
101
+ ## Output
102
+
103
+ ```
104
+ docs/guide.md
105
+ 3:15 error である調で統一してください no-desu-masu-mix
106
+ 7:1 warn 「サーバ」→「サーバー」に統一してください term-consistency
107
+ 12:5 error 見出しレベル4は上限(H3)を超えています heading-style
108
+
109
+ 3 problems (2 errors, 1 warning)
110
+ ```
111
+
112
+ ## CLI Options
113
+
114
+ ```
115
+ prooflint check [patterns...] [options]
116
+
117
+ Options:
118
+ -c, --config <path> 設定ファイルのパス (デフォルト: .prooflint.yml)
119
+ -f, --format <format> 出力形式: console または json (デフォルト: console)
120
+
121
+ prooflint init [options]
122
+
123
+ Options:
124
+ --force 既存の設定ファイルを上書き
125
+ ```
126
+
127
+ ## Node.js API
128
+
129
+ ```typescript
130
+ import { lintText, loadConfig } from 'prooflint'
131
+
132
+ const config = loadConfig('./.prooflint.yml')
133
+ const result = lintText(markdownContent, 'path/to/file.md', config)
134
+
135
+ console.log(result.errorCount, result.warningCount)
136
+ result.messages.forEach((msg) => {
137
+ console.log(`${msg.line}:${msg.column} [${msg.severity}] ${msg.message}`)
138
+ })
139
+ ```
140
+
141
+ ## Requirements
142
+
143
+ - Node.js >= 18
144
+
145
+ ## License
146
+
147
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
5
+ import { resolve as resolve2, dirname as dirname2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { Command } from "commander";
8
+ import { glob } from "fs/promises";
9
+
10
+ // src/config/loader.ts
11
+ import { readFileSync } from "fs";
12
+ import { resolve, dirname } from "path";
13
+ import yaml from "js-yaml";
14
+
15
+ // src/rules/types.ts
16
+ import { z } from "zod";
17
+ var SeveritySchema = z.enum(["error", "warning", "info"]);
18
+ var PatternRuleSchema = z.object({
19
+ id: z.string().min(1),
20
+ type: z.literal("pattern"),
21
+ description: z.string().optional(),
22
+ severity: SeveritySchema.default("warning"),
23
+ patterns: z.array(
24
+ z.object({
25
+ regex: z.string().min(1),
26
+ message: z.string().min(1),
27
+ flags: z.string().optional()
28
+ })
29
+ ).min(1)
30
+ });
31
+ var DictionaryRuleSchema = z.object({
32
+ id: z.string().min(1),
33
+ type: z.literal("dictionary"),
34
+ description: z.string().optional(),
35
+ severity: SeveritySchema.default("warning"),
36
+ terms: z.array(
37
+ z.object({
38
+ prefer: z.string().min(1),
39
+ avoid: z.array(z.string().min(1)).min(1)
40
+ })
41
+ ).min(1)
42
+ });
43
+ var StructureRuleSchema = z.object({
44
+ id: z.string().min(1),
45
+ type: z.literal("structure"),
46
+ description: z.string().optional(),
47
+ severity: SeveritySchema.default("warning"),
48
+ target: z.enum(["sentence", "heading"]).default("sentence"),
49
+ max_chars: z.number().int().positive().optional(),
50
+ max_level: z.number().int().min(1).max(6).optional(),
51
+ no_period: z.boolean().optional()
52
+ });
53
+ var RuleSchema = z.discriminatedUnion("type", [
54
+ PatternRuleSchema,
55
+ DictionaryRuleSchema,
56
+ StructureRuleSchema
57
+ ]);
58
+ var ContextSchema = z.object({
59
+ glossary: z.string().optional(),
60
+ style_guide: z.string().optional()
61
+ }).optional();
62
+ var ConfigSchema = z.object({
63
+ rules: z.array(RuleSchema).default([]),
64
+ context: ContextSchema
65
+ });
66
+
67
+ // src/config/loader.ts
68
+ var CONFIG_FILE_NAMES = [".prooflint.yml", ".prooflint.yaml", "prooflint.config.yml"];
69
+ function findConfigFile(cwd = process.cwd()) {
70
+ for (const name of CONFIG_FILE_NAMES) {
71
+ const candidate = resolve(cwd, name);
72
+ try {
73
+ readFileSync(candidate);
74
+ return candidate;
75
+ } catch {
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ function loadConfig(configPath) {
81
+ const raw = readFileSync(configPath, "utf-8");
82
+ const parsed = yaml.load(raw);
83
+ const result = ConfigSchema.safeParse(parsed);
84
+ if (!result.success) {
85
+ const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
86
+ throw new Error(`Invalid config at ${configPath}:
87
+ ${issues}`);
88
+ }
89
+ return result.data;
90
+ }
91
+
92
+ // src/rules/engine.ts
93
+ import { readFileSync as readFileSync2 } from "fs";
94
+
95
+ // src/parser/markdown.ts
96
+ import { remark } from "remark";
97
+ function parseMarkdown(content) {
98
+ const processor = remark();
99
+ const tree = processor.parse(content);
100
+ const textNodes = [];
101
+ const headings = [];
102
+ function visit(node) {
103
+ if (node.type === "heading") {
104
+ const text = extractText(node);
105
+ const position = node.position;
106
+ const textNode = {
107
+ text,
108
+ line: position?.start.line ?? 1,
109
+ column: position?.start.column ?? 1,
110
+ nodeType: "heading",
111
+ headingDepth: node.depth
112
+ };
113
+ headings.push(textNode);
114
+ textNodes.push(textNode);
115
+ } else if (node.type === "paragraph") {
116
+ const text = extractText(node);
117
+ const position = node.position;
118
+ textNodes.push({
119
+ text,
120
+ line: position?.start.line ?? 1,
121
+ column: position?.start.column ?? 1,
122
+ nodeType: "paragraph"
123
+ });
124
+ } else if (node.type === "listItem" || node.type === "blockquote") {
125
+ if ("children" in node) {
126
+ for (const child of node.children) {
127
+ visit(child);
128
+ }
129
+ return;
130
+ }
131
+ }
132
+ if ("children" in node && node.type !== "heading" && node.type !== "paragraph") {
133
+ for (const child of node.children) {
134
+ visit(child);
135
+ }
136
+ }
137
+ }
138
+ for (const child of tree.children) {
139
+ visit(child);
140
+ }
141
+ return { textNodes, headings, raw: content };
142
+ }
143
+ function extractText(node) {
144
+ if ("value" in node && typeof node.value === "string") {
145
+ return node.value;
146
+ }
147
+ if ("children" in node) {
148
+ return node.children.map(extractText).join("");
149
+ }
150
+ return "";
151
+ }
152
+
153
+ // src/rules/pattern.ts
154
+ function applyPatternRule(rule, nodes) {
155
+ const messages = [];
156
+ for (const node of nodes) {
157
+ for (const pattern of rule.patterns) {
158
+ const flags = pattern.flags ?? "g";
159
+ let regex;
160
+ try {
161
+ regex = new RegExp(pattern.regex, flags.includes("g") ? flags : flags + "g");
162
+ } catch {
163
+ throw new Error(`Rule "${rule.id}": invalid regex "${pattern.regex}"`);
164
+ }
165
+ let match;
166
+ while ((match = regex.exec(node.text)) !== null) {
167
+ const beforeMatch = node.text.slice(0, match.index);
168
+ const newlines = (beforeMatch.match(/\n/g) ?? []).length;
169
+ const lastNewline = beforeMatch.lastIndexOf("\n");
170
+ const col = lastNewline === -1 ? node.column + match.index : match.index - lastNewline;
171
+ messages.push({
172
+ ruleId: rule.id,
173
+ severity: rule.severity,
174
+ message: pattern.message,
175
+ line: node.line + newlines,
176
+ column: col,
177
+ source: match[0]
178
+ });
179
+ }
180
+ }
181
+ }
182
+ return messages;
183
+ }
184
+
185
+ // src/rules/dictionary.ts
186
+ function isWordBoundary(text, start, end) {
187
+ const charBefore = start > 0 ? text[start - 1] : null;
188
+ const charAfter = end < text.length ? text[end] : null;
189
+ if (charAfter === "\u30FC" || charAfter === "\u301C") return false;
190
+ return true;
191
+ }
192
+ function applyDictionaryRule(rule, nodes) {
193
+ const messages = [];
194
+ for (const node of nodes) {
195
+ for (const term of rule.terms) {
196
+ for (const avoidWord of term.avoid) {
197
+ const preferStartsWithAvoid = term.prefer.startsWith(avoidWord);
198
+ const escaped = avoidWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
199
+ const regex = new RegExp(escaped, "g");
200
+ let match;
201
+ while ((match = regex.exec(node.text)) !== null) {
202
+ const matchStart = match.index;
203
+ const matchEnd = matchStart + avoidWord.length;
204
+ if (preferStartsWithAvoid && !isWordBoundary(node.text, matchStart, matchEnd)) {
205
+ continue;
206
+ }
207
+ const beforeMatch = node.text.slice(0, matchStart);
208
+ const newlines = (beforeMatch.match(/\n/g) ?? []).length;
209
+ const lastNewline = beforeMatch.lastIndexOf("\n");
210
+ const col = lastNewline === -1 ? node.column + matchStart : matchStart - lastNewline;
211
+ messages.push({
212
+ ruleId: rule.id,
213
+ severity: rule.severity,
214
+ message: `\u300C${avoidWord}\u300D\u2192\u300C${term.prefer}\u300D\u306B\u7D71\u4E00\u3057\u3066\u304F\u3060\u3055\u3044`,
215
+ line: node.line + newlines,
216
+ column: col,
217
+ source: match[0]
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return messages;
224
+ }
225
+
226
+ // src/parser/sentence.ts
227
+ function splitSentences(text, startLine, startColumn) {
228
+ const sentences = [];
229
+ const lines = text.split("\n");
230
+ let currentLine = startLine;
231
+ let currentCol = startColumn;
232
+ let buffer = "";
233
+ let bufferLine = currentLine;
234
+ let bufferCol = currentCol;
235
+ for (let li = 0; li < lines.length; li++) {
236
+ const line = lines[li] ?? "";
237
+ let charPos = li === 0 ? startColumn - 1 : 0;
238
+ for (let ci = 0; ci < line.length; ci++) {
239
+ const ch = line[ci] ?? "";
240
+ buffer += ch;
241
+ charPos++;
242
+ if (isSentenceEnd(ch, line, ci)) {
243
+ const trimmed2 = buffer.trim();
244
+ if (trimmed2.length > 0) {
245
+ sentences.push({ text: trimmed2, line: bufferLine, column: bufferCol });
246
+ }
247
+ buffer = "";
248
+ bufferLine = currentLine;
249
+ bufferCol = charPos + 1;
250
+ }
251
+ }
252
+ if (li < lines.length - 1) {
253
+ buffer += "\n";
254
+ currentLine++;
255
+ currentCol = 1;
256
+ if (buffer.trim() === "") {
257
+ bufferLine = currentLine;
258
+ bufferCol = 1;
259
+ buffer = "";
260
+ }
261
+ }
262
+ }
263
+ const trimmed = buffer.trim();
264
+ if (trimmed.length > 0) {
265
+ sentences.push({ text: trimmed, line: bufferLine, column: bufferCol });
266
+ }
267
+ return sentences;
268
+ }
269
+ function isSentenceEnd(ch, line, index) {
270
+ if ("\u3002\uFF01\uFF1F".includes(ch)) return true;
271
+ if (ch === "." || ch === "!" || ch === "?") {
272
+ const next = line[index + 1];
273
+ if (next === void 0 || next === " " || next === " ") return true;
274
+ }
275
+ return false;
276
+ }
277
+
278
+ // src/rules/structure.ts
279
+ function applyStructureRule(rule, nodes) {
280
+ const messages = [];
281
+ const target = rule.target ?? "sentence";
282
+ if (target === "heading") {
283
+ return applyHeadingRules(rule, nodes);
284
+ }
285
+ for (const node of nodes) {
286
+ if (node.nodeType === "heading") continue;
287
+ if (rule.max_chars !== void 0) {
288
+ const sentences = splitSentences(node.text, node.line, node.column);
289
+ for (const sentence of sentences) {
290
+ if (sentence.text.length > rule.max_chars) {
291
+ messages.push({
292
+ ruleId: rule.id,
293
+ severity: rule.severity,
294
+ message: `\u4E00\u6587\u304C${sentence.text.length}\u6587\u5B57\u3067\u3059\uFF08\u4E0A\u9650: ${rule.max_chars}\u6587\u5B57\uFF09`,
295
+ line: sentence.line,
296
+ column: sentence.column,
297
+ source: sentence.text.slice(0, 40) + (sentence.text.length > 40 ? "..." : "")
298
+ });
299
+ }
300
+ }
301
+ }
302
+ }
303
+ return messages;
304
+ }
305
+ function applyHeadingRules(rule, nodes) {
306
+ const messages = [];
307
+ for (const node of nodes) {
308
+ if (node.nodeType !== "heading") continue;
309
+ if (rule.max_level !== void 0 && node.headingDepth !== void 0) {
310
+ if (node.headingDepth > rule.max_level) {
311
+ messages.push({
312
+ ruleId: rule.id,
313
+ severity: rule.severity,
314
+ message: `\u898B\u51FA\u3057\u30EC\u30D9\u30EB${node.headingDepth}\u306F\u4E0A\u9650\uFF08H${rule.max_level}\uFF09\u3092\u8D85\u3048\u3066\u3044\u307E\u3059`,
315
+ line: node.line,
316
+ column: node.column,
317
+ source: node.text
318
+ });
319
+ }
320
+ }
321
+ if (rule.no_period === true) {
322
+ if (/[。..]$/.test(node.text.trim())) {
323
+ messages.push({
324
+ ruleId: rule.id,
325
+ severity: rule.severity,
326
+ message: "\u898B\u51FA\u3057\u306E\u672B\u5C3E\u306B\u53E5\u70B9\u3092\u4F7F\u308F\u306A\u3044\u3067\u304F\u3060\u3055\u3044",
327
+ line: node.line,
328
+ column: node.column,
329
+ source: node.text
330
+ });
331
+ }
332
+ }
333
+ }
334
+ return messages;
335
+ }
336
+
337
+ // src/rules/engine.ts
338
+ function lintText(content, filePath, config) {
339
+ const doc = parseMarkdown(content);
340
+ const messages = [];
341
+ for (const rule of config.rules) {
342
+ switch (rule.type) {
343
+ case "pattern":
344
+ messages.push(...applyPatternRule(rule, doc.textNodes));
345
+ break;
346
+ case "dictionary":
347
+ messages.push(...applyDictionaryRule(rule, doc.textNodes));
348
+ break;
349
+ case "structure":
350
+ messages.push(...applyStructureRule(rule, doc.textNodes));
351
+ break;
352
+ }
353
+ }
354
+ messages.sort((a, b) => a.line !== b.line ? a.line - b.line : a.column - b.column);
355
+ return {
356
+ filePath,
357
+ messages,
358
+ errorCount: messages.filter((m) => m.severity === "error").length,
359
+ warningCount: messages.filter((m) => m.severity === "warning").length,
360
+ infoCount: messages.filter((m) => m.severity === "info").length
361
+ };
362
+ }
363
+ function lintFile(filePath, config) {
364
+ const content = readFileSync2(filePath, "utf-8");
365
+ return lintText(content, filePath, config);
366
+ }
367
+
368
+ // src/reporter/console.ts
369
+ import chalk from "chalk";
370
+ function severityLabel(severity) {
371
+ switch (severity) {
372
+ case "error":
373
+ return chalk.red("error");
374
+ case "warning":
375
+ return chalk.yellow("warn ");
376
+ case "info":
377
+ return chalk.blue("info ");
378
+ }
379
+ }
380
+ function formatMessage(msg) {
381
+ const location = chalk.dim(`${String(msg.line).padStart(4)}:${String(msg.column).padEnd(4)}`);
382
+ const sev = severityLabel(msg.severity);
383
+ const text = msg.message;
384
+ const rule = chalk.dim(msg.ruleId);
385
+ return ` ${location} ${sev} ${text} ${rule}`;
386
+ }
387
+ function formatConsole(results) {
388
+ const lines = [];
389
+ let totalErrors = 0;
390
+ let totalWarnings = 0;
391
+ let totalInfos = 0;
392
+ for (const result of results) {
393
+ if (result.messages.length === 0) continue;
394
+ lines.push("");
395
+ lines.push(chalk.underline(result.filePath));
396
+ for (const msg of result.messages) {
397
+ lines.push(formatMessage(msg));
398
+ }
399
+ totalErrors += result.errorCount;
400
+ totalWarnings += result.warningCount;
401
+ totalInfos += result.infoCount;
402
+ }
403
+ const total = totalErrors + totalWarnings + totalInfos;
404
+ if (total === 0) {
405
+ lines.push(chalk.green("\n0 problems"));
406
+ return lines.join("\n");
407
+ }
408
+ const parts = [];
409
+ if (totalErrors > 0) parts.push(chalk.red(`${totalErrors} error${totalErrors > 1 ? "s" : ""}`));
410
+ if (totalWarnings > 0) parts.push(chalk.yellow(`${totalWarnings} warning${totalWarnings > 1 ? "s" : ""}`));
411
+ if (totalInfos > 0) parts.push(chalk.blue(`${totalInfos} info`));
412
+ lines.push("");
413
+ lines.push(`${total} problem${total > 1 ? "s" : ""} (${parts.join(", ")})`);
414
+ return lines.join("\n");
415
+ }
416
+ function hasErrors(results) {
417
+ return results.some((r) => r.errorCount > 0);
418
+ }
419
+
420
+ // src/reporter/json.ts
421
+ function formatJson(results) {
422
+ const output = {
423
+ results,
424
+ summary: {
425
+ totalFiles: results.length,
426
+ filesWithProblems: results.filter((r) => r.messages.length > 0).length,
427
+ totalErrors: results.reduce((s, r) => s + r.errorCount, 0),
428
+ totalWarnings: results.reduce((s, r) => s + r.warningCount, 0),
429
+ totalInfos: results.reduce((s, r) => s + r.infoCount, 0)
430
+ }
431
+ };
432
+ return JSON.stringify(output, null, 2);
433
+ }
434
+
435
+ // src/cli.ts
436
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
437
+ var pkgPath = resolve2(__dirname, "../package.json");
438
+ var pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
439
+ var INIT_TEMPLATE = `rules:
440
+ # \u7528\u8A9E\u306E\u8868\u8A18\u3086\u308C\u3092\u7D71\u4E00
441
+ - id: term-consistency
442
+ type: dictionary
443
+ description: "\u7528\u8A9E\u306E\u8868\u8A18\u3086\u308C\u3092\u7D71\u4E00"
444
+ severity: warning
445
+ terms:
446
+ - prefer: "\u30B5\u30FC\u30D0\u30FC"
447
+ avoid: ["\u30B5\u30FC\u30D0"]
448
+ - prefer: "\u30E6\u30FC\u30B6\u30FC"
449
+ avoid: ["\u30E6\u30FC\u30B6"]
450
+
451
+ # \u4E00\u6587\u306E\u9577\u3055\u3092\u5236\u9650
452
+ - id: sentence-length
453
+ type: structure
454
+ description: "\u4E00\u6587\u306E\u9577\u3055\u3092\u5236\u9650"
455
+ severity: warning
456
+ target: sentence
457
+ max_chars: 120
458
+
459
+ # \u898B\u51FA\u3057\u306E\u30B9\u30BF\u30A4\u30EB\u3092\u7D71\u4E00
460
+ - id: heading-style
461
+ type: structure
462
+ description: "\u898B\u51FA\u3057\u306E\u30EC\u30D9\u30EB\u3068\u672B\u5C3E\u30D4\u30EA\u30AA\u30C9\u3092\u5236\u9650"
463
+ severity: error
464
+ target: heading
465
+ max_level: 4
466
+ no_period: true
467
+ `;
468
+ var program = new Command();
469
+ program.name("prooflint").description("Declarative text linter for Markdown \u2014 define rules in YAML").version(pkg.version);
470
+ program.command("init").description("Create a .prooflint.yml config file in the current directory").option("--force", "Overwrite existing config file").action((options) => {
471
+ const target = resolve2(process.cwd(), ".prooflint.yml");
472
+ if (existsSync(target) && !options.force) {
473
+ console.error(".prooflint.yml already exists. Use --force to overwrite.");
474
+ process.exit(1);
475
+ }
476
+ writeFileSync(target, INIT_TEMPLATE, "utf-8");
477
+ console.log(`Created ${target}`);
478
+ });
479
+ program.command("check [patterns...]").description("Lint Markdown files matching the given glob patterns").option("-c, --config <path>", "Path to config file").option("-f, --format <format>", "Output format: console or json", "console").action(async (patterns, options) => {
480
+ const cwd = process.cwd();
481
+ let config;
482
+ try {
483
+ if (options.config) {
484
+ config = loadConfig(resolve2(cwd, options.config));
485
+ } else {
486
+ const configPath = findConfigFile(cwd);
487
+ if (!configPath) {
488
+ console.error("No .prooflint.yml found. Run `prooflint init` to create one.");
489
+ process.exit(1);
490
+ }
491
+ config = loadConfig(configPath);
492
+ }
493
+ } catch (err) {
494
+ console.error(err instanceof Error ? err.message : String(err));
495
+ process.exit(1);
496
+ }
497
+ const targetPatterns = patterns.length > 0 ? patterns : ["**/*.md"];
498
+ const filePaths = [];
499
+ for (const pattern of targetPatterns) {
500
+ try {
501
+ for await (const entry of glob(pattern, { cwd })) {
502
+ const abs = resolve2(cwd, entry);
503
+ if (!filePaths.includes(abs)) filePaths.push(abs);
504
+ }
505
+ } catch {
506
+ const abs = resolve2(cwd, pattern);
507
+ if (existsSync(abs) && !filePaths.includes(abs)) filePaths.push(abs);
508
+ }
509
+ }
510
+ if (filePaths.length === 0) {
511
+ console.log("No files found.");
512
+ process.exit(0);
513
+ }
514
+ const results = filePaths.map((fp) => {
515
+ try {
516
+ return lintFile(fp, config);
517
+ } catch (err) {
518
+ console.error(`Error processing ${fp}: ${err instanceof Error ? err.message : String(err)}`);
519
+ process.exit(1);
520
+ }
521
+ });
522
+ const format = options.format ?? "console";
523
+ if (format === "json") {
524
+ console.log(formatJson(results));
525
+ } else {
526
+ const output = formatConsole(results);
527
+ console.log(output);
528
+ }
529
+ if (hasErrors(results)) {
530
+ process.exit(1);
531
+ }
532
+ });
533
+ program.parse();
534
+ //# sourceMappingURL=cli.js.map