@chaffjs/lang-ja 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,gBAAgB,CAAC;AA6CpF,eAAO,MAAM,OAAO,EAAE,eAmBrB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,eAAe,EAAgC,MAAM,gBAAgB,CAAC;AA2DlG,eAAO,MAAM,OAAO,EAAE,eAyBrB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
2
  import { loadLexicons } from "./lexicons.js";
3
+ import { isReady, predicateOnly, prepare, tokenize } from "./pos.js";
3
4
  // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
4
5
  /**
5
6
  * 日本語の文末は「。!?」に限られる。
@@ -35,18 +36,36 @@ const merge = (source, spans) => spans
35
36
  return [...acc, span];
36
37
  }, [])
37
38
  .map((span) => ({ span, text: source.slice(span.start, span.end) }));
39
+ /**
40
+ * token の span は文ではなく、segment に渡した文字列を基準にする。文の span と同じ座標系。
41
+ * 文ごとに解析して足し戻すのではなく、一度解析して文へ配る。同じ文字列を二度読まない。
42
+ */
43
+ const withTokens = (source, sentences) => {
44
+ const tokens = tokenize(source);
45
+ if (tokens === undefined)
46
+ return [...sentences];
47
+ return sentences.map((sentence) => ({
48
+ ...sentence,
49
+ // 述語かどうかは文の中でしか決まらないので、文へ配ってから印を落とす。
50
+ tokens: predicateOnly(tokens.filter((token) => token.span.start >= sentence.span.start && token.span.end <= sentence.span.end)),
51
+ }));
52
+ };
38
53
  export const adapter = {
39
54
  kind: "language",
40
55
  id: "ja",
41
56
  apiVersion: 1,
42
57
  capabilities: {
43
58
  sentenceSplit: true,
44
- // budoux も形態素解析も、まだ繋いでいない。spec §16 の Tier 0。
45
- wordSplit: false,
46
- pos: false,
47
- lemma: false,
59
+ // 「払えばできる」の宣言。実際に払うのは prepare。辞書の初期化に 1.5 秒かかる。
60
+ wordSplit: true,
61
+ pos: true,
62
+ lemma: true,
48
63
  lengthUnit: "char",
49
64
  },
65
+ prepare: async (need) => {
66
+ if (need.pos)
67
+ await prepare();
68
+ },
50
69
  detect: (source) => {
51
70
  const total = [...source.matchAll(COUNTABLE)].length;
52
71
  if (total === 0)
@@ -54,6 +73,9 @@ export const adapter = {
54
73
  return [...source.matchAll(JAPANESE)].length / total;
55
74
  },
56
75
  lexicons: loadLexicons(),
57
- segment: (text) => ({ sentences: merge(text, rawSpans(text)) }),
76
+ segment: (text) => {
77
+ const sentences = merge(text, rawSpans(text));
78
+ return { sentences: isReady() ? withTokens(text, sentences) : sentences };
79
+ },
58
80
  };
59
81
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/pos.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Token } from "chaffjs/plugin";
2
+ export declare const upos: (pos: string, detail: string) => string;
3
+ /** 二度目以降は同じ約束を待つ。並行して呼ばれても辞書を二度読まない。 */
4
+ export declare const prepare: () => Promise<void>;
5
+ export declare const isReady: () => boolean;
6
+ export declare const predicateOnly: (tokens: readonly Token[]) => Token[];
7
+ export declare const tokenize: (text: string) => Token[] | undefined;
8
+ //# sourceMappingURL=pos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pos.d.ts","sourceRoot":"","sources":["../src/pos.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAoF5C,eAAO,MAAM,IAAI,GAAI,KAAK,MAAM,EAAE,QAAQ,MAAM,KAAG,MAAiD,CAAC;AA8BrG,wCAAwC;AACxC,eAAO,MAAM,OAAO,QAAa,OAAO,CAAC,IAAI,CAG5C,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,OAAoC,CAAC;AA+BhE,eAAO,MAAM,aAAa,GAAI,QAAQ,SAAS,KAAK,EAAE,KAAG,KAAK,EAM1D,CAAC;AAEL,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,KAAK,EAAE,GAAG,SAMjD,CAAC"}
package/dist/pos.js ADDED
@@ -0,0 +1,136 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ const require = createRequire(import.meta.url);
4
+ const isRecord = (value) => typeof value === "object" && value !== null;
5
+ const toArray = (value) => (Array.isArray(value) ? value : []);
6
+ const isCallable = (value) => typeof value === "function";
7
+ const isTokenizer = (value) => isRecord(value) && isCallable(value["tokenize"]);
8
+ const isMorpheme = (value) => isRecord(value) &&
9
+ typeof value["word_position"] === "number" &&
10
+ typeof value["surface_form"] === "string" &&
11
+ typeof value["pos"] === "string" &&
12
+ typeof value["pos_detail_1"] === "string" &&
13
+ typeof value["basic_form"] === "string";
14
+ const dictionaryPath = () => join(dirname(require.resolve("@sglkc/kuromoji/package.json")), "dict");
15
+ /** 取り出した関数をそのまま呼ぶと receiver が外れる。kuromoji の build は this.dic_path を読む。 */
16
+ const callMethod = (owner, name, args) => {
17
+ const method = owner[name];
18
+ if (!isCallable(method))
19
+ throw new Error(`@sglkc/kuromoji が ${name} を持っていません`);
20
+ return Reflect.apply(method, owner, args);
21
+ };
22
+ const buildWith = (done) => {
23
+ const module = require("@sglkc/kuromoji");
24
+ if (!isRecord(module))
25
+ throw new Error("@sglkc/kuromoji が object を export していません");
26
+ const builder = callMethod(module, "builder", [{ dicPath: dictionaryPath() }]);
27
+ if (!isRecord(builder))
28
+ throw new Error("@sglkc/kuromoji の builder が object を返しませんでした");
29
+ callMethod(builder, "build", [done]);
30
+ };
31
+ /**
32
+ * IPADIC の品詞を UPOS に寄せる。detector にアダプタ固有の体系を見せない。spec §6。
33
+ * 引けなかったものは X。誤った品詞を当てるより、分からないと言うほうがまし。
34
+ */
35
+ const BY_DETAIL = {
36
+ 代名詞: "PRON",
37
+ 固有名詞: "PROPN",
38
+ 格助詞: "ADP",
39
+ 係助詞: "ADP",
40
+ 副助詞: "ADP",
41
+ 連体化: "ADP",
42
+ 接続助詞: "SCONJ",
43
+ 終助詞: "PART",
44
+ 並立助詞: "CCONJ",
45
+ };
46
+ const BY_POS = {
47
+ 名詞: "NOUN",
48
+ 動詞: "VERB",
49
+ 形容詞: "ADJ",
50
+ 副詞: "ADV",
51
+ 助動詞: "AUX",
52
+ 助詞: "PART",
53
+ 接続詞: "CCONJ",
54
+ 連体詞: "DET",
55
+ 感動詞: "INTJ",
56
+ 記号: "PUNCT",
57
+ 接頭詞: "ADJ",
58
+ フィラー: "INTJ",
59
+ };
60
+ export const upos = (pos, detail) => BY_DETAIL[detail] ?? BY_POS[pos] ?? "X";
61
+ /**
62
+ * 受動の「れる/られる」。IPADIC では動詞の接尾として出る。
63
+ * ただしこの語は可能・尊敬・自発も表す。区別は文脈が要るのでここではしない。
64
+ * 受動と言い切らず「受動の形」として印を付け、どう扱うかは rule 側に委ねる。
65
+ */
66
+ const PASSIVE_LEMMA = new Set(["れる", "られる"]);
67
+ const isPassive = (morpheme) => morpheme.pos === "動詞" && morpheme.pos_detail_1 === "接尾" && PASSIVE_LEMMA.has(morpheme.basic_form);
68
+ /**
69
+ * 非自立名詞(の・こと・もの・ため・はず)。品詞は名詞だが、単独では何も指さない。
70
+ * 「回るのか。」の「の」を体言止めと読むと、疑問文が全部ひっかかる。
71
+ *
72
+ * UD の FEATS は言語ごとの拡張を認めているので、そこに畳む。UPOS は NOUN のまま。
73
+ */
74
+ const isDependentNoun = (morpheme) => morpheme.pos === "名詞" && morpheme.pos_detail_1 === "非自立";
75
+ const state = { pending: undefined, ready: undefined };
76
+ const build = async () => new Promise((resolve, reject) => {
77
+ buildWith((error, tokenizer) => {
78
+ if (error !== null)
79
+ reject(new Error(`日本語辞書を読めませんでした: ${error.message}`));
80
+ else if (!isTokenizer(tokenizer))
81
+ reject(new Error("日本語辞書が tokenizer を返しませんでした"));
82
+ else
83
+ resolve(tokenizer);
84
+ });
85
+ });
86
+ /** 二度目以降は同じ約束を待つ。並行して呼ばれても辞書を二度読まない。 */
87
+ export const prepare = async () => {
88
+ state.pending ??= build();
89
+ state.ready = await state.pending;
90
+ };
91
+ export const isReady = () => state.ready !== undefined;
92
+ /**
93
+ * span は渡した文字列の先頭を 0 とする。kuromoji の word_position は 1 始まりなので 1 引く。
94
+ * 形の違うものが混ざったら、その 1 つを落とす。位置が NaN の token を下流に流さない。
95
+ */
96
+ const toToken = (morpheme) => ({
97
+ span: { start: morpheme.word_position - 1, end: morpheme.word_position - 1 + morpheme.surface_form.length },
98
+ surface: morpheme.surface_form,
99
+ // UD の日本語では「れる/られる」は AUX。IPADIC の「動詞,接尾」をそこへ寄せる。
100
+ pos: isPassive(morpheme) ? "AUX" : upos(morpheme.pos, morpheme.pos_detail_1),
101
+ ...(morpheme.basic_form === "*" ? {} : { lemma: morpheme.basic_form }),
102
+ ...featuresOf(morpheme),
103
+ });
104
+ const featuresOf = (morpheme) => {
105
+ if (isPassive(morpheme))
106
+ return { features: { Voice: "Pass" } };
107
+ if (isDependentNoun(morpheme))
108
+ return { features: { NounType: "Dependent" } };
109
+ return {};
110
+ };
111
+ /**
112
+ * 名詞を修飾しているだけの受動から印を外す。「使用されるフレームワーク」
113
+ * 「開催される BootCamp」は動作主を隠しているのではなく、名前の付けかたで、
114
+ * 書き手に直す余地がない。実文書で測ったら、この形が指摘の 7 割を占めていた。
115
+ *
116
+ * 日本語は修飾が名詞の前に来るので「後ろに名詞が無い」で述語だと言える。
117
+ * 英語は語順が逆なので、この判断は英語のアダプタには持ち込めない。
118
+ */
119
+ const NOMINAL = new Set(["NOUN", "PROPN", "PRON"]);
120
+ export const predicateOnly = (tokens) => tokens.map((token) => {
121
+ if (token.features?.["Voice"] !== "Pass")
122
+ return token;
123
+ const modifiesNoun = tokens.some((other) => other.span.start >= token.span.end && NOMINAL.has(other.pos));
124
+ if (!modifiesNoun)
125
+ return token;
126
+ return { span: token.span, surface: token.surface, pos: token.pos, ...(token.lemma === undefined ? {} : { lemma: token.lemma }) };
127
+ });
128
+ export const tokenize = (text) => {
129
+ const tokenizer = state.ready;
130
+ if (tokenizer === undefined)
131
+ return undefined;
132
+ return toArray(callMethod(tokenizer, "tokenize", [text]))
133
+ .filter(isMorpheme)
134
+ .map(toToken);
135
+ };
136
+ //# sourceMappingURL=pos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pos.js","sourceRoot":"","sources":["../src/pos.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ # 動作主を示す語。受動文にこれがあれば、誰がしたのかは書かれている。
2
+ id: agent-marker
3
+ language: ja
4
+ entries:
5
+ - pattern: によって
6
+ - pattern: により
7
+ - pattern: による
8
+ - pattern: から
@@ -0,0 +1,21 @@
1
+ # AI 生成の signal。1 つでは何も言えないので重みを足し合わせる。
2
+ # 単独で「AI が書いた」とは言わない。spec §20.2。
3
+ id: ai-tell
4
+ language: ja
5
+ entries:
6
+ - pattern: 現代社会において
7
+ weight: 0.9
8
+ - pattern: 急速に変化する
9
+ weight: 0.7
10
+ - pattern: 重要な役割を果たし
11
+ weight: 0.8
12
+ - pattern: と言えるでしょう
13
+ weight: 0.6
14
+ - pattern: 様々な要因が
15
+ weight: 0.6
16
+ - pattern: 大きな可能性を秘め
17
+ weight: 0.8
18
+ - pattern: 深く掘り下げ
19
+ weight: 0.7
20
+ - pattern: ぜひ参考にしてみてください
21
+ weight: 0.7
@@ -0,0 +1,12 @@
1
+ # クッション言葉。1 つなら礼儀だが、重なると本題が遠ざかる。
2
+ id: cushion-phrase
3
+ language: ja
4
+ entries:
5
+ - pattern: お忙しいところ
6
+ - pattern: 恐れ入りますが
7
+ - pattern: 差し支えなければ
8
+ - pattern: もしよろしければ
9
+ - pattern: ご多忙のところ
10
+ - pattern: 大変恐縮ですが
11
+ - pattern: 念のため
12
+ - pattern: 取り急ぎ
@@ -0,0 +1,17 @@
1
+ # 二重敬語。1 つの動詞に同じ種類の敬語を二重にかけた形。
2
+ #
3
+ # 「ご説明させていただきます」のように議論の分かれるものは入れない。
4
+ # 割れる形を入れると、rule ごと無視される。
5
+ id: double-keigo
6
+ language: ja
7
+ entries:
8
+ - pattern: おっしゃられ
9
+ - pattern: ご覧になられ
10
+ - pattern: お読みになられ
11
+ - pattern: お聞きになられ
12
+ - pattern: お見えになられ
13
+ - pattern: お帰りになられ
14
+ - pattern: お召し上がりになら
15
+ - pattern: 拝見させていただ
16
+ - pattern: お伺いさせていただ
17
+ - pattern: 拝読させていただ
@@ -0,0 +1,12 @@
1
+ # 逃げの表現。断言を避けるほど、読み手は何が決まったのか分からなくなる。
2
+ id: excessive-hedging
3
+ language: ja
4
+ entries:
5
+ - pattern: かもしれません
6
+ - pattern: と思われます
7
+ - pattern: のような気がします
8
+ - pattern: 可能性があります
9
+ - pattern: 場合があります
10
+ - pattern: 一概には言えません
11
+ - pattern: と考えられます
12
+ - pattern: ではないでしょうか
@@ -0,0 +1,17 @@
1
+ # ひらがなで書く副詞。漢字が表外、または公用文でかな書きと決まっているもの。
2
+ #
3
+ # 「既に」「全く」のように漢字でも公用文で認められるものは入れない。
4
+ # どちらが正しいかで割れる語を入れると、rule ごと無視される。
5
+ id: hiragana-fukushi
6
+ language: ja
7
+ entries:
8
+ - pattern: 殆ど
9
+ - pattern: 尤も
10
+ - pattern: 勿論
11
+ - pattern: 丁度
12
+ - pattern: 沢山
13
+ - pattern: 何故
14
+ - pattern: 予め
15
+ - pattern: 暫く
16
+ - pattern: 些か
17
+ - pattern: 頗る
@@ -0,0 +1,8 @@
1
+ # 名詞を名詞に繋いで入れ子を作る助詞。「弊社の新製品の販売の計画」の「の」。
2
+ #
3
+ # 「も」の並列(A も B も C も)と「て」の連用(コピーして持っていって)は
4
+ # 何重に続いても読める。入れ子になるものだけを入れる。
5
+ id: nesting-particle
6
+ language: ja
7
+ entries:
8
+ - pattern: の
@@ -0,0 +1,12 @@
1
+ # 段落の先頭に立つ接続詞。続くと、どの段落も前の付け足しに見える。
2
+ id: paragraph-opener
3
+ language: ja
4
+ entries:
5
+ - pattern: また
6
+ - pattern: さらに
7
+ - pattern: そして
8
+ - pattern: しかし
9
+ - pattern: ただし
10
+ - pattern: なお
11
+ - pattern: 一方
12
+ - pattern: つまり
@@ -0,0 +1,10 @@
1
+ # ですます調の文末。これを含む文を「ですます」と見る。
2
+ id: polite-ending
3
+ language: ja
4
+ entries:
5
+ - pattern: です
6
+ - pattern: ます
7
+ - pattern: ません
8
+ - pattern: でしょう
9
+ - pattern: ください
10
+ - pattern: ございます
@@ -0,0 +1,7 @@
1
+ # 「させていただく」。1 度なら丁寧だが、重なると誰の判断なのかが消える。
2
+ id: sasete-itadaku
3
+ language: ja
4
+ entries:
5
+ - pattern: させていただ
6
+ - pattern: させて頂
7
+ - pattern: させて戴
@@ -0,0 +1,12 @@
1
+ # 最上級。何と比べて最上なのかが要る。
2
+ id: superlative
3
+ language: ja
4
+ entries:
5
+ - pattern: 最も
6
+ - pattern: 最高
7
+ - pattern: 最速
8
+ - pattern: 最大
9
+ - pattern: 世界初
10
+ - pattern: 業界初
11
+ - pattern: 唯一の
12
+ - pattern: 圧倒的
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaffjs/lang-ja",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Japanese language adapter for chaff",
5
5
  "license": "MIT",
