diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 4c49c2d..58d6fba 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -8,97 +8,141 @@ export interface PatternDefinition { samples: string[]; } -// 語幹の異表記(ひらがな/カタカナ/半角カナ)をまとめた文字クラス -const KI = "きキキ"; -const CHI = "ちチチ"; -const O = "おぉオォオォ"; -const U = "うぅウゥウゥ"; -const DO = "どドド"; -const WA = "わゎワヮワ"; -const SA = "さササ"; -const MU = "むムム"; -const I = "いぃイィイィ"; -const TA = "たタタ"; -const MO = "もモモ"; +// モーラ単位の異表記(ひらがな/カタカナ/半角カナ)。そのまま連結できるように +// 単独モーラは文字クラス `[...]`、半角に濁点合成が必要なモーラ(が/ざ/だ行など) +// は `(?:...)` の選択構造にしている(半角カナの濁点は「ト」+「゙」の2文字のため +// 文字クラスでは表現できない)。 +const A = "[あぁアァアァ]"; +const CHI = "[ちチチ]"; +const DE = "(?:で|デ|デ)"; +const DO = "(?:ど|ド|ド)"; +const E = "[えぇエェエェ]"; +const GO = "(?:ご|ゴ|ゴ)"; +const GU = "(?:ぐ|グ|グ)"; +const I = "[いぃイィイィ]"; +const KA = "[かカカ]"; +const KE = "[けケケ]"; +const KI = "[きキキ]"; +const KO = "[こココ]"; +const KU = "[くクク]"; +const MO = "[もモモ]"; +const MU = "[むムム]"; +const N = "[んンン]"; +const NA = "[なナナ]"; +const NO = "[のノノ]"; +const O = "[おぉオォオォ]"; +const RI = "[りリリ]"; +const RO = "[ろロロ]"; +const SA = "[さササ]"; +const SHI = "[しシシ]"; +const SMALL_TSU = "[っッッ]"; +const SMALL_YO = "[ょョョ]"; +const SO = "[そソソ]"; +const SU = "[すスス]"; +const TA = "[たタタ]"; +const U = "[うぅウゥウゥ]"; +const WA = "[わゎワヮワ]"; +const YA = "[やヤヤ]"; +const YO = "[よヨヨ]"; // 語幹の後に続く「伸ばし棒/促音の繰り返し」+「!/?」+「w/笑/爆笑/(笑)」 const STEM_SUFFIX = - "[-ーー~っッッ]*[!!??]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; -const stem = (a: string, b: string) => `[${a}][${b}]${STEM_SUFFIX}`; + "[-ーー~っッッ]*[!!??❗❓]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; +// モーラ断片を連結して語幹+STEM_SUFFIXの正規表現ソースを作る +const stem = (...parts: string[]) => parts.join("") + STEM_SUFFIX; +// 語尾(w/笑など)を要求しない語幹単体(伸ばし棒の繰り返しのみ許容) +const bare = (...parts: string[]) => parts.join("") + "[-ーー~っッッ]*"; +// 「(です)やん」で終わる冷笑フレーズ共通のビルダー +const yan = (word = "") => `${word}(?:${DE}${SU})?${YA}${N}${STEM_SUFFIX}`; export const patterns: PatternDefinition[] = [ // --- 絵文字 (strict) --- { id: "emoji-sweat-smile", strict: true, - source: "\\u{1F605}", - samples: ["これは😅です"], + source: "\\u{1F605}", // 😅 + samples: ["それはさすがに草😅"], }, { id: "emoji-rofl", strict: true, - source: "\\u{1F923}", - samples: ["爆笑🤣爆笑"], + source: "\\u{1F923}", // 🤣 + samples: ["それ何回同じネタやってんの🤣🤣"], }, { id: "emoji-double-exclamation", strict: true, - source: "\\u{203C}\\u{FE0F}?", - samples: ["本当‼️"], + source: "\\u{203C}\\u{FE0F}?", // ‼️ + samples: ["そんな‼️"], + }, + { + id: "emoji-bang", + strict: true, + source: "\\u{2757}\\u{FE0F}?", // ❗ + samples: ["いいね❗"], }, - { id: "emoji-bang", strict: true, source: "\\u{2757}", samples: ["早く❗"] }, { id: "emoji-question", strict: true, - source: "\\u{2753}", + source: "\\u{2753}\\u{FE0F}?", // ❓ samples: ["は❓"], }, { id: "emoji-interrobang", strict: true, - source: "\\u{2049}\\u{FE0F}?", - samples: ["は⁉️"], + source: "\\u{2049}\\u{FE0F}?", // ⁉️ + samples: ["まじで言ってる⁉️"], }, { id: "emoji-eye-roll", strict: true, - source: "\\u{1F644}", - samples: ["は?🙄"], + source: "\\u{1F644}", // 🙄 + samples: ["はいはい🙄"], }, { id: "emoji-smirk", strict: true, - source: "\\u{1F60F}", + source: "\\u{1F60F}", // 😏 samples: ["それな😏"], }, { id: "emoji-clown", strict: true, - source: "\\u{1F921}", - samples: ["🤡だなw"], + source: "\\u{1F921}", // 🤡 + samples: ["ふっ🤡"], }, // --- 絵文字 (relaxedのみ: 単体だと冷笑と断定しづらいもの) --- { id: "emoji-sweat-drop", strict: false, - source: "\\u{1F4A6}", - samples: ["いや💦"], + source: "\\u{1F4A6}", // 💦 + samples: ["それはさすがに草だわ💦"], }, { id: "emoji-expressionless", strict: false, - source: "\\u{1F611}", - samples: ["😑"], + source: "\\u{1F611}", // 😑 + samples: ["……😑"], }, { id: "emoji-upside-down", strict: false, - source: "\\u{1F643}", - samples: ["🙃"], + source: "\\u{1F643}", // 🙃 + samples: ["はいはい、そうですね🙃"], + }, + { + id: "emoji-skull", + strict: false, + source: "\\u{1F480}", // 💀 + samples: ["その理論はさすがに無理💀"], + }, + { + id: "emoji-melting", + strict: false, + source: "\\u{1FAE0}", // 🫠 + samples: ["見てるだけでしんど🫠"], }, - { id: "emoji-skull", strict: false, source: "\\u{1F480}", samples: ["💀"] }, - { id: "emoji-melting", strict: false, source: "\\u{1FAE0}", samples: ["🫠"] }, // --- 語幹 + w/笑/爆笑/(笑) (strict) --- { @@ -107,119 +151,178 @@ export const patterns: PatternDefinition[] = [ source: stem(KI, CHI), samples: ["きちーw"], }, - { id: "stem-ou", strict: true, source: stem(O, U), samples: ["お、おうw"] }, - { id: "stem-uo", strict: true, source: stem(U, O), samples: ["うおw"] }, - { id: "stem-dowa", strict: true, source: stem(DO, WA), samples: ["どわーw"] }, - { id: "stem-uwa", strict: true, source: stem(U, WA), samples: ["うわw"] }, - { id: "stem-samu", strict: true, source: stem(SA, MU), samples: ["さむw"] }, - { id: "stem-ita", strict: true, source: stem(I, TA), samples: ["いたw"] }, - { id: "stem-kimo", strict: true, source: stem(KI, MO), samples: ["きもw"] }, + { + id: "stem-ou", + strict: true, + source: stem(O, U), + samples: ["お、おうwそれは良かったな"], + }, + { + id: "stem-uo", + strict: true, + source: stem(U, O), + samples: ["うおw"], + }, + { + id: "stem-dowa", + strict: true, + source: stem(DO, WA), + samples: ["どわーwww"], + }, + { + id: "stem-uwa", + strict: true, + source: stem(U, WA), + samples: ["うわーw"], + }, + { + id: "stem-samu", + strict: true, + source: stem(SA, MU), + samples: ["そのノリさむw"], + }, + { + id: "stem-ita", + strict: true, + source: stem(`${A}?`, I, `${TA}+`), + samples: ["アイタタタタw"], + }, + { + id: "stem-kimo", + strict: true, + source: stem(KI, MO), + samples: ["きもwドン引きだわ"], + }, + { + id: "stem-kita", + strict: true, + source: stem(KI, TA), + samples: ["キター!!!!w"], + }, + + // --- 語幹 + w/笑/爆笑/(笑) (relaxedのみ: 単体では冷笑以外の文脈でも頻出するため) --- + { + id: "stem-iya", + strict: false, + source: stem(I, YA), + samples: ["いやwそれは草"], + }, // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- { id: "bare-uo", strict: false, - source: `[${U}][${O}][-ーー~っッッ]*`, - samples: ["うお"], + source: bare(U, O), + samples: ["うお、うお、しか言えなくなってて草"], }, { id: "bare-dowa", strict: false, - source: `[${DO}][${WA}][-ーー~っッッ]*`, - samples: ["どわ"], + source: bare(DO, WA), + samples: ["どわ…しか反応できてなくて草"], + }, + { + id: "bare-bakushou", + strict: false, + source: "爆笑", + samples: ["その返し思わず爆笑してしまった"], + }, + { + id: "bare-reishou", + strict: false, + source: "冷笑", + samples: ["これが世に言う冷笑ってやつか"], }, - { id: "bare-bakushou", strict: false, source: "爆笑", samples: ["爆笑"] }, - { id: "bare-reishou", strict: false, source: "冷笑", samples: ["冷笑"] }, // --- 繰り返しパターン (strict) --- { id: "repeat-bakushou", strict: true, source: "(?:爆笑){2,}", - samples: ["爆笑爆笑"], + samples: ["その言い訳マジで爆笑爆笑"], }, { id: "repeat-reishou", strict: true, source: "(?:冷笑){2,}", - samples: ["冷笑冷笑"], + samples: ["これぞ正統派の冷笑冷笑という感じ"], + }, + { + id: "repeat-warai", + strict: true, + source: "(?:笑){2,}", + samples: ["それは草生えるわ笑笑"], }, - { id: "repeat-warai", strict: true, source: "(?:笑){2,}", samples: ["笑笑"] }, { id: "paren-warai", strict: true, source: "[((]笑[))]", - samples: ["(笑)", "(笑)"], + samples: ["はいはい、すごいですね(笑)"], }, // --- フレーズ系(strict) --- - // 元ネタ: https://note.com/kido_meigen/n/nc0fb2d47f6f6 / https://w.atwiki.jp/reisyou/pages/10.html { id: "phrase-kakke", strict: true, - source: `かっけ${STEM_SUFFIX}`, - samples: ["かっけーw"], + source: stem(KA, SMALL_TSU, KE), + samples: ["かっけーwイキっててウケる"], }, { id: "phrase-kakkoyo", strict: true, - source: `かっこよ${STEM_SUFFIX}`, - samples: ["かっこよw"], + source: stem(KA, SMALL_TSU, KO, YO), + samples: ["かっこよwナルシストかよ"], }, { id: "phrase-egui", strict: true, - source: `えぐ${STEM_SUFFIX}`, - samples: ["えぐー!笑"], + source: stem(E, GU), + samples: ["その自己評価えぐー!笑"], }, { id: "phrase-do-doshita", strict: true, - source: `ど、?どした${STEM_SUFFIX}`, - samples: ["ど、どした?笑"], - }, - { - id: "phrase-joudan-desu-yan", - strict: true, - source: `冗談ですやん${STEM_SUFFIX}`, - samples: ["冗談ですやん!!w"], + source: stem(DO, "[、,]?", DO, SHI, TA, `${N}?`), + samples: ["ど、どした?笑 急に早口になって"], }, + // 「必死やんw」「冗談ですやんw」など、前の語を問わず「(です)やん」+ 語尾で + // 冷笑的な相槌として使われる構文 { - id: "phrase-hisshi-yan", + id: "phrase-yan", strict: true, - source: `必死やん${STEM_SUFFIX}`, - samples: ["必死やんww"], + source: yan(), + samples: ["めっちゃ必死やんw", "冗談やんwノリ悪いなあ", "冗談ですやんw"], }, { id: "phrase-sonna-nori", strict: true, - source: `そういうノリ[…\\.・]*${STEM_SUFFIX}`, - samples: ["そういうノリ...w"], + source: stem(SO, U, I, U, NO, RI, "[…\\.・・]*"), + samples: ["あぁ、そういうノリ...w理解した"], }, // --- フレーズ系(relaxedのみ: 単体では冷笑以外の文脈でも頻出するため) --- { id: "phrase-cho", strict: false, - source: `ちょ${STEM_SUFFIX}`, - samples: ["ちょw"], + source: stem(CHI, SMALL_YO), + samples: ["ちょwそれは草すぎる"], }, { id: "phrase-mattaku", strict: false, - source: `ったく${STEM_SUFFIX}`, - samples: ["ったくw"], + source: stem(SMALL_TSU, TA, KU), + samples: ["ったくwしょうがないやつだな"], }, { id: "phrase-omoroi", strict: false, - source: `おもろいな[あぁ]?${STEM_SUFFIX}`, - samples: ["おもろいなあww"], + source: stem(O, MO, RO, I, NA, `${A}?`), + samples: ["おもろいなあwキミw"], }, { id: "phrase-sugoi", strict: false, - source: `すごいな[あぁ]?${STEM_SUFFIX}`, - samples: ["すごいなあww"], + source: stem(SU, GO, I, NA, `${A}?`), + samples: ["すごいなあwwキミ見損なったわ"], }, ]; diff --git a/test/regex.test.ts b/test/regex.test.ts index 9ffef78..729c282 100644 --- a/test/regex.test.ts +++ b/test/regex.test.ts @@ -47,7 +47,6 @@ const cases: Case[] = [ { content: "どわー爆笑爆笑", expected: true }, { content: "お、おうw", expected: true }, { content: "きちーw", expected: true }, - // (笑) は語幹込みで一致する(旧実装では語幹が欠落するバグがあった) { content: "うお(笑)", expected: ["うお(笑)"] }, { content: "どわー(笑)", expected: ["どわー(笑)"] }, // 複数マッチ