@textlint-ja/textlint-rule-preset-ai-writing
Version:
textlintプリセット:AIっぽい記述パターンを検出し、より自然な日本語表現を促すルール集
120 lines (110 loc) • 4.39 kB
text/typescript
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;