6
6
  "author": "isamu",
@@ -26,6 +26,7 @@
26
26
  "prepack": "yarn build"
27
27
  },
28
28
  "dependencies": {
29
+ "@sglkc/kuromoji": "^1.1.0",
29
30
  "sentence-splitter": "^5.0.1",
30
31
  "yaml": "^2.9.0"
31
32
  },
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
2
  import { loadLexicons } from "./lexicons.ts";
3
- import type { LanguageAdapter, Segmentation, Sentence, Span } from "chaffjs/plugin";
3
+ import { isReady, predicateOnly, prepare, tokenize } from "./pos.ts";
4
+ import type { AdapterNeeds, LanguageAdapter, Segmentation, Sentence, Span } from "chaffjs/plugin";
4
5
 
5
6
  // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
6
7
 
@@ -45,23 +46,43 @@ const merge = (source: string, spans: readonly Span[]): Sentence[] =>
45
46
  }, [])
46
47
  .map((span) => ({ span, text: source.slice(span.start, span.end) }));
47
48
 
49
+ /**
50
+ * token の span は文ではなく、segment に渡した文字列を基準にする。文の span と同じ座標系。
51
+ * 文ごとに解析して足し戻すのではなく、一度解析して文へ配る。同じ文字列を二度読まない。
52
+ */
53
+ const withTokens = (source: string, sentences: readonly Sentence[]): Sentence[] => {
54
+ const tokens = tokenize(source);
55
+ if (tokens === undefined) return [...sentences];
56
+ return sentences.map((sentence) => ({
57
+ ...sentence,
58
+ // 述語かどうかは文の中でしか決まらないので、文へ配ってから印を落とす。
59
+ tokens: predicateOnly(tokens.filter((token) => token.span.start >= sentence.span.start && token.span.end <= sentence.span.end)),
60
+ }));
61
+ };
62
+
48
63
  export const adapter: LanguageAdapter = {
49
64
  kind: "language",
50
65
  id: "ja",
51
66
  apiVersion: 1,
52
67
  capabilities: {
53
68
  sentenceSplit: true,
54
- // budoux も形態素解析も、まだ繋いでいない。spec §16 の Tier 0。
55
- wordSplit: false,
56
- pos: false,
57
- lemma: false,
69
+ // 「払えばできる」の宣言。実際に払うのは prepare。辞書の初期化に 1.5 秒かかる。
70
+ wordSplit: true,
71
+ pos: true,
72
+ lemma: true,
58
73
  lengthUnit: "char",
59
74
  },
75
+ prepare: async (need: AdapterNeeds): Promise<void> => {
76
+ if (need.pos) await prepare();
77
+ },
60
78
  detect: (source: string): number => {
61
79
  const total = [...source.matchAll(COUNTABLE)].length;
62
80
  if (total === 0) return 0;
63
81
  return [...source.matchAll(JAPANESE)].length / total;
64
82
  },
65
83
  lexicons: loadLexicons(),
66
- segment: (text: string): Segmentation => ({ sentences: merge(text, rawSpans(text)) }),
84
+ segment: (text: string): Segmentation => {
85
+ const sentences = merge(text, rawSpans(text));
86
+ return { sentences: isReady() ? withTokens(text, sentences) : sentences };
87
+ },
67
88
  };
