Skip to content

Commit cab7fb4

Browse files
committed
feat: 見出し冒頭で連番を使うパターンの検知
1 parent c7da636 commit cab7fb4

File tree

4 files changed

+207
-2
lines changed

4 files changed

+207
-2
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,34 @@ command
256256
- `disableQuote`: `true`にするとコロン後の引用検出を無効にする
257257
- `disableTable`: `true`にするとコロン後のテーブル検出を無効にする
258258

259+
### no-ai-heading-numbers
260+
261+
AIが機械的に生成しがちな、見出しに連番を含めるパターンを検出します。
262+
263+
#### 検出される例
264+
265+
```markdown
266+
# 1. タイトル
267+
## 2. セクション
268+
### 3.1. サブセクション
269+
#### 4) 項目
270+
```
271+
272+
#### より自然な表現
273+
274+
```markdown
275+
# タイトル
276+
## セクション
277+
### サブセクション
278+
#### 項目
279+
```
280+
281+
#### オプション
282+
283+
- `allows`: 指定したパターンにマッチする場合、エラーを報告しません
284+
- 文字列: `"許可したいテキスト"`
285+
- 正規表現: `"/パターン/フラグ"` (例: `"/\\d+\\. .*/i"`)
286+
259287
### ai-tech-writing-guideline
260288

