UNPKG

@textlint-ja/textlint-rule-preset-ai-writing

Version:

textlintプリセット:AIっぽい記述パターンを検出し、より自然な日本語表現を促すルール集

120 lines (110 loc) 4.39 kB
import { matchPatterns } from "@textlint/regexp-string-matcher"; import type { TextlintRuleContext, TextlintRuleModule } from "@textlint/types"; type Options = { // If node's text includes allowed patterns, does not report. // Can be string or RegExp-like string ("/pattern/flags") allows?: string[]; // Disable specific pattern checks disableBoldListItems?: boolean; disableEmojiListItems?: boolean; }; const rule: TextlintRuleModule<Options> = (context: TextlintRuleContext, options = {}) => { const { Syntax, RuleError, report, getSource, locator } = context; const allows = options.allows ?? []; const disableBoldListItems = options.disableBoldListItems ?? false; const disableEmojiListItems = options.disableEmojiListItems ?? false; // Pattern to detect "flashy" emojis commonly used in AI-generated content // Using explicit list of decorative emojis that give mechanical impression const flashyEmojis = [ // Check marks and status indicators "✅", "❌", "⭐", "✨", "💯", // Warning and attention symbols "⚠️", "❗", "❓", "💥", // Energy and fire symbols "🔥", "⚡", "💪", "🚀", // Ideas and thinking "💡", "🤔", "💭", "🧠", // Targets and goals "🎯", "📈", "📊", "🏆", // Common decorative symbols "👍", "👎", "😊", "😎", "🎉", "🌟", // Work and productivity "📝", "📋", "✏️", "🖊️", "💼" ]; // Create pattern from explicit emoji list // Need to escape special regex characters const escapedEmojis = flashyEmojis.map((emoji) => emoji.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); const flashyEmojiPattern = new RegExp(`(${escapedEmojis.join("|")})`); return { [Syntax.ListItem](node) { const text = getSource(node); // Check if text matches any allowed patterns if (allows.length > 0) { const matches = matchPatterns(text, allows); if (matches.length > 0) { return; } } // Check for bold list item pattern: - **text**: description if (!disableBoldListItems) { const boldListPattern = /^[\s]*[-*+]\s+\*\*([^*]+)\*\*\s*([::])/; const boldMatch: RegExpMatchArray | null = text.match(boldListPattern); if (boldMatch) { const matchStart: number = boldMatch.index ?? 0; const matchEnd = matchStart + boldMatch[0].length; const matchRange = [matchStart, matchEnd] as const; const colon = boldMatch[2]; const ruleError = new RuleError( `リストアイテムで強調(**)とコロン(${colon})の組み合わせは機械的な印象を与える可能性があります。より自然な表現を検討してください。`, { padding: locator.range(matchRange) } ); report(node, ruleError); } } // Check for emoji list items if (!disableEmojiListItems) { const emojiMatch: RegExpMatchArray | null = text.match(flashyEmojiPattern); if (emojiMatch) { const emoji = emojiMatch[0]; const emojiIndex: number = emojiMatch.index ?? 0; const matchRange = [emojiIndex, emojiIndex + emoji.length] as const; const ruleError = new RuleError( `リストアイテムでの絵文字「${emoji}」の使用は、読み手によっては機械的な印象を与える場合があります。テキストベースの表現も検討してみてください。`, { padding: locator.range(matchRange) } ); report(node, ruleError); } } } }; }; export default rule;