package/src/pos.ts ADDED
@@ -0,0 +1,168 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname, join } from "node:path";
3
+ import type { Token } from "chaffjs/plugin";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ /**
8
+ * kuromoji は CommonJS で、辞書を非同期に読む。ESM からは createRequire で取る。
9
+ * 辞書の初期化に 1.5 秒かかるので、prepare が呼ばれるまで触らない。
10
+ */
11
+ type Morpheme = {
12
+ readonly word_position: number;
13
+ readonly surface_form: string;
14
+ readonly pos: string;
15
+ readonly pos_detail_1: string;
16
+ readonly basic_form: string;
17
+ };
18
+
19
+ type Tokenizer = Record<string, unknown>;
20
+
21
+ type BuildDone = (error: Error | null, tokenizer: unknown) => void;
22
+
23
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
24
+
25
+ const toArray = (value: unknown): readonly unknown[] => (Array.isArray(value) ? value : []);
26
+
27
+ const isCallable = (value: unknown): value is (...args: readonly unknown[]) => unknown => typeof value === "function";
28
+
29
+ const isTokenizer = (value: unknown): value is Tokenizer => isRecord(value) && isCallable(value["tokenize"]);
30
+
31
+ const isMorpheme = (value: unknown): value is Morpheme =>
32
+ isRecord(value) &&
33
+ typeof value["word_position"] === "number" &&
34
+ typeof value["surface_form"] === "string" &&
35
+ typeof value["pos"] === "string" &&
36
+ typeof value["pos_detail_1"] === "string" &&
37
+ typeof value["basic_form"] === "string";
38
+
39
+ const dictionaryPath = (): string => join(dirname(require.resolve("@sglkc/kuromoji/package.json")), "dict");
40
+
41
+ /** 取り出した関数をそのまま呼ぶと receiver が外れる。kuromoji の build は this.dic_path を読む。 */
42
+ const callMethod = (owner: Record<string, unknown>, name: string, args: readonly unknown[]): unknown => {
43
+ const method: unknown = owner[name];
44
+ if (!isCallable(method)) throw new Error(`@sglkc/kuromoji が ${name} を持っていません`);
45
+ return Reflect.apply(method, owner, args);
46
+ };
47
+
48
+ const buildWith = (done: BuildDone): void => {
49
+ const module: unknown = require("@sglkc/kuromoji");
50
+ if (!isRecord(module)) throw new Error("@sglkc/kuromoji が object を export していません");
51
+ const builder: unknown = callMethod(module, "builder", [{ dicPath: dictionaryPath() }]);
52
+ if (!isRecord(builder)) throw new Error("@sglkc/kuromoji の builder が object を返しませんでした");
53
+ callMethod(builder, "build", [done]);
54
+ };
55
+
56
+ /**
57
+ * IPADIC の品詞を UPOS に寄せる。detector にアダプタ固有の体系を見せない。spec §6。
58
+ * 引けなかったものは X。誤った品詞を当てるより、分からないと言うほうがまし。
59
+ */
60
+ const BY_DETAIL: Readonly<Record<string, string>> = {
61
+ 代名詞: "PRON",
62
+ 固有名詞: "PROPN",
63
+ 格助詞: "ADP",
64
+ 係助詞: "ADP",
65
+ 副助詞: "ADP",
66
+ 連体化: "ADP",
67
+ 接続助詞: "SCONJ",
68
+ 終助詞: "PART",
69
+ 並立助詞: "CCONJ",
70
+ };
71
+
72
+ const BY_POS: Readonly<Record<string, string>> = {
73
+ 名詞: "NOUN",
74
+ 動詞: "VERB",
75
+ 形容詞: "ADJ",
76
+ 副詞: "ADV",
77
+ 助動詞: "AUX",
78
+ 助詞: "PART",
79
+ 接続詞: "CCONJ",
80
+ 連体詞: "DET",
81
+ 感動詞: "INTJ",
82
+ 記号: "PUNCT",
83
+ 接頭詞: "ADJ",
84
+ フィラー: "INTJ",
85
+ };
86
+
87
+ export const upos = (pos: string, detail: string): string => BY_DETAIL[detail] ?? BY_POS[pos] ?? "X";
88
+
89
+ /**
90
+ * 受動の「れる/られる」。IPADIC では動詞の接尾として出る。
91
+ * ただしこの語は可能・尊敬・自発も表す。区別は文脈が要るのでここではしない。
92
+ * 受動と言い切らず「受動の形」として印を付け、どう扱うかは rule 側に委ねる。
93
+ */
94
+ const PASSIVE_LEMMA = new Set(["れる", "られる"]);
95
+
96
+ const isPassive = (morpheme: Morpheme): boolean => morpheme.pos === "動詞" && morpheme.pos_detail_1 === "接尾" && PASSIVE_LEMMA.has(morpheme.basic_form);
97
+
98
+ /**
99
+ * 非自立名詞(の・こと・もの・ため・はず)。品詞は名詞だが、単独では何も指さない。
100
+ * 「回るのか。」の「の」を体言止めと読むと、疑問文が全部ひっかかる。
101
+ *
102
+ * UD の FEATS は言語ごとの拡張を認めているので、そこに畳む。UPOS は NOUN のまま。
103
+ */
104
+ const isDependentNoun = (morpheme: Morpheme): boolean => morpheme.pos === "名詞" && morpheme.pos_detail_1 === "非自立";
105
+
106
+ const state: { pending: Promise<Tokenizer> | undefined; ready: Tokenizer | undefined } = { pending: undefined, ready: undefined };
107
+
108
+ const build = async (): Promise<Tokenizer> =>
109
+ new Promise((resolve, reject) => {
110
+ buildWith((error, tokenizer) => {
111
+ if (error !== null) reject(new Error(`日本語辞書を読めませんでした: ${error.message}`));
112
+ else if (!isTokenizer(tokenizer)) reject(new Error("日本語辞書が tokenizer を返しませんでした"));
113
+ else resolve(tokenizer);
114
+ });
115
+ });
116
+
117
+ /** 二度目以降は同じ約束を待つ。並行して呼ばれても辞書を二度読まない。 */
118
+ export const prepare = async (): Promise<void> => {
119
+ state.pending ??= build();
120
+ state.ready = await state.pending;
121
+ };
122
+
123
+ export const isReady = (): boolean => state.ready !== undefined;
124
+
125
+ /**
126
+ * span は渡した文字列の先頭を 0 とする。kuromoji の word_position は 1 始まりなので 1 引く。
127
+ * 形の違うものが混ざったら、その 1 つを落とす。位置が NaN の token を下流に流さない。
128
+ */
129
+ const toToken = (morpheme: Morpheme): Token => ({
130
+ span: { start: morpheme.word_position - 1, end: morpheme.word_position - 1 + morpheme.surface_form.length },
131
+ surface: morpheme.surface_form,
132
+ // UD の日本語では「れる/られる」は AUX。IPADIC の「動詞,接尾」をそこへ寄せる。
133
+ pos: isPassive(morpheme) ? "AUX" : upos(morpheme.pos, morpheme.pos_detail_1),
134
+ ...(morpheme.basic_form === "*" ? {} : { lemma: morpheme.basic_form }),
135
+ ...featuresOf(morpheme),
136
+ });
137
+
138
+ const featuresOf = (morpheme: Morpheme): { features?: Readonly<Record<string, string>> } => {
139
+ if (isPassive(morpheme)) return { features: { Voice: "Pass" } };
140
+ if (isDependentNoun(morpheme)) return { features: { NounType: "Dependent" } };
141
+ return {};
142
+ };
143
+
144
+ /**
145
+ * 名詞を修飾しているだけの受動から印を外す。「使用されるフレームワーク」
146
+ * 「開催される BootCamp」は動作主を隠しているのではなく、名前の付けかたで、
147
+ * 書き手に直す余地がない。実文書で測ったら、この形が指摘の 7 割を占めていた。
148
+ *
149
+ * 日本語は修飾が名詞の前に来るので「後ろに名詞が無い」で述語だと言える。
150
+ * 英語は語順が逆なので、この判断は英語のアダプタには持ち込めない。
151
+ */
152
+ const NOMINAL = new Set(["NOUN", "PROPN", "PRON"]);
153
+
154
+ export const predicateOnly = (tokens: readonly Token[]): Token[] =>
155
+ tokens.map((token) => {
156
+ if (token.features?.["Voice"] !== "Pass") return token;
157
+ const modifiesNoun = tokens.some((other) => other.span.start >= token.span.end && NOMINAL.has(other.pos));
158
+ if (!modifiesNoun) return token;
159
+ return { span: token.span, surface: token.surface, pos: token.pos, ...(token.lemma === undefined ? {} : { lemma: token.lemma }) };
160
+ });
161
+
162
+ export const tokenize = (text: string): Token[] | undefined => {
163
+ const tokenizer = state.ready;
164
+ if (tokenizer === undefined) return undefined;
165
+ return toArray(callMethod(tokenizer, "tokenize", [text]))
166
+ .filter(isMorpheme)
167
+ .map(toToken);
168
+ };