@chaffjs/lang-ja 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isamu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @chaffjs/lang-ja
2
+
3
+ [chaff](https://www.npmjs.com/package/chaffjs) の公式日本語アダプタ。`chaff` に同梱されているので、単体で入れる必要はありません。
4
+
5
+ 持っているもの:
6
+
7
+ - 文分割(日本語の文末は `。!?` に限る。`Dr.` のようなラテン略語で切らない)
8
+ - L2 の語彙表(空虚な強調 / 水増しの導入 / 定型の結び)
9
+
10
+ 新しい言語を足すときは、このパッケージが雛形になります。公式は `@chaffjs/<言語>`、第三者は `chaff-lang-<言語>` と名乗ってください。
11
+
12
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { LanguageAdapter } from "chaffjs/plugin";
2
+ export declare const adapter: LanguageAdapter;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,gBAAgB,CAAC;AAyCpF,eAAO,MAAM,OAAO,EAAE,eAmBrB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
+ import { loadLexicons } from "./lexicons.js";
3
+ // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
4
+ /**
5
+ * 日本語の文末は「。!?」に限られる。
6
+ *
7
+ * sentence-splitter は "." も文末と見なすため、「、Dr. 田中は」で誤分割する。
8
+ * AbbrMarker の language を差し替えても直らない。原因は略語の保護ではなく、
9
+ * "." を終端と見なすこと自体にあるため。spec §7.2。
10
+ *
11
+ * 文長は sentence-rhythm と max-sentence-length の入力なので、
12
+ * 誤分割はそのまま指標を動かす。ここで閉じる。
13
+ */
14
+ const CLOSED = /[。!?!?]["))」』]*\s*$/u;
15
+ const JAPANESE = /[぀-ゟ゠-ヿ一-鿿]/gu;
16
+ const COUNTABLE = /\S/gu;
17
+ const isOpen = (text) => text.trim().length > 0 && !CLOSED.test(text);
18
+ const rawSpans = (text) => split(text)
19
+ .filter((node) => node.type === SentenceSplitterSyntax.Sentence)
20
+ .map((node) => ({ start: node.range[0], end: node.range[1] }));
21
+ /**
22
+ * 断片を繋ぐときは raw の連結ではなくオフセットを使う。
23
+ * sentence-splitter は空白を別ノードに分けるため、raw を繋ぐと空白が落ちて文長が縮む。
24
+ */
25
+ const merge = (source, spans) => spans
26
+ .reduce((acc, span) => {
27
+ const last = acc.at(-1);
28
+ if (last !== undefined && isOpen(source.slice(last.start, last.end))) {
29
+ return [...acc.slice(0, -1), { start: last.start, end: span.end }];
30
+ }
31
+ return [...acc, span];
32
+ }, [])
33
+ .map((span) => ({ span, text: source.slice(span.start, span.end) }));
34
+ export const adapter = {
35
+ kind: "language",
36
+ id: "ja",
37
+ apiVersion: 1,
38
+ capabilities: {
39
+ sentenceSplit: true,
40
+ // budoux も形態素解析も、まだ繋いでいない。spec §16 の Tier 0。
41
+ wordSplit: false,
42
+ pos: false,
43
+ lemma: false,
44
+ lengthUnit: "char",
45
+ },
46
+ detect: (source) => {
47
+ const total = [...source.matchAll(COUNTABLE)].length;
48
+ if (total === 0)
49
+ return 0;
50
+ return [...source.matchAll(JAPANESE)].length / total;
51
+ },
52
+ lexicons: loadLexicons(),
53
+ segment: (text) => ({ sentences: merge(text, rawSpans(text)) }),
54
+ };
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAG7C,2CAA2C;AAE3C;;;;;;;;;GASG;AACH,MAAM,MAAM,GAAG,sBAAsB,CAAC;AAEtC,MAAM,QAAQ,GAAG,eAAe,CAAC;AACjC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB,MAAM,MAAM,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEvF,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CACxC,KAAK,CAAC,IAAI,CAAC;KACR,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,sBAAsB,CAAC,QAAQ,CAAC;KAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAEnE;;;GAGG;AACH,MAAM,KAAK,GAAG,CAAC,MAAc,EAAE,KAAsB,EAAc,EAAE,CACnE,KAAK;KACF,MAAM,CAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;IAC5B,MAAM,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACrE,OAAO,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC,EAAE,EAAE,CAAC;KACL,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAEzE,MAAM,CAAC,MAAM,OAAO,GAAoB;IACtC,IAAI,EAAE,UAAU;IAChB,EAAE,EAAE,IAAI;IACR,UAAU,EAAE,CAAC;IACb,YAAY,EAAE;QACZ,aAAa,EAAE,IAAI;QACnB,6CAA6C;QAC7C,SAAS,EAAE,KAAK;QAChB,GAAG,EAAE,KAAK;QACV,KAAK,EAAE,KAAK;QACZ,UAAU,EAAE,MAAM;KACnB;IACD,MAAM,EAAE,CAAC,MAAc,EAAU,EAAE;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACrD,IAAI,KAAK,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QAC1B,OAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,GAAG,KAAK,CAAC;IACvD,CAAC;IACD,QAAQ,EAAE,YAAY,EAAE;IACxB,OAAO,EAAE,CAAC,IAAY,EAAgB,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;CACtF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Lexicon } from "chaffjs/plugin";
2
+ /**
3
+ * 語彙表はアダプタが持つ。detector は共通で、これだけが言語別。spec §11。
4
+ * 新しい言語のサポートは、ここを書くところから始まる。
5
+ */
6
+ export declare const loadLexicons: (dir?: string) => Record<string, Lexicon>;
7
+ //# sourceMappingURL=lexicons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexicons.d.ts","sourceRoot":"","sources":["../src/lexicons.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,gBAAgB,CAAC;AAY5D;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAK,MAAY,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAQ7D,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { parse } from "yaml";
5
+ const DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "lexicons");
6
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
+ const toEntry = (raw) => {
8
+ if (!isRecord(raw) || typeof raw["pattern"] !== "string")
9
+ return undefined;
10
+ const weight = raw["weight"];
11
+ return { pattern: raw["pattern"], weight: typeof weight === "number" ? weight : undefined };
12
+ };
13
+ /**
14
+ * 語彙表はアダプタが持つ。detector は共通で、これだけが言語別。spec §11。
15
+ * 新しい言語のサポートは、ここを書くところから始まる。
16
+ */
17
+ export const loadLexicons = (dir = DIR) => readdirSync(dir)
18
+ .filter((file) => file.endsWith(".yaml"))
19
+ .reduce((acc, file) => {
20
+ const raw = parse(readFileSync(join(dir, file), "utf8"));
21
+ if (!isRecord(raw) || typeof raw["id"] !== "string" || !Array.isArray(raw["entries"]))
22
+ return acc;
23
+ const entries = raw["entries"].map(toEntry).filter((entry) => entry !== undefined);
24
+ return { ...acc, [raw["id"]]: entries };
25
+ }, {});
26
+ //# sourceMappingURL=lexicons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lexicons.js","sourceRoot":"","sources":["../src/lexicons.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAG7B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;AAE5E,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAE5I,MAAM,OAAO,GAAG,CAAC,GAAY,EAA4B,EAAE;IACzD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,SAAS,CAAC,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC3E,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC9F,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAc,GAAG,EAA2B,EAAE,CACzE,WAAW,CAAC,GAAG,CAAC;KACb,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;KACxC,MAAM,CAA0B,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;IAC7C,MAAM,GAAG,GAAY,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IAClG,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC;IACnF,OAAO,EAAE,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC;AAC1C,CAAC,EAAE,EAAE,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ # 定型の結び。読者に何も残さない締め。
2
+ id: closing-cliche
3
+ language: ja
4
+ entries:
5
+ - pattern: いかがでしたか
6
+ - pattern: いかがでしょうか
7
+ - pattern: まとめると
8
+ - pattern: 最後までお読みいただき
9
+ - pattern: 参考になれば幸いです
10
+ - pattern: ぜひ試してみてください
@@ -0,0 +1,13 @@
1
+ # 空虚な強調。程度を言っているようで、何も言っていない表現。
2
+ id: empty-intensifier
3
+ language: ja
4
+ entries:
5
+ - pattern: 非常に重要
6
+ - pattern: 極めて重要
7
+ - pattern: きわめて重要
8
+ - pattern: 大変重要
9
+ - pattern: とても重要
10
+ - pattern: 最も重要なのは
11
+ - pattern: 言うまでもなく
12
+ - pattern: 不可欠です
13
+ - pattern: 欠かせません
@@ -0,0 +1,12 @@
1
+ # 水増しの導入。どの記事にも当てはまる書き出し。
2
+ id: padded-intro
3
+ language: ja
4
+ entries:
5
+ - pattern: 近年
6
+ - pattern: 昨今
7
+ - pattern: 注目されています
8
+ - pattern: 注目を集めています
9
+ - pattern: 重要性が高まっています
10
+ - pattern: 方も多いのではないでしょうか
11
+ - pattern: ではないでしょうか
12
+ - pattern: 皆さんは
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@chaffjs/lang-ja",
3
+ "version": "0.0.1",
4
+ "description": "Japanese language adapter for chaff",
5
+ "license": "MIT",
6
+ "author": "isamu",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "LICENSE",
16
+ "README.md",
17
+ "dist",
18
+ "lexicons",
19
+ "src"
20
+ ],
21
+ "engines": {
22
+ "node": ">=24"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc -p tsconfig.build.json",
26
+ "prepack": "yarn build"
27
+ },
28
+ "dependencies": {
29
+ "sentence-splitter": "^5.0.1",
30
+ "yaml": "^2.9.0"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/isamu/lab.git",
35
+ "directory": "text/packages/lang-ja"
36
+ },
37
+ "homepage": "https://github.com/isamu/lab/tree/main/text",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { split, SentenceSplitterSyntax } from "sentence-splitter";
2
+ import { loadLexicons } from "./lexicons.ts";
3
+ import type { LanguageAdapter, Segmentation, Sentence, Span } from "chaffjs/plugin";
4
+
5
+ // chaff からは型だけを取る。実行時の値依存を作らない。アダプタは単体で動く。
6
+
7
+ /**
8
+ * 日本語の文末は「。!?」に限られる。
9
+ *
10
+ * sentence-splitter は "." も文末と見なすため、「、Dr. 田中は」で誤分割する。
11
+ * AbbrMarker の language を差し替えても直らない。原因は略語の保護ではなく、
12
+ * "." を終端と見なすこと自体にあるため。spec §7.2。
13
+ *
14
+ * 文長は sentence-rhythm と max-sentence-length の入力なので、
15
+ * 誤分割はそのまま指標を動かす。ここで閉じる。
16
+ */
17
+ const CLOSED = /[。!?!?]["))」』]*\s*$/u;
18
+
19
+ const JAPANESE = /[぀-ゟ゠-ヿ一-鿿]/gu;
20
+ const COUNTABLE = /\S/gu;
21
+
22
+ const isOpen = (text: string): boolean => text.trim().length > 0 && !CLOSED.test(text);
23
+
24
+ const rawSpans = (text: string): Span[] =>
25
+ split(text)
26
+ .filter((node) => node.type === SentenceSplitterSyntax.Sentence)
27
+ .map((node) => ({ start: node.range[0], end: node.range[1] }));
28
+
29
+ /**
30
+ * 断片を繋ぐときは raw の連結ではなくオフセットを使う。
31
+ * sentence-splitter は空白を別ノードに分けるため、raw を繋ぐと空白が落ちて文長が縮む。
32
+ */
33
+ const merge = (source: string, spans: readonly Span[]): Sentence[] =>
34
+ spans
35
+ .reduce<Span[]>((acc, span) => {
36
+ const last = acc.at(-1);
37
+ if (last !== undefined && isOpen(source.slice(last.start, last.end))) {
38
+ return [...acc.slice(0, -1), { start: last.start, end: span.end }];
39
+ }
40
+ return [...acc, span];
41
+ }, [])
42
+ .map((span) => ({ span, text: source.slice(span.start, span.end) }));
43
+
44
+ export const adapter: LanguageAdapter = {
45
+ kind: "language",
46
+ id: "ja",
47
+ apiVersion: 1,
48
+ capabilities: {
49
+ sentenceSplit: true,
50
+ // budoux も形態素解析も、まだ繋いでいない。spec §16 の Tier 0。
51
+ wordSplit: false,
52
+ pos: false,
53
+ lemma: false,
54
+ lengthUnit: "char",
55
+ },
56
+ detect: (source: string): number => {
57
+ const total = [...source.matchAll(COUNTABLE)].length;
58
+ if (total === 0) return 0;
59
+ return [...source.matchAll(JAPANESE)].length / total;
60
+ },
61
+ lexicons: loadLexicons(),
62
+ segment: (text: string): Segmentation => ({ sentences: merge(text, rawSpans(text)) }),
63
+ };
@@ -0,0 +1,29 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { parse } from "yaml";
5
+ import type { Lexicon, LexiconEntry } from "chaffjs/plugin";
6
+
7
+ const DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "lexicons");
8
+
9
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
10
+
11
+ const toEntry = (raw: unknown): LexiconEntry | undefined => {
12
+ if (!isRecord(raw) || typeof raw["pattern"] !== "string") return undefined;
13
+ const weight = raw["weight"];
14
+ return { pattern: raw["pattern"], weight: typeof weight === "number" ? weight : undefined };
15
+ };
16
+
17
+ /**
18
+ * 語彙表はアダプタが持つ。detector は共通で、これだけが言語別。spec §11。
19
+ * 新しい言語のサポートは、ここを書くところから始まる。
20
+ */
21
+ export const loadLexicons = (dir: string = DIR): Record<string, Lexicon> =>
22
+ readdirSync(dir)
23
+ .filter((file) => file.endsWith(".yaml"))
24
+ .reduce<Record<string, Lexicon>>((acc, file) => {
25
+ const raw: unknown = parse(readFileSync(join(dir, file), "utf8"));
26
+ if (!isRecord(raw) || typeof raw["id"] !== "string" || !Array.isArray(raw["entries"])) return acc;
27
+ const entries = raw["entries"].map(toEntry).filter((entry) => entry !== undefined);
28
+ return { ...acc, [raw["id"]]: entries };
29
+ }, {});