From 3bd18a1741566945e025d6afd189b9f10085e988 Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 21:50:32 +0900 Subject: [PATCH 1/2] feat!: switch to an options-object API and data-driven patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: findAll/contains take an options object instead of a positional boolean. `findAll(text, true)` -> `findAll(text, { relaxed: true })` - src/lib/patterns.ts: patterns are now declared as data (id/label/strict/ source/samples) instead of being baked into one giant regex literal, and each pattern's samples auto-generate coverage in test/patterns.test.ts - src/lib/regex.ts: builds regexStrict/regexRelaxed from the patterns list; strict patterns are always ordered before relaxed-only ones so greedy matches (e.g. "(?:爆笑){2,}") aren't shadowed by shorter relaxed-only alternatives - fixed a latent bug where the "(笑)" branch after a stem required a trailing space, causing findAll to drop the stem from the match (e.g. findAll("うお(笑)") returned ["(笑)"] instead of ["うお(笑)"]) - added new strict patterns (うわ/さむ/いた/きも, 🙄/😏/🤡, 笑2回以上) and new relaxed-only patterns (😑/🙃/💀/🫠) - README/CONTRIBUTING updated for the new API and the patterns.ts workflow --- CONTRIBUTING.md | 8 +- README.md | 12 ++- src/index.ts | 31 +++++- src/lib/patterns.ts | 230 ++++++++++++++++++++++++++++++++++++++++++ src/lib/regex.ts | 33 +++--- test/patterns.test.ts | 18 ++++ test/regex.test.ts | 9 +- 7 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 src/lib/patterns.ts create mode 100644 test/patterns.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bc7dff..077d8c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,11 +16,11 @@ PRに適切なラベルを付与してください ## 説明 -- 冷笑のパターン(regex)を追加する場合 - - [./src/lib/regex.ts](./src/lib/regex.ts) にパターンを追加してください - - [テストケース](./test/regex.test.ts) に該当の冷笑を追加してください +- 冷笑のパターンを追加する場合 + - [./src/lib/patterns.ts](./src/lib/patterns.ts) の `patterns` 配列に `PatternDefinition` を1つ追加してください(`id` / `label` / `strict` / `source` / `samples`) + - `samples` に書いたサンプル文字列は自動でテスト化されます([test/patterns.test.ts](./test/patterns.test.ts))。個別にテストコードを書く必要はありません - 可能であれば **ひらがな** , **カタカナ** , **半角カナ** , **小文字** も同様に含めてください - - 追加するパターンが明らかに冷笑な場合は `regexStrict` のほうに、冷笑か怪しい場合や文脈によっては冷笑ではないパターンは `relaxedOnly` に追加してください + - 追加するパターンが明らかに冷笑な場合は `strict: true` に、冷笑か怪しい場合や文脈によっては冷笑ではないパターンは `strict: false` (relaxedモードでのみ検知)にしてください - 機能の追加をする場合 - PRの説明欄に機能についての説明を記載してください diff --git a/README.md b/README.md index 438dc85..cea8d25 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,19 @@ const { findAll, contains } = require('dowa'); findAll('←うおw、爆笑'); // => ['うおw', '爆笑'] contains('うおw'); // => true // relaxed モード(検知範囲を拡大) -contains('どわー', true); +contains('どわー', { relaxed: true }); ``` +> [!Important] +> v2 で `relaxed` は第2引数の boolean からオプションオブジェクトに変わりました。 +> `findAll(text, true)` → `findAll(text, { relaxed: true })` + ## API -- `findAll(text: string, relaxed = false): string[] | null` — マッチした冷笑の配列を返すw(見つからなければ `null`) -- `contains(text: string, relaxed = false): boolean` — 冷笑が含まれるかを真偽値で返すw +- `findAll(text: string, options?: DowaOptions): string[] | null` — マッチした冷笑の配列を返すw(見つからなければ `null`) +- `contains(text: string, options?: DowaOptions): boolean` — 冷笑が含まれるかを真偽値で返すw +- `DowaOptions` + - `relaxed?: boolean` (デフォルト: `false`) — `true` で検知範囲を拡大する ## 貢献について diff --git a/src/index.ts b/src/index.ts index b1018d3..f3404e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,29 @@ import { regexStrict, regexRelaxed } from "./lib/regex"; -export function findAll(text: string, relaxed = false): string[] | null { +export interface DowaOptions { + /** 検知範囲を拡大するか (デフォルト: false) */ + relaxed?: boolean; +} + +/** + * テキスト中の冷笑パターンをすべて検出する。 + * @param text 検査対象の文字列 + * @param options.relaxed true で検知範囲を拡大する (デフォルト: false) + * @returns マッチした文字列の配列。見つからなければ null + */ +export function findAll( + text: string, + options: DowaOptions = {}, +): string[] | null { if (typeof text !== "string") { throw new TypeError('"text" must be a string.'); } + if (typeof options !== "object" || options === null) { + throw new TypeError('"options" must be an object.'); + } + const { relaxed = false } = options; if (typeof relaxed !== "boolean") { - throw new TypeError('"relaxed" must be a boolean.'); + throw new TypeError('"options.relaxed" must be a boolean.'); } const re = relaxed ? regexRelaxed : regexStrict; re.lastIndex = 0; @@ -13,8 +31,13 @@ export function findAll(text: string, relaxed = false): string[] | null { return m && m.length ? m : null; } -export function contains(text: string, relaxed = false): boolean { - return !!findAll(text, relaxed); +/** + * テキストに冷笑パターンが含まれるかを判定する。 + * @param text 検査対象の文字列 + * @param options.relaxed true で検知範囲を拡大する (デフォルト: false) + */ +export function contains(text: string, options: DowaOptions = {}): boolean { + return !!findAll(text, options); } export { regexStrict, regexRelaxed }; diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts new file mode 100644 index 0000000..77c1972 --- /dev/null +++ b/src/lib/patterns.ts @@ -0,0 +1,230 @@ +export interface PatternDefinition { + id: string; + label: string; + /** true: strict(既定)に含む / false: relaxedのときだけ含む */ + strict: boolean; + /** 正規表現ソース(フラグなし) */ + source: string; + /** マッチするはずのサンプル(自動テストに使用) */ + samples: string[]; +} + +// 語幹の異表記(ひらがな/カタカナ/半角カナ)をまとめた文字クラス +const KI = "きキキ"; +const CHI = "ちチチ"; +const O = "おぉオォオォ"; +const U = "うぅウゥウゥ"; +const DO = "どドド"; +const WA = "わゎワヮワ"; +const SA = "さササ"; +const MU = "むムム"; +const I = "いぃイィイィ"; +const TA = "たタタ"; +const MO = "もモモ"; + +// 語幹の後に続く「伸ばし棒/促音の繰り返し」+「w/笑/爆笑/(笑)」 +const STEM_SUFFIX = "[-ーー~っッッ]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; +const stem = (a: string, b: string) => `[${a}][${b}]${STEM_SUFFIX}`; + +export const patterns: PatternDefinition[] = [ + // --- 絵文字 (strict) --- + { + id: "emoji-sweat-smile", + label: "😅", + strict: true, + source: "\\u{1F605}", + samples: ["これは😅です"], + }, + { + id: "emoji-rofl", + label: "🤣", + strict: true, + source: "\\u{1F923}", + samples: ["爆笑🤣爆笑"], + }, + { + id: "emoji-double-exclamation", + label: "‼️", + strict: true, + source: "\\u{203C}\\u{FE0F}?", + samples: ["本当‼️?"], + }, + { + id: "emoji-eye-roll", + label: "🙄 (白目/呆れ)", + strict: true, + source: "\\u{1F644}", + samples: ["は?🙄"], + }, + { + id: "emoji-smirk", + label: "😏 (ニヤリ)", + strict: true, + source: "\\u{1F60F}", + samples: ["それな😏"], + }, + { + id: "emoji-clown", + label: "🤡 (道化=馬鹿にする)", + strict: true, + source: "\\u{1F921}", + samples: ["🤡だなw"], + }, + + // --- 絵文字 (relaxedのみ: 単体だと冷笑と断定しづらいもの) --- + { + id: "emoji-sweat-drop", + label: "💦", + strict: false, + source: "\\u{1F4A6}", + samples: ["いや💦"], + }, + { + id: "emoji-expressionless", + label: "😑", + strict: false, + source: "\\u{1F611}", + samples: ["😑"], + }, + { + id: "emoji-upside-down", + label: "🙃 (皮肉)", + strict: false, + source: "\\u{1F643}", + samples: ["🙃"], + }, + { + id: "emoji-skull", + label: "💀", + strict: false, + source: "\\u{1F480}", + samples: ["💀"], + }, + { + id: "emoji-melting", + label: "🫠", + strict: false, + source: "\\u{1FAE0}", + samples: ["🫠"], + }, + + // --- 語幹 + w/笑/爆笑/(笑) (strict, 既存4種) --- + { + id: "stem-kichi", + label: "きち", + strict: true, + source: stem(KI, CHI), + samples: ["きちーw"], + }, + { + id: "stem-ou", + label: "おう", + strict: true, + source: stem(O, U), + samples: ["お、おうw"], + }, + { + id: "stem-uo", + label: "うお", + strict: true, + source: stem(U, O), + samples: ["うおw"], + }, + { + id: "stem-dowa", + label: "どわ", + strict: true, + source: stem(DO, WA), + samples: ["どわーw"], + }, + + // --- 語幹 + w/笑/爆笑/(笑) (strict, 新規追加候補) --- + { + id: "stem-uwa", + label: "うわ", + strict: true, + source: stem(U, WA), + samples: ["うわw"], + }, + { + id: "stem-samu", + label: "さむ (寒い=しらける)", + strict: true, + source: stem(SA, MU), + samples: ["さむw"], + }, + { + id: "stem-ita", + label: "いた (痛い=イタい)", + strict: true, + source: stem(I, TA), + samples: ["いたw"], + }, + { + id: "stem-kimo", + label: "きも (気持ち悪い)", + strict: true, + source: stem(KI, MO), + samples: ["きもw"], + }, + + // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- + { + id: "bare-uo", + label: "うお (語尾なし)", + strict: false, + source: `[${U}][${O}][-ーー~っッッ]*`, + samples: ["うお"], + }, + { + id: "bare-dowa", + label: "どわ (語尾なし)", + strict: false, + source: `[${DO}][${WA}][-ーー~っッッ]*`, + samples: ["どわ"], + }, + { + id: "bare-bakushou", + label: "爆笑 (1回のみ、relaxedのみ)", + strict: false, + source: "爆笑", + samples: ["爆笑"], + }, + { + id: "bare-reishou", + label: "冷笑 (1回のみ、relaxedのみ)", + strict: false, + source: "冷笑", + samples: ["冷笑"], + }, + + // --- 繰り返しパターン (strict) --- + { + id: "repeat-bakushou", + label: "爆笑2回以上", + strict: true, + source: "(?:爆笑){2,}", + samples: ["爆笑爆笑"], + }, + { + id: "repeat-reishou", + label: "冷笑2回以上", + strict: true, + source: "(?:冷笑){2,}", + samples: ["冷笑冷笑"], + }, + { + id: "repeat-warai", + label: "笑2回以上 (新規)", + strict: true, + source: "(?:笑){2,}", + samples: ["笑笑"], + }, + { + id: "paren-warai", + label: "(笑)/(笑)", + strict: true, + source: "[((]笑[))]", + samples: ["(笑)", "(笑)"], + }, +]; diff --git a/src/lib/regex.ts b/src/lib/regex.ts index cbc008d..a02f828 100644 --- a/src/lib/regex.ts +++ b/src/lib/regex.ts @@ -1,21 +1,18 @@ -/** - * \u - * 1F605: 😅 - * 1F923: 🤣 - * 203C FE0F: ‼️ - */ -export const regexStrict: RegExp = - /(?:\u{1F605}|\u{1F923}|\u{203C}\u{FE0F}?|(?:[きキキ][ちチチ]|[おぉオォオォ][うぅウゥウゥ]|[うぅウゥウゥ][おぉオォオォ]|[どドド][わゎワヮ])[-ーー~っッッ]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))] )|(?:爆笑){2,}|(?:冷笑){2,}|[((]笑[))])/gu; -/** - * 1F4A6: 💦 - */ -export const relaxedOnly: RegExp = - /(?:[うぅウゥウゥ][おぉオォオォ]|[どドド][わゎワヮ])[-ーー~っッッ]*|爆笑|冷笑|(?:\u{1F4A6})/gu; +import { patterns } from "./patterns"; -function mergeRegex(r1: RegExp, r2: RegExp): RegExp { - const flags = Array.from(new Set((r1.flags + r2.flags).split(""))).join(""); - const src = `(?:${r1.source})|(?:${r2.source})`; - return new RegExp(src, flags); +function build(includeRelaxedOnly: boolean): RegExp { + // strict なパターンを常に先に並べる。正規表現の選択肢(|)は最初に + // マッチしたものを採用するため、順序が逆だと "爆笑" のような + // relaxed専用の短い一致が "(?:爆笑){2,}" より先に取られてしまう。 + const relevant = includeRelaxedOnly + ? [ + ...patterns.filter((p) => p.strict), + ...patterns.filter((p) => !p.strict), + ] + : patterns.filter((p) => p.strict); + const sources = relevant.map((p) => `(?:${p.source})`); + return new RegExp(sources.join("|"), "gu"); } -export const regexRelaxed: RegExp = mergeRegex(regexStrict, relaxedOnly); +export const regexStrict: RegExp = build(false); +export const regexRelaxed: RegExp = build(true); diff --git a/test/patterns.test.ts b/test/patterns.test.ts new file mode 100644 index 0000000..842f1c2 --- /dev/null +++ b/test/patterns.test.ts @@ -0,0 +1,18 @@ +import { describe, test, expect } from "vitest"; +import { contains } from "../src/index"; +import { patterns } from "../src/lib/patterns"; + +describe("パターン別サンプル", () => { + for (const p of patterns) { + describe(`[${p.id}] ${p.label}`, () => { + for (const sample of p.samples) { + test(`strict -> ${sample}`, () => { + expect(contains(sample)).toBe(p.strict); + }); + test(`relaxed -> ${sample}`, () => { + expect(contains(sample, { relaxed: true })).toBe(true); + }); + } + }); + } +}); diff --git a/test/regex.test.ts b/test/regex.test.ts index 12b7a35..e9bd300 100644 --- a/test/regex.test.ts +++ b/test/regex.test.ts @@ -47,6 +47,9 @@ const cases: Case[] = [ { content: "どわー爆笑爆笑", expected: true }, { content: "お、おうw", expected: true }, { content: "きちーw", expected: true }, + // (笑) は語幹込みで一致する(旧実装では語幹が欠落するバグがあった) + { content: "うお(笑)", expected: ["うお(笑)"] }, + { content: "どわー(笑)", expected: ["どわー(笑)"] }, // 複数マッチ { content: "うおうおうおw、爆笑爆笑", expected: ["うおw", "爆笑爆笑"] }, { @@ -61,11 +64,13 @@ describe("冷笑検出", () => { const isRelaxed = !!c.relaxed; if (typeof c.expected === "boolean") { test(`case ${i} [${isRelaxed ? "relaxed" : "strict"}] contains -> ${c.content}`, () => { - expect(contains(c.content, isRelaxed)).toBe(c.expected as boolean); + expect(contains(c.content, { relaxed: isRelaxed })).toBe( + c.expected as boolean, + ); }); } else { test(`case ${i} [${isRelaxed ? "relaxed" : "strict"}] findAll -> ${c.content}`, () => { - const res = findAll(c.content, isRelaxed) || []; + const res = findAll(c.content, { relaxed: isRelaxed }) || []; expect(res).toEqual(c.expected as string[]); }); } From 0e68ce47c98b20a5a90ee9b41d4e4a06a03fb7ea Mon Sep 17 00:00:00 2001 From: "otoneko." Date: Sun, 23 Aug 2026 22:02:12 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20expand=20patterns=20from=20?= =?UTF-8?q?=E5=86=B7=E7=AC=91=E6=A7=8B=E6=96=87=20references,=20drop=20lab?= =?UTF-8?q?els,=20normalize=20line=20endings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/lib/patterns.ts: drop the unused `label` field from PatternDefinition (id/strict/source/samples only); merge the "新規追加候補" stems into the regular strict stem section - add ❗/❓/⁉️ as strict emoji patterns (alongside the existing ‼️) - add phrase-based patterns sourced from https://note.com/kido_meigen/n/nc0fb2d47f6f6 and https://w.atwiki.jp/reisyou/pages/10.html: strict: かっこよ/えぐ/ど、どした/冗談ですやん/必死やん/そういうノリ relaxed-only (common enough alone that strict felt too aggressive): ちょ/ったく/おもろいな(あ)/すごいな(あ) skipped as too narrow or too likely to false-positive: よせやい/ あたぼうよ/あらよっと/てやんでい (obscure, single-copypasta-specific), the "教養🤣" meme (one specific tweet, not a general construction) - extend STEM_SUFFIX to optionally allow "!/?" before the w/笑 marker so phrases like "えぐー!笑" and "ど、どした?笑" match as one token - test/patterns.test.ts: drop label from describe() titles - .prettierrc.json / .gitattributes: adopt oto-lab/npm-eslint-prettier-ts's settings, in particular `* text=auto eol=lf` so Windows checkouts stop drifting from the LF committed in the repo --- .gitattributes | 5 ++ .prettierrc.json | 17 +++- CONTRIBUTING.md | 2 +- eslint.config.js | 2 +- package-lock.json | 66 --------------- src/index.ts | 2 +- src/lib/patterns.ts | 193 ++++++++++++++++++++---------------------- test/patterns.test.ts | 2 +- test/regex.test.ts | 2 +- 9 files changed, 117 insertions(+), 174 deletions(-) diff --git a/.gitattributes b/.gitattributes index 091ce89..ba9cca7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ +# Normalize all text files to LF, regardless of the platform or the +# contributor's core.autocrlf setting, so Prettier (which writes LF) never +# disagrees with what's checked out on Windows. +* text=auto eol=lf + test/** linguist-vendored diff --git a/.prettierrc.json b/.prettierrc.json index 75fa134..7e3a921 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,18 @@ { - "tabWidth": 2 + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "proseWrap": "preserve", + "htmlWhitespaceSensitivity": "css", + "embeddedLanguageFormatting": "auto", + "singleAttributePerLine": false, + "endOfLine": "lf" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 077d8c2..d3d21ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ PRに適切なラベルを付与してください ## 説明 - 冷笑のパターンを追加する場合 - - [./src/lib/patterns.ts](./src/lib/patterns.ts) の `patterns` 配列に `PatternDefinition` を1つ追加してください(`id` / `label` / `strict` / `source` / `samples`) + - [./src/lib/patterns.ts](./src/lib/patterns.ts) の `patterns` 配列に `PatternDefinition` を1つ追加してください(`id` / `strict` / `source` / `samples`) - `samples` に書いたサンプル文字列は自動でテスト化されます([test/patterns.test.ts](./test/patterns.test.ts))。個別にテストコードを書く必要はありません - 可能であれば **ひらがな** , **カタカナ** , **半角カナ** , **小文字** も同様に含めてください - 追加するパターンが明らかに冷笑な場合は `strict: true` に、冷笑か怪しい場合や文脈によっては冷笑ではないパターンは `strict: false` (relaxedモードでのみ検知)にしてください diff --git a/eslint.config.js b/eslint.config.js index 7dace11..d199b40 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,5 +8,5 @@ export default tseslint.config( js.configs.recommended, tseslint.configs.recommended, { languageOptions: { globals: { ...globals.node, ...globals.browser } } }, - eslintConfigPrettier, + eslintConfigPrettier ); diff --git a/package-lock.json b/package-lock.json index ccea1a3..075de38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -431,9 +431,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -451,9 +448,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -471,9 +465,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -491,9 +482,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -511,9 +499,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -531,9 +516,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1118,9 +1100,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1135,9 +1114,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1152,9 +1128,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1169,9 +1142,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1186,9 +1156,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1203,9 +1170,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1304,9 +1268,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1321,9 +1282,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1338,9 +1296,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1355,9 +1310,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1372,9 +1324,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1389,9 +1338,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2336,9 +2282,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2360,9 +2303,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2384,9 +2324,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2408,9 +2345,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/index.ts b/src/index.ts index f3404e8..05de882 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ export interface DowaOptions { */ export function findAll( text: string, - options: DowaOptions = {}, + options: DowaOptions = {} ): string[] | null { if (typeof text !== "string") { throw new TypeError('"text" must be a string.'); diff --git a/src/lib/patterns.ts b/src/lib/patterns.ts index 77c1972..4ffe472 100644 --- a/src/lib/patterns.ts +++ b/src/lib/patterns.ts @@ -1,6 +1,5 @@ export interface PatternDefinition { id: string; - label: string; /** true: strict(既定)に含む / false: relaxedのときだけ含む */ strict: boolean; /** 正規表現ソース(フラグなし) */ @@ -22,50 +21,58 @@ const I = "いぃイィイィ"; const TA = "たタタ"; const MO = "もモモ"; -// 語幹の後に続く「伸ばし棒/促音の繰り返し」+「w/笑/爆笑/(笑)」 -const STEM_SUFFIX = "[-ーー~っッッ]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; +// 語幹の後に続く「伸ばし棒/促音の繰り返し」+「!/?」+「w/笑/爆笑/(笑)」 +const STEM_SUFFIX = + "[-ーー~っッッ]*[!!??]*(?:[ww]+|(?:(?:爆笑)|笑)+|[((]笑[))])"; const stem = (a: string, b: string) => `[${a}][${b}]${STEM_SUFFIX}`; export const patterns: PatternDefinition[] = [ // --- 絵文字 (strict) --- { id: "emoji-sweat-smile", - label: "😅", strict: true, source: "\\u{1F605}", samples: ["これは😅です"], }, { id: "emoji-rofl", - label: "🤣", strict: true, source: "\\u{1F923}", samples: ["爆笑🤣爆笑"], }, { id: "emoji-double-exclamation", - label: "‼️", strict: true, source: "\\u{203C}\\u{FE0F}?", samples: ["本当‼️?"], }, + { id: "emoji-bang", strict: true, source: "\\u{2757}", samples: ["早く❗"] }, + { + id: "emoji-question", + strict: true, + source: "\\u{2753}", + samples: ["は❓"], + }, + { + id: "emoji-interrobang", + strict: true, + source: "\\u{2049}\\u{FE0F}?", + samples: ["は⁉️"], + }, { id: "emoji-eye-roll", - label: "🙄 (白目/呆れ)", strict: true, source: "\\u{1F644}", samples: ["は?🙄"], }, { id: "emoji-smirk", - label: "😏 (ニヤリ)", strict: true, source: "\\u{1F60F}", samples: ["それな😏"], }, { id: "emoji-clown", - label: "🤡 (道化=馬鹿にする)", strict: true, source: "\\u{1F921}", samples: ["🤡だなw"], @@ -74,157 +81,139 @@ export const patterns: PatternDefinition[] = [ // --- 絵文字 (relaxedのみ: 単体だと冷笑と断定しづらいもの) --- { id: "emoji-sweat-drop", - label: "💦", strict: false, source: "\\u{1F4A6}", samples: ["いや💦"], }, { id: "emoji-expressionless", - label: "😑", strict: false, source: "\\u{1F611}", samples: ["😑"], }, { id: "emoji-upside-down", - label: "🙃 (皮肉)", strict: false, source: "\\u{1F643}", 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"] }, + + // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- { - id: "emoji-skull", - label: "💀", + id: "bare-uo", strict: false, - source: "\\u{1F480}", - samples: ["💀"], + source: `[${U}][${O}][-ーー~っッッ]*`, + samples: ["うお"], }, { - id: "emoji-melting", - label: "🫠", + id: "bare-dowa", strict: false, - source: "\\u{1FAE0}", - samples: ["🫠"], + source: `[${DO}][${WA}][-ーー~っッッ]*`, + samples: ["どわ"], }, + { id: "bare-bakushou", strict: false, source: "爆笑", samples: ["爆笑"] }, + { id: "bare-reishou", strict: false, source: "冷笑", samples: ["冷笑"] }, - // --- 語幹 + w/笑/爆笑/(笑) (strict, 既存4種) --- + // --- 繰り返しパターン (strict) --- { - id: "stem-kichi", - label: "きち", + id: "repeat-bakushou", strict: true, - source: stem(KI, CHI), - samples: ["きちーw"], + source: "(?:爆笑){2,}", + samples: ["爆笑爆笑"], }, { - id: "stem-ou", - label: "おう", + id: "repeat-reishou", strict: true, - source: stem(O, U), - samples: ["お、おうw"], + source: "(?:冷笑){2,}", + samples: ["冷笑冷笑"], }, + { id: "repeat-warai", strict: true, source: "(?:笑){2,}", samples: ["笑笑"] }, { - id: "stem-uo", - label: "うお", + id: "paren-warai", strict: true, - source: stem(U, O), - samples: ["うおw"], + source: "[((]笑[))]", + samples: ["(笑)", "(笑)"], }, + + // --- フレーズ系(strict) --- + // 元ネタ: https://note.com/kido_meigen/n/nc0fb2d47f6f6 / https://w.atwiki.jp/reisyou/pages/10.html { - id: "stem-dowa", - label: "どわ", + id: "phrase-kakkoyo", strict: true, - source: stem(DO, WA), - samples: ["どわーw"], + source: `かっこよ${STEM_SUFFIX}`, + samples: ["かっこよw"], }, - - // --- 語幹 + w/笑/爆笑/(笑) (strict, 新規追加候補) --- { - id: "stem-uwa", - label: "うわ", + id: "phrase-egui", strict: true, - source: stem(U, WA), - samples: ["うわw"], + source: `えぐ${STEM_SUFFIX}`, + samples: ["えぐー!笑"], }, { - id: "stem-samu", - label: "さむ (寒い=しらける)", + id: "phrase-do-doshita", strict: true, - source: stem(SA, MU), - samples: ["さむw"], + source: `ど、?どした${STEM_SUFFIX}`, + samples: ["ど、どした?笑"], }, { - id: "stem-ita", - label: "いた (痛い=イタい)", + id: "phrase-joudan-desu-yan", strict: true, - source: stem(I, TA), - samples: ["いたw"], + source: `冗談ですやん${STEM_SUFFIX}`, + samples: ["冗談ですやん!!w"], }, { - id: "stem-kimo", - label: "きも (気持ち悪い)", + id: "phrase-hisshi-yan", strict: true, - source: stem(KI, MO), - samples: ["きもw"], + source: `必死やん${STEM_SUFFIX}`, + samples: ["必死やんww"], }, - - // --- 語幹単体 (relaxedのみ: 語尾のw/笑がなくても検知する) --- { - id: "bare-uo", - label: "うお (語尾なし)", - strict: false, - source: `[${U}][${O}][-ーー~っッッ]*`, - samples: ["うお"], + id: "phrase-sonna-nori", + strict: true, + source: `そういうノリ[…\\.・]*${STEM_SUFFIX}`, + samples: ["そういうノリ...w"], }, + + // --- フレーズ系(relaxedのみ: 単体では冷笑以外の文脈でも頻出するため) --- { - id: "bare-dowa", - label: "どわ (語尾なし)", + id: "phrase-cho", strict: false, - source: `[${DO}][${WA}][-ーー~っッッ]*`, - samples: ["どわ"], + source: `ちょ${STEM_SUFFIX}`, + samples: ["ちょw"], }, { - id: "bare-bakushou", - label: "爆笑 (1回のみ、relaxedのみ)", + id: "phrase-mattaku", strict: false, - source: "爆笑", - samples: ["爆笑"], + source: `ったく${STEM_SUFFIX}`, + samples: ["ったくw"], }, { - id: "bare-reishou", - label: "冷笑 (1回のみ、relaxedのみ)", + id: "phrase-omoroi", strict: false, - source: "冷笑", - samples: ["冷笑"], - }, - - // --- 繰り返しパターン (strict) --- - { - id: "repeat-bakushou", - label: "爆笑2回以上", - strict: true, - source: "(?:爆笑){2,}", - samples: ["爆笑爆笑"], - }, - { - id: "repeat-reishou", - label: "冷笑2回以上", - strict: true, - source: "(?:冷笑){2,}", - samples: ["冷笑冷笑"], + source: `おもろいな[あぁ]?${STEM_SUFFIX}`, + samples: ["おもろいなあww"], }, { - id: "repeat-warai", - label: "笑2回以上 (新規)", - strict: true, - source: "(?:笑){2,}", - samples: ["笑笑"], - }, - { - id: "paren-warai", - label: "(笑)/(笑)", - strict: true, - source: "[((]笑[))]", - samples: ["(笑)", "(笑)"], + id: "phrase-sugoi", + strict: false, + source: `すごいな[あぁ]?${STEM_SUFFIX}`, + samples: ["すごいなあww"], }, ]; diff --git a/test/patterns.test.ts b/test/patterns.test.ts index 842f1c2..0798f31 100644 --- a/test/patterns.test.ts +++ b/test/patterns.test.ts @@ -4,7 +4,7 @@ import { patterns } from "../src/lib/patterns"; describe("パターン別サンプル", () => { for (const p of patterns) { - describe(`[${p.id}] ${p.label}`, () => { + describe(`[${p.id}]`, () => { for (const sample of p.samples) { test(`strict -> ${sample}`, () => { expect(contains(sample)).toBe(p.strict); diff --git a/test/regex.test.ts b/test/regex.test.ts index e9bd300..9ffef78 100644 --- a/test/regex.test.ts +++ b/test/regex.test.ts @@ -65,7 +65,7 @@ describe("冷笑検出", () => { if (typeof c.expected === "boolean") { test(`case ${i} [${isRelaxed ? "relaxed" : "strict"}] contains -> ${c.content}`, () => { expect(contains(c.content, { relaxed: isRelaxed })).toBe( - c.expected as boolean, + c.expected as boolean ); }); } else {