Skip to content

Commit e68dc7d

Browse files
authored
feat: コロン(:)と箇条書きの機械的組み合わせパターンの検出機能を追加 (#9)
feat: コロン(:)と箇条書きの機械的組み合わせパターンの検出機能を追加 (#9) - ai-tech-writing-guideline ルールに新しい検出パターンを追加 - Document レベルでParagraph → List の連続パターンを解析 - 段落末尾がコロン(:、:)で終わり直後にリストが続く場合を検出 - 建設的で中立的なメッセージで自然な表現への改善を提案 - 既存の巨大な処理を関数に分割してモジュール化し保守性を向上 - テストケースを追加して機能の動作を保証 Closes #8
1 parent 73524ec commit e68dc7d

File tree

3 files changed

+231
-68
lines changed

3 files changed

+231
-68
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
applyTo: '**/*.ts'
3+
---
4+
5+
6+
## テスト
7+
8+
完全なテストは次のように行います。
9+
README.mdのチェックをtextlintで行い、ルールのテストは`textlint-tester`を使用します。
10+
11+
```
12+
npm test
13+
```
14+
15+
ルールのテストは`textlint-tester`を使用して、ルールのテストを実装する
16+
17+
Unit Testの実行方法
18+
19+
```bash
20+
npm run test:unit
21+
```
22+
23+
特定のルールのみをUnit Testする場合は、以下のように実行します。
24+
25+
```bash
26+
npm run test:unit -- --grep no-repetitive-expressions
27+
```
28+
29+
実際に `textlint` コマンドを使ってルールを適用する場合は、以下のように実行します。
30+
31+
```
32+
npm test
33+
```
34+
35+
## ダミーファイルを使ったテスト
36+
37+
- Git管理下にダミーファイルは作らない
38+
- `tmp/` ディレクトリを作成し、そこにダミーファイルを配置してテストを行う
39+
40+
```
41+
#! /bin/bash
42+
set -ex
43+
44+
mkdir -p tmp/
45+
npm install --save-dev . textlint technological-book-corpus-ja --prefix tmp/
46+
cd tmp
47+
# ダミーファイルを作成
48+
echo "ダミーファイル" > dummy.md
49+
./node_modules/.bin/textlint --preset @textlint-ja/ai-writing dummy.md
50+
```

src/rules/ai-tech-writing-guideline.ts

Lines changed: 151 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -186,90 +186,173 @@ const rule: TextlintRuleModule<Options> = (context, options = {}) => {
186186
structure: 0
187187
};
188188

189-
return {
190-
[Syntax.Paragraph](node) {
191-
// StringSourceを使用してコードブロックを除外したテキストを取得
192-
const source = new StringSource(node, {
193-
replacer({ node, emptyValue }) {
194-
// コードブロック、インラインコードを除外
195-
if (node.type === "Code" || node.type === "InlineCode") {
196-
return emptyValue();
189+
/**
190+
* コロン(:)で終わる段落の直後に箇条書きが続くパターンを検出
191+
*/
192+
const detectColonListPattern = (node: any) => {
193+
const children = node.children || [];
194+
195+
for (let i = 0; i < children.length - 1; i++) {
196+
const currentNode = children[i];
197+
const nextNode = children[i + 1];
198+
199+
// Paragraph → List のパターンを検出
200+
if (currentNode.type === "Paragraph" && nextNode.type === "List") {
201+
// Paragraphの最後の文字列ノードを取得
202+
const paragraphSource = new StringSource(currentNode, {
203+
replacer({ node, emptyValue }) {
204+
if (node.type === "Code" || node.type === "InlineCode") {
205+
return emptyValue();
206+
}
207+
return undefined;
208+
}
209+
});
210+
const paragraphText = paragraphSource.toString();
211+
212+
// 「:」で終わる段落の後にリストが続く場合を検出
213+
if (/[:][\s]*$/.test(paragraphText.trim())) {
214+
// 許可パターンのチェック
215+
if (allows.length > 0) {
216+
const matches = matchPatterns(paragraphText, allows);
217+
if (matches.length > 0) {
218+
continue;
219+
}
197220
}
198-
return undefined;
221+
222+
documentQualityMetrics.structure++;
223+
hasDocumentIssues = true;
224+
225+
report(currentNode, {
226+
message:
227+
"【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。"
228+
});
199229
}
200-
});
201-
const text = source.toString();
230+
}
231+
}
232+
};
233+
234+
/**
235+
* 構造化ガイダンスに関する文書レベルの検出処理
236+
*/
237+
const processDocumentStructureGuidance = (node: any) => {
238+
if (disableStructureGuidance) {
239+
return;
240+
}
202241

203-
// 許可パターンのチェック
204-
if (allows.length > 0) {
205-
const matches = matchPatterns(text, allows);
206-
if (matches.length > 0) {
207-
return;
242+
// コロン + 箇条書きパターンの検出
243+
detectColonListPattern(node);
244+
// 将来的にここに他の文書レベルの構造化パターンを追加できます
245+
// 例:
246+
// detectExcessiveNestedLists(node);
247+
// detectInconsistentHeadingStructure(node);
248+
// detectPoorSectionOrganization(node);
249+
// detectInconsistentListFormatting(node);
250+
};
251+
252+
/**
253+
* 段落内のガイダンスパターンを検出・報告
254+
*/
255+
const processParagraphGuidance = (node: any) => {
256+
// StringSourceを使用してコードブロックを除外したテキストを取得
257+
const source = new StringSource(node, {
258+
replacer({ node, emptyValue }) {
259+
// コードブロック、インラインコードを除外
260+
if (node.type === "Code" || node.type === "InlineCode") {
261+
return emptyValue();
208262
}
263+
return undefined;
209264
}
265+
});
266+
const text = source.toString();
210267

211-
// 各カテゴリのガイダンスを統合
212-
const allGuidancePatterns = [
213-
...(disableRedundancyGuidance ? [] : redundancyGuidance),
214-
...(disableVoiceGuidance ? [] : voiceGuidance),
215-
...(disableClarityGuidance ? [] : clarityGuidance),
216-
...(disableConsistencyGuidance ? [] : consistencyGuidance),
217-
...(disableStructureGuidance ? [] : structureGuidance)
218-
];
268+
// 許可パターンのチェック
269+
if (allows.length > 0) {
270+
const matches = matchPatterns(text, allows);
271+
if (matches.length > 0) {
272+
return;
273+
}
274+
}
219275

220-
for (const { pattern, message, category } of allGuidancePatterns) {
221-
const matches = text.matchAll(pattern);
222-
for (const match of matches) {
223-
const index = match.index ?? 0;
276+
// 各カテゴリのガイダンスを統合
277+
const allGuidancePatterns = [
278+
...(disableRedundancyGuidance ? [] : redundancyGuidance),
279+
...(disableVoiceGuidance ? [] : voiceGuidance),
280+
...(disableClarityGuidance ? [] : clarityGuidance),
281+
...(disableConsistencyGuidance ? [] : consistencyGuidance),
282+
...(disableStructureGuidance ? [] : structureGuidance)
283+
];
224284

225-
// プレーンテキストの位置を元のノード内の位置に変換
226-
const originalIndex = source.originalIndexFromIndex(index);
227-
const originalEndIndex = source.originalIndexFromIndex(index + match[0].length);
285+
for (const { pattern, message, category } of allGuidancePatterns) {
286+
const matches = text.matchAll(pattern);
287+
for (const match of matches) {
288+
const index = match.index ?? 0;
228289

229-
if (originalIndex !== undefined && originalEndIndex !== undefined) {
230-
const originalRange = [originalIndex, originalEndIndex] as const;
290+
// プレーンテキストの位置を元のノード内の位置に変換
291+
const originalIndex = source.originalIndexFromIndex(index);
292+
const originalEndIndex = source.originalIndexFromIndex(index + match[0].length);
231293

232-
// カテゴリ別のメトリクスを更新
233-
documentQualityMetrics[category as keyof typeof documentQualityMetrics]++;
234-
hasDocumentIssues = true;
294+
if (originalIndex !== undefined && originalEndIndex !== undefined) {
295+
const originalRange = [originalIndex, originalEndIndex] as const;
235296

236-
report(node, {
237-
message: message,
238-
padding: locator.range(originalRange)
239-
});
240-
}
241-
}
242-
}
243-
},
244-
[Syntax.DocumentExit](node) {
245-
// 文書全体の分析を実行(enableDocumentAnalysisがtrueの場合)
246-
if (enableDocumentAnalysis && hasDocumentIssues) {
247-
const totalIssues = Object.values(documentQualityMetrics).reduce((sum, count) => sum + count, 0);
297+
// カテゴリ別のメトリクスを更新
298+
documentQualityMetrics[category as keyof typeof documentQualityMetrics]++;
299+
hasDocumentIssues = true;
248300

249-
// カテゴリ別の詳細な分析結果を含むメッセージを生成
250-
const categoryDetails = [];
251-
if (documentQualityMetrics.redundancy > 0) {
252-
categoryDetails.push(`簡潔性: ${documentQualityMetrics.redundancy}件`);
253-
}
254-
if (documentQualityMetrics.voice > 0) {
255-
categoryDetails.push(`明確性: ${documentQualityMetrics.voice}件`);
256-
}
257-
if (documentQualityMetrics.clarity > 0) {
258-
categoryDetails.push(`具体性: ${documentQualityMetrics.clarity}件`);
259-
}
260-
if (documentQualityMetrics.consistency > 0) {
261-
categoryDetails.push(`一貫性: ${documentQualityMetrics.consistency}件`);
262-
}
263-
if (documentQualityMetrics.structure > 0) {
264-
categoryDetails.push(`構造化: ${documentQualityMetrics.structure}件`);
301+
report(node, {
302+
message: message,
303+
padding: locator.range(originalRange)
304+
});
265305
}
306+
}
307+
}
308+
};
266309

267-
const detailsText = categoryDetails.length > 0 ? ` [内訳: ${categoryDetails.join(", ")}]` : "";
310+
/**
311+
* 文書全体の品質分析結果を報告
312+
*/
313+
const processDocumentAnalysis = (node: any) => {
314+
// 文書全体の分析を実行(enableDocumentAnalysisがtrueの場合)
315+
if (enableDocumentAnalysis && hasDocumentIssues) {
316+
const totalIssues = Object.values(documentQualityMetrics).reduce((sum, count) => sum + count, 0);
268317

269-
report(node, {
270-
message: `【テクニカルライティング品質分析】この文書で${totalIssues}件の改善提案が見つかりました${detailsText}。効果的なテクニカルライティングの7つのC(Clear, Concise, Correct, Coherent, Concrete, Complete, Courteous)の原則に基づいて見直しを検討してください。詳細なガイドライン: https://github.com/textlint-ja/textlint-rule-preset-ai-writing/blob/main/docs/tech-writing-guidelines.md`
271-
});
318+
// カテゴリ別の詳細な分析結果を含むメッセージを生成
319+
const categoryDetails = [];
320+
if (documentQualityMetrics.redundancy > 0) {
321+
categoryDetails.push(`簡潔性: ${documentQualityMetrics.redundancy}件`);
322+
}
323+
if (documentQualityMetrics.voice > 0) {
324+
categoryDetails.push(`明確性: ${documentQualityMetrics.voice}件`);
325+
}
326+
if (documentQualityMetrics.clarity > 0) {
327+
categoryDetails.push(`具体性: ${documentQualityMetrics.clarity}件`);
328+
}
329+
if (documentQualityMetrics.consistency > 0) {
330+
categoryDetails.push(`一貫性: ${documentQualityMetrics.consistency}件`);
331+
}
332+
if (documentQualityMetrics.structure > 0) {
333+
categoryDetails.push(`構造化: ${documentQualityMetrics.structure}件`);
272334
}
335+
336+
const detailsText = categoryDetails.length > 0 ? ` [内訳: ${categoryDetails.join(", ")}]` : "";
337+
338+
report(node, {
339+
message: `【テクニカルライティング品質分析】この文書で${totalIssues}件の改善提案が見つかりました${detailsText}。効果的なテクニカルライティングの7つのC(Clear, Concise, Correct, Coherent, Concrete, Complete, Courteous)の原則に基づいて見直しを検討してください。詳細なガイドライン: https://github.com/textlint-ja/textlint-rule-preset-ai-writing/blob/main/docs/tech-writing-guidelines.md`
340+
});
341+
}
342+
};
343+
344+
return {
345+
[Syntax.Document](node) {
346+
// 文書レベルの構造化ガイダンス処理
347+
processDocumentStructureGuidance(node);
348+
},
349+
[Syntax.Paragraph](node) {
350+
// 段落内のガイダンスパターンを検出・報告
351+
processParagraphGuidance(node);
352+
},
353+
[Syntax.DocumentExit](node) {
354+
// 文書全体の品質分析結果を報告
355+
processDocumentAnalysis(node);
273356
}
274357
};
275358
};

test/rules/ai-tech-writing-guideline.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ tester.run("ai-tech-writing-guideline", rule, {
1717
{
1818
text: "システムがデータを検証します。エラーが発生した場合、ログファイルに記録されます。",
1919
options: { enableDocumentAnalysis: false }
20+
},
21+
// 自然な箇条書きの導入例
22+
{
23+
text: "Vueのリアクティビティシステムは確かに便利ですが、その仕組みの見えにくさが気になります。\nたとえば、次のような点が見えにくいと感じます。\n\n- refとreactiveの使い分けが最初は分からない。",
24+
options: { enableDocumentAnalysis: false }
25+
},
26+
{
27+
text: "JSXはJavaScriptの中でUIを記述するため、プログラマーにとって理解しやすいです。\nたとえば、JSXの次のような点がわかりやすいと思っています。\n\n- 条件分岐やループは通常のJavaScriptの記法",
28+
options: { enableDocumentAnalysis: false }
2029
}
2130
],
2231
invalid: [
@@ -105,6 +114,27 @@ tester.run("ai-tech-writing-guideline", rule, {
105114
}
106115
]
107116
},
117+
// 構造化の問題(コロンと箇条書きの組み合わせ)
118+
{
119+
text: "Vueのリアクティビティシステムは確かに便利ですが、その仕組みの見えにくさが気になります。例えば:\n\n- refとreactiveの使い分けが最初は分からない。",
120+
options: { enableDocumentAnalysis: false },
121+
errors: [
122+
{
123+
message:
124+
"【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。"
125+
}
126+
]
127+
},
128+
{
129+
text: "JSXはJavaScriptの中でUIを記述するため、プログラマーにとって理解しやすいです:\n\n- 条件分岐やループは通常のJavaScriptの記法",
130+
options: { enableDocumentAnalysis: false },
131+
errors: [
132+
{
133+
message:
134+
"【構造化】コロン(:)で終わる文の直後の箇条書きは機械的な印象を与える可能性があります。「たとえば、次のような点があります。」のような導入文を使った自然な表現を検討してください。"
135+
}
136+
]
137+
},
108138
// 複数の問題が同時に存在する場合
109139
{
110140
text: "まず最初に高速なパフォーマンスの実装を実施することができます。",

0 commit comments

Comments
 (0)