Skip to content

Commit 1704a94

Browse files
committed
feat(ルール): 新しい「no-ai-emphasis-patterns」ルールを追加
- 絵文字と太字の組み合わせや情報系プレフィックスの太字を検出 - 機械的な表現を避け、より自然な日本語表現を促進 - README.mdのチェック機能を追加
1 parent 2f2163f commit 1704a94

File tree

5 files changed

+334
-2
lines changed

5 files changed

+334
-2
lines changed

.github/copilot-instructions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,6 @@ cd tmp
120120
# 特定のファイルを作成してテストする
121121
echo "これは機械的な表現です。" > test.md
122122
./node_modules/.bin/textlint --preset ai-writing test.md
123+
# README.mdのチェックを行う
124+
./node_modules/.bin/textlint --preset ai-writing ../README.md
123125
```

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import noAiListFormatting from "./rules/no-ai-list-formatting";
22
import noAiFormalExpressions from "./rules/no-ai-formal-expressions";
33
import noAiHypeExpressions from "./rules/no-ai-hype-expressions";
4+
import noAiEmphasisPatterns from "./rules/no-ai-emphasis-patterns";
45

56
const preset = {
67
rules: {
78
"no-ai-list-formatting": noAiListFormatting,
89
"no-ai-formal-expressions": noAiFormalExpressions,
9-
"no-ai-hype-expressions": noAiHypeExpressions
10+
"no-ai-hype-expressions": noAiHypeExpressions,
11+
"no-ai-emphasis-patterns": noAiEmphasisPatterns
1012
},
1113
rulesConfig: {
1214
"no-ai-list-formatting": true,
1315
"no-ai-formal-expressions": true,
14-
"no-ai-hype-expressions": true
16+
"no-ai-hype-expressions": true,
17+
"no-ai-emphasis-patterns": true
1518
}
1619
};
1720

src/rules/no-ai-emphasis-patterns.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import type { TextlintRuleModule } from "@textlint/types";
2+
import { matchPatterns } from "@textlint/regexp-string-matcher";
3+
4+
export interface Options {
5+
// 指定したパターンにマッチする場合、エラーを報告しません
6+
// 文字列または正規表現パターン ("/pattern/flags") で指定可能
7+
allows?: string[];
8+
// 絵文字と太字の組み合わせパターンの検出を無効にする
9+
disableEmojiEmphasisPatterns?: boolean;
10+
// 情報系プレフィックスパターンの検出を無効にする
11+
disableInfoPatterns?: boolean;
12+
}
13+
14+
const rule: TextlintRuleModule<Options> = (context, options = {}) => {
15+
const { Syntax, RuleError, report, getSource, locator } = context;
16+
const allows = options.allows ?? [];
17+
const disableEmojiEmphasisPatterns = options.disableEmojiEmphasisPatterns ?? false;
18+
const disableInfoPatterns = options.disableInfoPatterns ?? false;
19+
20+
// 機械的な情報系プレフィックス
21+
const infoPatterns = [
22+
"注意",
23+
"重要",
24+
"ポイント",
25+
"メモ",
26+
"参考",
27+
"補足",
28+
"確認",
29+
"チェック",
30+
"推奨",
31+
"おすすめ",
32+
"検出される例",
33+
"推奨される表現",
34+
"良い例",
35+
"悪い例",
36+
"例",
37+
"サンプル",
38+
"使用例",
39+
"設定例"
40+
];
41+
42+
return {
43+
[Syntax.Paragraph](node) {
44+
const text = getSource(node);
45+
46+
// allowsパターンにマッチする場合はスキップ
47+
if (allows.length > 0) {
48+
const matchedResults = matchPatterns(text, allows);
49+
if (matchedResults.length > 0) {
50+
return;
51+
}
52+
}
53+
54+
// リストアイテムの場合はListItemのハンドラーで処理するのでスキップ
55+
if (node.parent?.type === "ListItem") {
56+
return;
57+
}
58+
59+
let emojiEmphasizeMatches: RegExpExecArray[] = [];
60+
61+
// 絵文字 + 太字の組み合わせパターンを検出
62+
if (!disableEmojiEmphasisPatterns) {
63+
// 絵文字の正規表現を修正(サロゲートペア対応)
64+
const emojiEmphasizePattern = /([🔍💡📝📋📌🔗🎯🚀💯🔥📊📈])\s*\*\*([^*]+)\*\*/g;
65+
66+
let match;
67+
while ((match = emojiEmphasizePattern.exec(text)) !== null) {
68+
const matchStart = match.index;
69+
const matchEnd = match.index + match[0].length;
70+
71+
emojiEmphasizeMatches.push(match);
72+
73+
report(
74+
node,
75+
new RuleError(
76+
`絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。`,
77+
{
78+
padding: locator.range([matchStart, matchEnd])
79+
}
80+
)
81+
);
82+
}
83+
}
84+
85+
// 情報系プレフィックスの太字パターンを検出(絵文字+太字と重複しない場合のみ)
86+
if (!disableInfoPatterns) {
87+
const infoPrefixPattern = new RegExp(`\\*\\*(${infoPatterns.join("|")})([::].*?)?\\*\\*`, "g");
88+
89+
let match;
90+
while ((match = infoPrefixPattern.exec(text)) !== null) {
91+
const matchStart = match.index;
92+
const matchEnd = match.index + match[0].length;
93+
const prefixText = match[1];
94+
95+
// 絵文字+太字のマッチと重複していないかチェック
96+
const isOverlapping = emojiEmphasizeMatches.some((emojiMatch) => {
97+
const emojiStart = emojiMatch.index!;
98+
const emojiEnd = emojiMatch.index! + emojiMatch[0].length;
99+
return matchStart < emojiEnd && matchEnd > emojiStart;
100+
});
101+
102+
if (!isOverlapping) {
103+
report(
104+
node,
105+
new RuleError(
106+
`「**${prefixText}**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。`,
107+
{
108+
padding: locator.range([matchStart, matchEnd])
109+
}
110+
)
111+
);
112+
}
113+
}
114+
}
115+
},
116+
117+
[Syntax.ListItem](node) {
118+
const text = getSource(node);
119+
120+
// allowsパターンにマッチする場合はスキップ
121+
if (allows.length > 0) {
122+
const matchedResults = matchPatterns(text, allows);
123+
if (matchedResults.length > 0) {
124+
return;
125+
}
126+
}
127+
128+
let emojiEmphasizeMatches: RegExpExecArray[] = [];
129+
130+
// リストアイテム内での絵文字 + 太字パターンを検出
131+
if (!disableEmojiEmphasisPatterns) {
132+
const emojiEmphasizePattern = /([🔍💡📝📋📌🔗🎯🚀💯🔥📊📈])\s*\*\*([^*]+)\*\*/g;
133+
134+
let match;
135+
while ((match = emojiEmphasizePattern.exec(text)) !== null) {
136+
const matchStart = match.index;
137+
const matchEnd = match.index + match[0].length;
138+
139+
emojiEmphasizeMatches.push(match);
140+
141+
report(
142+
node,
143+
new RuleError(
144+
`リストアイテムで絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。`,
145+
{
146+
padding: locator.range([matchStart, matchEnd])
147+
}
148+
)
149+
);
150+
}
151+
}
152+
153+
// リストアイテム内での情報系プレフィックスの太字パターンを検出
154+
if (!disableInfoPatterns) {
155+
const infoPrefixPattern = new RegExp(`\\*\\*(${infoPatterns.join("|")})([::].*?)?\\*\\*`, "g");
156+
157+
let match;
158+
while ((match = infoPrefixPattern.exec(text)) !== null) {
159+
const matchStart = match.index;
160+
const matchEnd = match.index + match[0].length;
161+
const prefixText = match[1];
162+
163+
// 絵文字+太字のマッチと重複していないかチェック
164+
const isOverlapping = emojiEmphasizeMatches.some((emojiMatch) => {
165+
const emojiStart = emojiMatch.index!;
166+
const emojiEnd = emojiMatch.index! + emojiMatch[0].length;
167+
return matchStart < emojiEnd && matchEnd > emojiStart;
168+
});
169+
170+
if (!isOverlapping) {
171+
report(
172+
node,
173+
new RuleError(
174+
`リストアイテムで「**${prefixText}**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。`,
175+
{
176+
padding: locator.range([matchStart, matchEnd])
177+
}
178+
)
179+
);
180+
}
181+
}
182+
}
183+
}
184+
};
185+
};
186+
187+
export default rule;

test/index.test.ts

Whitespace-only changes.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import TextLintTester from "textlint-tester";
2+
import noAiEmphasisPatterns from "../../src/rules/no-ai-emphasis-patterns";
3+
4+
const tester = new TextLintTester();
5+
6+
tester.run("no-ai-emphasis-patterns", noAiEmphasisPatterns, {
7+
valid: [
8+
// 絵文字のみ(太字なし)
9+
"ℹ️ これは情報です",
10+
"🔍 検索結果",
11+
"✅ 完了",
12+
13+
// 太字のみ(絵文字なし)
14+
"**重要な項目**",
15+
"**注意事項**",
16+
17+
// 自然な表現
18+
"重要な項目について説明します",
19+
"注意が必要な点をお伝えします",
20+
"次の手順で進めてください",
21+
22+
// allowsオプションでのスキップ
23+
{
24+
text: "ℹ️ **注意**: この機能は重要です",
25+
options: {
26+
allows: ["/ℹ️.*注意/"]
27+
}
28+
}
29+
],
30+
invalid: [
31+
// 絵文字 + 太字の組み合わせ
32+
{
33+
text: "ℹ️ **注意**: この機能は重要です",
34+
errors: [
35+
{
36+
message:
37+
"絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
38+
}
39+
]
40+
},
41+
{
42+
text: "🔍 **検出される例**: サンプルコード",
43+
errors: [
44+
{
45+
message:
46+
"絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
47+
}
48+
]
49+
},
50+
{
51+
text: "✅ **推奨される表現**: この書き方がおすすめです",
52+
errors: [
53+
{
54+
message:
55+
"絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
56+
}
57+
]
58+
},
59+
60+
// 情報系プレフィックスの太字
61+
{
62+
text: "**注意**: この設定は重要です",
63+
errors: [
64+
{
65+
message:
66+
"「**注意**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
67+
}
68+
]
69+
},
70+
{
71+
text: "**重要**: 必ず確認してください",
72+
errors: [
73+
{
74+
message:
75+
"「**重要**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
76+
}
77+
]
78+
},
79+
{
80+
text: "**ポイント**: ここがキーになります",
81+
errors: [
82+
{
83+
message:
84+
"「**ポイント**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
85+
}
86+
]
87+
},
88+
{
89+
text: "**検出される例**: このパターンが問題です",
90+
errors: [
91+
{
92+
message:
93+
"「**検出される例**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
94+
}
95+
]
96+
},
97+
{
98+
text: "**推奨される表現**: このように書きましょう",
99+
errors: [
100+
{
101+
message:
102+
"「**推奨される表現**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
103+
}
104+
]
105+
},
106+
107+
// リストアイテム内での検出
108+
{
109+
text: "- ℹ️ **注意**: リスト内でも検出されます",
110+
errors: [
111+
{
112+
message:
113+
"リストアイテムで絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
114+
}
115+
]
116+
},
117+
118+
// コロン付きパターン
119+
{
120+
text: "**注意:** この機能は廃止予定です",
121+
errors: [
122+
{
123+
message:
124+
"「**注意**」のような太字の情報プレフィックスは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
125+
}
126+
]
127+
},
128+
129+
// 複数パターンの組み合わせ
130+
{
131+
text: "🔥 **重要**: パフォーマンスが向上します",
132+
errors: [
133+
{
134+
message:
135+
"絵文字と太字の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。"
136+
}
137+
]
138+
}
139+
]
140+
});

0 commit comments

Comments
 (0)