261289
テクニカルライティングのベストプラクティスに基づいて、文書品質の改善提案を行います。
@@ -335,6 +363,9 @@ command
335363
"disableQuote": false,
336364
"disableTable": false
337365
},
366+
"no-ai-heading-numbers": {
367+
"allows": ["許可したいテキスト", "/正規表現パターン/"]
368+
},
338369
"ai-tech-writing-guideline": {
339370
"severity": "info", // サジェストとして扱う
340371
"allows": ["許可したいテキスト", "/正規表現パターン/"],

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import aiTechWritingGuideline from "./rules/ai-tech-writing-guideline";
22
import noAiColonContinuation from "./rules/no-ai-colon-continuation";
33
import noAiEmphasisPatterns from "./rules/no-ai-emphasis-patterns";
4+
import noAiHeadingNumbers from "./rules/no-ai-heading-numbers";
45
import noAiHypeExpressions from "./rules/no-ai-hype-expressions";
56
import noAiListFormatting from "./rules/no-ai-list-formatting";
67

@@ -10,7 +11,8 @@ const preset = {
1011
"no-ai-hype-expressions": noAiHypeExpressions,
1112
"no-ai-emphasis-patterns": noAiEmphasisPatterns,
1213
"ai-tech-writing-guideline": aiTechWritingGuideline,
13-
"no-ai-colon-continuation": noAiColonContinuation
14+
"no-ai-colon-continuation": noAiColonContinuation,
15+
"no-ai-heading-numbers": noAiHeadingNumbers
1416
},
1517
rulesConfig: {
1618
"no-ai-list-formatting": true,
@@ -19,7 +21,8 @@ const preset = {
1921
"ai-tech-writing-guideline": {
2022
severity: "info"
2123
},
22-
"no-ai-colon-continuation": true
24+
"no-ai-colon-continuation": true,
25+
"no-ai-heading-numbers": true
2326
}
2427
};
2528

src/rules/no-ai-heading-numbers.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { matchPatterns } from "@textlint/regexp-string-matcher";
2+
import type { TextlintRuleContext, TextlintRuleModule } from "@textlint/types";
3+
4+
type Options = {
5+
// If node's text includes allowed patterns, does not report.
6+
// Can be string or RegExp-like string ("/pattern/flags")
7+
allows?: string[];
8+
};
9+
10+
const rule: TextlintRuleModule<Options> = (context: TextlintRuleContext, options = {}) => {
11+
const { Syntax, RuleError, report, getSource, locator } = context;
12+
const allows = options.allows ?? [];
13+
14+
// Pattern to detect numbering at the beginning of headings
15+
// Matches patterns like: "1. ", "2. ", "3.1. ", "1) ", etc.
16+
const numberingPattern = /^\s*(\d+(?:\.\d+)*[.)]\s+)/;
17+
18+
return {
19+
[Syntax.Header](node) {
20+
// Use raw text which includes the heading marker (#)
21+
const text = node.raw || getSource(node);
22+
23+
// Check if text matches any allowed patterns
24+
if (allows.length > 0) {
25+
const matches = matchPatterns(text, allows);
26+
if (matches.length > 0) {
27+
return;
28+
}
29+
}
30+
31+
// For raw text, we need to skip the heading marker first
32+
// Extract just the heading content (after # and space)
33+
const headingContentMatch = text.match(/^#{1,6}\s+(.+)$/);
34+
const headingContent = headingContentMatch ? headingContentMatch[1] : text;
35+
36+
const match = headingContent.match(numberingPattern);
37+
if (match) {
38+
const numberPart = match[1];
39+
const matchStart = match.index ?? 0;
40+
const matchEnd = matchStart + numberPart.length;
41+
42+
report(
43+
node,
44+
new RuleError(
45+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
46+
{
47+
padding: locator.range([matchStart, matchEnd])
48+
}
49+
)
50+
);
51+
}
52+
}
53+
};
54+
};
55+
56+
export default rule;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import TextLintTester from "textlint-tester";
2+
import noAiHeadingNumbers from "../../src/rules/no-ai-heading-numbers";
3+
4+
const tester = new TextLintTester();
5+
6+
tester.run("no-ai-heading-numbers", noAiHeadingNumbers, {
7+
valid: [
8+
// Normal headings without numbering
9+
"# タイトル",
10+
"## セクション",
11+
"### サブセクション",
12+
"#### 詳細項目",
13+
"##### 更に詳細な項目",
14+
"###### 最小レベルの見出し",
15+
// Headings with text that happens to start with numbers (but no dot/paren)
16+
"# 2025年の振り返り",
17+
"## 3つのポイント",
18+
// Allowed patterns (string)
19+
{
20+
text: "# 1. イントロダクション",
21+
options: {
22+
allows: ["1. イントロダクション"]
23+
}
24+
},
25+
// Allowed patterns (RegExp-like string)
26+
{
27+
text: "## 2. 背景",
28+
options: {
29+
allows: ["/\\d+\\. .*/"]
30+
}
31+
},
32+
// Allowed patterns (case insensitive)
33+
{
34+
text: "### 3.1. サブセクション",
35+
options: {
36+
allows: ["/\\d+\\.\\d+\\. .*/i"]
37+
}
38+
}
39+
],
40+
invalid: [
41+
// Simple numbering patterns
42+
{
43+
text: "# 1. タイトル",
44+
errors: [
45+
{
46+
message:
47+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
48+
range: [0, 3]
49+
}
50+
]
51+
},
52+
{
53+
text: "## 2. セクション",
54+
errors: [
55+
{
56+
message:
57+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
58+
range: [0, 3]
59+
}
60+
]
61+
},
62+
{
63+
text: "### 10. 10番目のアイテム",
64+
errors: [
65+
{
66+
message:
67+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
68+
range: [0, 4]
69+
}
70+
]
71+
},
72+
// Hierarchical numbering (1.1, 1.2, etc.)
73+
{
74+
text: "### 3.1. サブセクション",
75+
errors: [
76+
{
77+
message:
78+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
79+
range: [0, 5]
80+
}
81+
]
82+
},
83+
{
84+
text: "#### 1.2.3. 深い階層",
85+
errors: [
86+
{
87+
message:
88+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
89+
range: [0, 7]
90+
}
91+
]
92+
},
93+
// Parenthesis style numbering
94+
{
95+
text: "# 1) はじめに",
96+
errors: [
97+
{
98+
message:
99+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
100+
range: [0, 3]
101+
}
102+
]
103+
},
104+
{
105+
text: "## 2) 背景",
106+
errors: [
107+
{
108+
message:
109+
"見出しに連番を含めるパターンは機械的な印象を与える可能性があります。連番の必要性を検討してみてください。",
110+
range: [0, 3]
111+
}
112+
]
113+
}
114+
]
115+
});

0 commit comments

Comments
 (0)