From 53ee83911deb06b5974a01e13b9be753ab603ee7 Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 22:15:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20add=20katakana/half-width=20kana=20?= =?UTF-8?q?variants=20to=20phrase=20patterns,=20add=20=E3=81=8D=E3=81=9F?= =?UTF-8?q?=E8=AA=9E=E5=B9=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rework the mora building blocks as self-contained regex fragments (character class for plain mora, alternation for voiced mora like ど/ご/ぐ) so they can be concatenated freely with stem()/bare() - along the way, fix a latent bug: DO was previously the character class [どドド], which (being a class, not an alternation) also accepted a bare half-width ト or a lone combining dakuten as valid matches on their own; it's now the alternation (?:ど|ド|ド) - extend phrase-kakke/phrase-kakkoyo/phrase-egui/phrase-do-doshita/ phrase-cho/phrase-mattaku/phrase-omoroi/phrase-sugoi and the そういう part of phrase-sonna-nori to also match katakana and half-width kana spellings, matching how the original stem patterns already worked - left phrase-joudan-desu-yan/phrase-hisshi-yan as literal strings since they contain kanji that can't be swapped for kana variants directly - add stem-kita (きた/キタ/キタ + w), matching how the existing stems work --- src/lib/patterns.ts | 83 ++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 4c49c2d..6496d83 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -8,23 +8,45 @@ 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 DO = "(?:ど|ド|ド)"; +const E = "[えぇエェエェ]"; +const GO = "(?:ご|ゴ|ゴ)"; +const GU = "(?:ぐ|グ|グ)"; +const I = "[いぃイィイィ]"; +const KA = "[かカカ]"; +const KE = "[けケケ]"; +const KI = "[きキキ]"; +const KO = "[こココ]"; +const KU = "[くクク]"; +const MO = "[もモモ]"; +const MU = "[むムム]"; +const NA = "[なナナ]"; +const O = "[おぉオォオォ]"; +const RO = "[ろロロ]"; +const SA = "[さササ]"; +const SHI = "[しシシ]"; +const SMALL_TSU = "[っッッ]"; +const SMALL_YO = "[ょョョ]"; +const SO = "[そソソ]"; +const SU = "[すスス]"; +const TA = "[たタタ]"; +const U = "[うぅウゥウゥ]"; +const WA = "[わゎワヮワ]"; +const YO = "[よヨヨ]"; // 語幹の後に続く「伸ばし棒/促音の繰り返し」+「!/?」+「w/笑/爆笑/(笑)」 const STEM_SUFFIX = "[-ーー~っッッ]*[!!??]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; -const stem = (a: string, b: string) => `[${a}][${b}]${STEM_SUFFIX}`; +// モーラ断片を連結して語幹+STEM_SUFFIXの正規表現ソースを作る +const stem = (...parts: string[]) => parts.join("") + STEM_SUFFIX; +// 語尾(w/笑など)を要求しない語幹単体(伸ばし棒の繰り返しのみ許容) +const bare = (...parts: string[]) => parts.join("") + "[-ーー~っッッ]*"; export const patterns: PatternDefinition[] = [ // --- 絵文字 (strict) --- @@ -114,20 +136,11 @@ export const patterns: PatternDefinition[] = [ { 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-kita", strict: true, source: stem(KI, TA), samples: ["きたーw"] }, // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- - { - id: "bare-uo", - strict: false, - source: `[${U}][${O}][-ーー~っッッ]*`, - samples: ["うお"], - }, - { - id: "bare-dowa", - strict: false, - source: `[${DO}][${WA}][-ーー~っッッ]*`, - samples: ["どわ"], - }, + { id: "bare-uo", strict: false, source: bare(U, O), samples: ["うお"] }, + { id: "bare-dowa", strict: false, source: bare(DO, WA), samples: ["どわ"] }, { id: "bare-bakushou", strict: false, source: "爆笑", samples: ["爆笑"] }, { id: "bare-reishou", strict: false, source: "冷笑", samples: ["冷笑"] }, @@ -154,28 +167,30 @@ export const patterns: PatternDefinition[] = [ // --- フレーズ系(strict) --- // 元ネタ: https://note.com/kido_meigen/n/nc0fb2d47f6f6 / https://w.atwiki.jp/reisyou/pages/10.html + // 「冗談ですやん」「必死やん」は漢字を含み仮名の読み替えが素直に作れないため + // カタカナ/半角カナ対応は見送り、リテラルのまま。 { id: "phrase-kakke", strict: true, - source: `かっけ${STEM_SUFFIX}`, + source: stem(KA, SMALL_TSU, KE), samples: ["かっけーw"], }, { id: "phrase-kakkoyo", strict: true, - source: `かっこよ${STEM_SUFFIX}`, + source: stem(KA, SMALL_TSU, KO, YO), samples: ["かっこよw"], }, { id: "phrase-egui", strict: true, - source: `えぐ${STEM_SUFFIX}`, + source: stem(E, GU), samples: ["えぐー!笑"], }, { id: "phrase-do-doshita", strict: true, - source: `ど、?どした${STEM_SUFFIX}`, + source: stem(DO, "[、,]?", DO, SHI, TA), samples: ["ど、どした?笑"], }, { @@ -193,7 +208,7 @@ export const patterns: PatternDefinition[] = [ { id: "phrase-sonna-nori", strict: true, - source: `そういうノリ[…\\.・]*${STEM_SUFFIX}`, + source: stem(SO, U, I, U, "ノリ[…\\.・]*"), samples: ["そういうノリ...w"], }, @@ -201,25 +216,25 @@ export const patterns: PatternDefinition[] = [ { id: "phrase-cho", strict: false, - source: `ちょ${STEM_SUFFIX}`, + source: stem(CHI, SMALL_YO), samples: ["ちょw"], }, { id: "phrase-mattaku", strict: false, - source: `ったく${STEM_SUFFIX}`, + source: stem(SMALL_TSU, TA, KU), samples: ["ったくw"], }, { id: "phrase-omoroi", strict: false, - source: `おもろいな[あぁ]?${STEM_SUFFIX}`, + source: stem(O, MO, RO, I, NA, `${A}?`), samples: ["おもろいなあww"], }, { id: "phrase-sugoi", strict: false, - source: `すごいな[あぁ]?${STEM_SUFFIX}`, + source: stem(SU, GO, I, NA, `${A}?`), samples: ["すごいなあww"], }, ]; From d5159b8a585d8674530b2d0eaa1fa6576cf9c60f Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 22:22:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20add=20missing=20VS16=20variants,=20a?= =?UTF-8?q?dd=20=E3=81=84=E3=82=84/=E3=82=84=E3=82=93=20patterns,=20?= =?UTF-8?q?=E3=83=8E=E3=83=AA=20kana=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - emoji-bang (❗) / emoji-question (❓) were missing the optional \u{FE0F} variation selector that ‼️/⁉️ already had, so text copied with the selector attached (❗️) relied on incidental match behavior instead of being handled explicitly; made consistent with the other two - also add ❗/❓ to STEM_SUFFIX's own punctuation set for consistency - add stem-iya (いや + suffix, relaxed-only: too common standalone to be strict) - generalize the "やん" phrases: instead of two literal-word patterns (必死やん / 冗談ですやん), add one phrase-yan pattern for "(です)?やん" + suffix regardless of the preceding word, since that's the actual productive construction - phrase-sonna-nori's "ノリ" now also matches のり/ノリ via NO/RI fragments instead of being a fixed katakana literal --- src/lib/patterns.ts | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 6496d83..8296a25 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -27,7 +27,9 @@ const KU = "[くクク]"; const MO = "[もモモ]"; const MU = "[むムム]"; const NA = "[なナナ]"; +const NO = "[のノノ]"; const O = "[おぉオォオォ]"; +const RI = "[りリリ]"; const RO = "[ろロロ]"; const SA = "[さササ]"; const SHI = "[しシシ]"; @@ -38,15 +40,18 @@ const SU = "[すスス]"; const TA = "[たタタ]"; const U = "[うぅウゥウゥ]"; const WA = "[わゎワヮワ]"; +const YA = "[やヤヤ]"; const YO = "[よヨヨ]"; // 語幹の後に続く「伸ばし棒/促音の繰り返し」+「!/?」+「w/笑/爆笑/(笑)」 const STEM_SUFFIX = - "[-ーー~っッッ]*[!!??]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; + "[-ーー~っッッ]*[!!??❗❓]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; // モーラ断片を連結して語幹+STEM_SUFFIXの正規表現ソースを作る const stem = (...parts: string[]) => parts.join("") + STEM_SUFFIX; // 語尾(w/笑など)を要求しない語幹単体(伸ばし棒の繰り返しのみ許容) const bare = (...parts: string[]) => parts.join("") + "[-ーー~っッッ]*"; +// 「(です)やん」で終わる冷笑フレーズ共通のビルダー +const yan = (word = "") => `${word}(?:です)?やん${STEM_SUFFIX}`; export const patterns: PatternDefinition[] = [ // --- 絵文字 (strict) --- @@ -68,11 +73,16 @@ export const patterns: PatternDefinition[] = [ source: "\\u{203C}\\u{FE0F}?", samples: ["本当‼️"], }, - { id: "emoji-bang", strict: true, source: "\\u{2757}", samples: ["早く❗"] }, + { + id: "emoji-bang", + strict: true, + source: "\\u{2757}\\u{FE0F}?", + samples: ["早く❗"], + }, { id: "emoji-question", strict: true, - source: "\\u{2753}", + source: "\\u{2753}\\u{FE0F}?", samples: ["は❓"], }, { @@ -138,6 +148,9 @@ export const patterns: PatternDefinition[] = [ { 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: bare(U, O), samples: ["うお"] }, { id: "bare-dowa", strict: false, source: bare(DO, WA), samples: ["どわ"] }, @@ -166,9 +179,6 @@ export const patterns: PatternDefinition[] = [ }, // --- フレーズ系(strict) --- - // 元ネタ: https://note.com/kido_meigen/n/nc0fb2d47f6f6 / https://w.atwiki.jp/reisyou/pages/10.html - // 「冗談ですやん」「必死やん」は漢字を含み仮名の読み替えが素直に作れないため - // カタカナ/半角カナ対応は見送り、リテラルのまま。 { id: "phrase-kakke", strict: true, @@ -193,22 +203,18 @@ export const patterns: PatternDefinition[] = [ source: stem(DO, "[、,]?", DO, SHI, TA), samples: ["ど、どした?笑"], }, + // 「必死やんw」「冗談ですやんw」など、前の語を問わず「(です)やん」+ 語尾で + // 冷笑的な相槌として使われる構文 { - id: "phrase-joudan-desu-yan", - strict: true, - source: `冗談ですやん${STEM_SUFFIX}`, - samples: ["冗談ですやん!!w"], - }, - { - id: "phrase-hisshi-yan", + id: "phrase-yan", strict: true, - source: `必死やん${STEM_SUFFIX}`, - samples: ["必死やんww"], + source: yan(), + samples: ["必死ですやんw", "冗談やんw"], }, { id: "phrase-sonna-nori", strict: true, - source: stem(SO, U, I, U, "ノリ[…\\.・]*"), + source: stem(SO, U, I, U, NO, RI, "[…\\.・]*"), samples: ["そういうノリ...w"], }, From 2d28c6b90bee78da298eff71b5628b0ee9488503 Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 22:28:17 +0900 Subject: [PATCH 3/4] docs: show emoji next to source, rewrite samples as realistic examples - each emoji pattern's source line now has a trailing comment with the actual emoji character, since removing the label field made the raw \u{...} escapes hard to recognize at a glance - rewrite every pattern's samples from minimal test fragments into realistic Twitter-style example sentences, so they double as readable documentation of how each pattern actually gets triggered in the wild --- src/lib/patterns.ts | 193 +++++++++++++++++++++++++++++++------------- test/regex.test.ts | 1 - 2 files changed, 137 insertions(+), 57 deletions(-) diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 8296a25..17085e4 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -26,6 +26,7 @@ const KO = "[こココ]"; const KU = "[くクク]"; const MO = "[もモモ]"; const MU = "[むムム]"; +const N = "[んンン]"; const NA = "[なナナ]"; const NO = "[のノノ]"; const O = "[おぉオォオォ]"; @@ -58,124 +59,204 @@ export const patterns: PatternDefinition[] = [ { 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: ["早く❗"], + source: "\\u{2757}\\u{FE0F}?", // ❗ + samples: ["それはさすがに無理があるだろ❗"], }, { id: "emoji-question", strict: true, - source: "\\u{2753}\\u{FE0F}?", - samples: ["は❓"], + 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}", - samples: ["それな😏"], + 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) --- { id: "stem-kichi", strict: true, 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-kita", strict: true, source: stem(KI, TA), samples: ["きたーw"] }, + 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(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"] }, + { + id: "stem-iya", + strict: false, + source: stem(I, YA), + samples: ["いやwそれは草すぎるでしょ"], + }, // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- - { id: "bare-uo", strict: false, source: bare(U, O), samples: ["うお"] }, - { id: "bare-dowa", strict: false, source: bare(DO, WA), samples: ["どわ"] }, - { id: "bare-bakushou", strict: false, source: "爆笑", samples: ["爆笑"] }, - { id: "bare-reishou", strict: false, source: "冷笑", samples: ["冷笑"] }, + { + id: "bare-uo", + strict: false, + source: bare(U, O), + samples: ["うお、うお、しか言えなくなってて草"], + }, + { + id: "bare-dowa", + strict: false, + source: bare(DO, WA), + 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) --- @@ -183,25 +264,25 @@ export const patterns: PatternDefinition[] = [ id: "phrase-kakke", strict: true, source: stem(KA, SMALL_TSU, KE), - samples: ["かっけーw"], + samples: ["かっけーwイキっててウケる"], }, { id: "phrase-kakkoyo", strict: true, source: stem(KA, SMALL_TSU, KO, YO), - samples: ["かっこよw"], + samples: ["かっこよwナルシストかよ"], }, { id: "phrase-egui", strict: true, source: stem(E, GU), - samples: ["えぐー!笑"], + samples: ["その自己評価えぐー!笑"], }, { id: "phrase-do-doshita", strict: true, - source: stem(DO, "[、,]?", DO, SHI, TA), - samples: ["ど、どした?笑"], + source: stem(DO, "[、,]?", DO, SHI, TA, `${N}?`), + samples: ["ど、どした?笑 急に早口になって"], }, // 「必死やんw」「冗談ですやんw」など、前の語を問わず「(です)やん」+ 語尾で // 冷笑的な相槌として使われる構文 @@ -209,13 +290,13 @@ export const patterns: PatternDefinition[] = [ id: "phrase-yan", strict: true, source: yan(), - samples: ["必死ですやんw", "冗談やんw"], + samples: ["それめっちゃ必死ですやんw", "冗談やんwノリ悪いなあ"], }, { id: "phrase-sonna-nori", strict: true, source: stem(SO, U, I, U, NO, RI, "[…\\.・]*"), - samples: ["そういうノリ...w"], + samples: ["あぁ、そういうノリ...w理解した"], }, // --- フレーズ系(relaxedのみ: 単体では冷笑以外の文脈でも頻出するため) --- @@ -223,24 +304,24 @@ export const patterns: PatternDefinition[] = [ id: "phrase-cho", strict: false, source: stem(CHI, SMALL_YO), - samples: ["ちょw"], + samples: ["ちょwそれは草すぎる"], }, { id: "phrase-mattaku", strict: false, source: stem(SMALL_TSU, TA, KU), - samples: ["ったくw"], + samples: ["ったくwしょうがないやつだな"], }, { id: "phrase-omoroi", strict: false, source: stem(O, MO, RO, I, NA, `${A}?`), - samples: ["おもろいなあww"], + samples: ["おもろいなあwそのノリ嫌いじゃない"], }, { id: "phrase-sugoi", strict: false, source: stem(SU, GO, I, NA, `${A}?`), - samples: ["すごいなあww"], + 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: ["どわー(笑)"] }, // 複数マッチ From 780a1273196f133b2c3d294f2f8e289119a69343 Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 22:37:00 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20kana=20variants=20for=20=E3=81=A7?= =?UTF-8?q?=E3=81=99/=E3=82=84=E3=82=93,=20allow=20repeated=20=E3=81=9F=20?= =?UTF-8?q?in=20stem-ita?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - yan(): です/やん were literal strings; now built from DE/SU/YA/N mora fragments so katakana/half-width spellings (デス、ヤン、デスヤン...) match too - stem-ita: い+た was a single fixed pair, but the actual expression repeats (いたたたた / あいたたた); now optional あ prefix + one-or-more た repetitions --- src/lib/patterns.ts | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 17085e4..58d6fba 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -14,6 +14,7 @@ export interface PatternDefinition { // 文字クラスでは表現できない)。 const A = "[あぁアァアァ]"; const CHI = "[ちチチ]"; +const DE = "(?:で|デ|デ)"; const DO = "(?:ど|ド|ド)"; const E = "[えぇエェエェ]"; const GO = "(?:ご|ゴ|ゴ)"; @@ -52,7 +53,7 @@ const stem = (...parts: string[]) => parts.join("") + STEM_SUFFIX; // 語尾(w/笑など)を要求しない語幹単体(伸ばし棒の繰り返しのみ許容) const bare = (...parts: string[]) => parts.join("") + "[-ーー~っッッ]*"; // 「(です)やん」で終わる冷笑フレーズ共通のビルダー -const yan = (word = "") => `${word}(?:です)?やん${STEM_SUFFIX}`; +const yan = (word = "") => `${word}(?:${DE}${SU})?${YA}${N}${STEM_SUFFIX}`; export const patterns: PatternDefinition[] = [ // --- 絵文字 (strict) --- @@ -72,19 +73,19 @@ export const patterns: PatternDefinition[] = [ id: "emoji-double-exclamation", strict: true, source: "\\u{203C}\\u{FE0F}?", // ‼️ - samples: ["は?そんなことある‼️"], + samples: ["そんな‼️"], }, { id: "emoji-bang", strict: true, source: "\\u{2757}\\u{FE0F}?", // ❗ - samples: ["それはさすがに無理があるだろ❗"], + samples: ["いいね❗"], }, { id: "emoji-question", strict: true, source: "\\u{2753}\\u{FE0F}?", // ❓ - samples: ["は❓意味わからんのだが"], + samples: ["は❓"], }, { id: "emoji-interrobang", @@ -96,19 +97,19 @@ export const patterns: PatternDefinition[] = [ id: "emoji-eye-roll", strict: true, source: "\\u{1F644}", // 🙄 - samples: ["はいはい、また同じ話🙄"], + samples: ["はいはい🙄"], }, { id: "emoji-smirk", strict: true, source: "\\u{1F60F}", // 😏 - samples: ["それな、知ってた😏"], + samples: ["それな😏"], }, { id: "emoji-clown", strict: true, source: "\\u{1F921}", // 🤡 - samples: ["自分だけ気づいてない🤡"], + samples: ["ふっ🤡"], }, // --- 絵文字 (relaxedのみ: 単体だと冷笑と断定しづらいもの) --- @@ -148,7 +149,7 @@ export const patterns: PatternDefinition[] = [ id: "stem-kichi", strict: true, source: stem(KI, CHI), - samples: ["きちーw急に早口になってて草"], + samples: ["きちーw"], }, { id: "stem-ou", @@ -160,43 +161,43 @@ export const patterns: PatternDefinition[] = [ id: "stem-uo", strict: true, source: stem(U, O), - samples: ["うおw急にキレ出してて草"], + samples: ["うおw"], }, { id: "stem-dowa", strict: true, source: stem(DO, WA), - samples: ["どわーwww必死すぎん"], + samples: ["どわーwww"], }, { id: "stem-uwa", strict: true, source: stem(U, WA), - samples: ["うわw自分で気づいてないんかな"], + samples: ["うわーw"], }, { id: "stem-samu", strict: true, source: stem(SA, MU), - samples: ["そのノリさむw誰も乗ってないよ"], + samples: ["そのノリさむw"], }, { id: "stem-ita", strict: true, - source: stem(I, TA), - samples: ["それいたw自覚ないの草"], + source: stem(`${A}?`, I, `${TA}+`), + samples: ["アイタタタタw"], }, { id: "stem-kimo", strict: true, source: stem(KI, MO), - samples: ["その言い方きもwドン引きだわ"], + samples: ["きもwドン引きだわ"], }, { id: "stem-kita", strict: true, source: stem(KI, TA), - samples: ["きたーw予想通りの反応で草"], + samples: ["キター!!!!w"], }, // --- 語幹 + w/笑/爆笑/(笑) (relaxedのみ: 単体では冷笑以外の文脈でも頻出するため) --- @@ -204,7 +205,7 @@ export const patterns: PatternDefinition[] = [ id: "stem-iya", strict: false, source: stem(I, YA), - samples: ["いやwそれは草すぎるでしょ"], + samples: ["いやwそれは草"], }, // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- @@ -290,12 +291,12 @@ export const patterns: PatternDefinition[] = [ id: "phrase-yan", strict: true, source: yan(), - samples: ["それめっちゃ必死ですやんw", "冗談やんwノリ悪いなあ"], + samples: ["めっちゃ必死やんw", "冗談やんwノリ悪いなあ", "冗談ですやんw"], }, { id: "phrase-sonna-nori", strict: true, - source: stem(SO, U, I, U, NO, RI, "[…\\.・]*"), + source: stem(SO, U, I, U, NO, RI, "[…\\.・・]*"), samples: ["あぁ、そういうノリ...w理解した"], }, @@ -316,7 +317,7 @@ export const patterns: PatternDefinition[] = [ id: "phrase-omoroi", strict: false, source: stem(O, MO, RO, I, NA, `${A}?`), - samples: ["おもろいなあwそのノリ嫌いじゃない"], + samples: ["おもろいなあwキミw"], }, { id: "phrase-sugoi",