react-native-jieba 0.1.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/Jieba.podspec +2 -0
  3. package/README.md +118 -7
  4. package/android/CMakeLists.txt +2 -0
  5. package/android/generated/java/com/jieba/NativeJiebaSpec.java +4 -0
  6. package/android/generated/jni/JiebaSpec-generated.cpp +6 -0
  7. package/android/generated/jni/react/renderer/components/JiebaSpec/JiebaSpecJSI.h +8 -0
  8. package/android/src/main/java/com/jieba/JiebaAndroidHelperModule.kt +7 -22
  9. package/android/src/main/java/com/jieba/JiebaDict.kt +72 -0
  10. package/android/src/main/java/com/jieba/JiebaPackage.kt +4 -0
  11. package/cpp/JiebaDictAndroid.cpp +26 -0
  12. package/cpp/JiebaDictAndroid.h +14 -0
  13. package/cpp/JiebaImpl.cpp +23 -0
  14. package/cpp/JiebaImpl.h +1 -0
  15. package/cpp/cppjieba/CHANGELOG.md +18 -0
  16. package/cpp/cppjieba/README.md +288 -0
  17. package/cpp/cppjieba/deps/limonp/CHANGELOG.md +175 -0
  18. package/cpp/cppjieba/deps/limonp/README.md +43 -0
  19. package/cpp/cppjieba/dict/README.md +62 -0
  20. package/ios/generated/ReactCodegen/JiebaSpec/JiebaSpec-generated.mm +7 -0
  21. package/ios/generated/ReactCodegen/JiebaSpec/JiebaSpec.h +1 -0
  22. package/ios/generated/ReactCodegen/JiebaSpecJSI.h +8 -0
  23. package/lib/module/NativeJieba.js.map +1 -1
  24. package/lib/module/NativeJieba.web.js +66 -0
  25. package/lib/module/NativeJieba.web.js.map +1 -0
  26. package/lib/module/index.js +1 -1
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/init.js +34 -7
  29. package/lib/module/init.js.map +1 -1
  30. package/lib/module/init.web.js +40 -0
  31. package/lib/module/init.web.js.map +1 -0
  32. package/lib/module/jieba.js +1 -1
  33. package/lib/module/jieba.js.map +1 -1
  34. package/lib/typescript/src/NativeJieba.d.ts +1 -0
  35. package/lib/typescript/src/NativeJieba.d.ts.map +1 -1
  36. package/lib/typescript/src/NativeJieba.web.d.ts +16 -0
  37. package/lib/typescript/src/NativeJieba.web.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +2 -1
  39. package/lib/typescript/src/index.d.ts.map +1 -1
  40. package/lib/typescript/src/init.d.ts +32 -1
  41. package/lib/typescript/src/init.d.ts.map +1 -1
  42. package/lib/typescript/src/init.web.d.ts +18 -0
  43. package/lib/typescript/src/init.web.d.ts.map +1 -0
  44. package/lib/typescript/src/jieba.d.ts.map +1 -1
  45. package/package.json +13 -1
  46. package/src/NativeJieba.ts +1 -0
  47. package/src/NativeJieba.web.ts +90 -0
  48. package/src/index.tsx +2 -1
  49. package/src/init.ts +43 -6
  50. package/src/init.web.ts +53 -0
  51. package/src/jieba.ts +1 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## 0.3.0
9
+
10
+ ### Breaking changes
11
+
12
+ - **`initJieba` has been renamed to `prepareJieba`.** The old name implied a
13
+ mandatory global init step, which it no longer is on native. Update imports and
14
+ calls:
15
+
16
+ ```diff
17
+ - import { initJieba } from 'react-native-jieba';
18
+ - await initJieba();
19
+ + import { prepareJieba } from 'react-native-jieba';
20
+ + await prepareJieba();
21
+ ```
22
+
23
+ On **web** `prepareJieba()` is still required (it loads the `jieba-wasm`
24
+ binary). On **iOS/Android** it is now optional — see below.
25
+
26
+ `InitJiebaOptions` is likewise renamed to `PrepareJiebaOptions`.
27
+
28
+ ### Added
29
+
30
+ - **`prepareJieba()` is now optional on native.** You can call `cut`, `tag`, etc.
31
+ directly without any setup — the dictionary is located/extracted automatically on
32
+ first use (iOS resolves it from the app bundle; Android lazily extracts the bundled
33
+ assets to `filesDir/jieba-dict/` via an fbjni bridge). Calling `prepareJieba()`
34
+ remains useful as an async warm-up to avoid a one-time blocking pause on Android's
35
+ first call.
36
+ - **`isJiebaReady(): boolean`** — a synchronous readiness check, backed by the real
37
+ native engine state (a new `isReady()` JSI method) rather than a JS flag, so it
38
+ stays correct even when the dictionary is resolved lazily on the first call.
39
+
40
+ ## 0.2.0
41
+
42
+ ### Added
43
+
44
+ - Web support via [`jieba-wasm`](https://github.com/fengkx/jieba-wasm) for
45
+ react-native-web, exposing the same API as the native build.
46
+
47
+ ## 0.1.0
48
+
49
+ - Initial release: cppjieba C++ Turbo Module for iOS and Android with `cut`,
50
+ `cutAll`, `cutForSearch`, `cutHMM`, `cutSmall`, `tag`, `extract`, `insertUserWord`,
51
+ and `find`.
package/Jieba.podspec CHANGED
@@ -14,6 +14,8 @@ Pod::Spec.new do |s|
14
14
  s.source = { :git => "https://github.com/leonsilicon/react-native-jieba.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm}", "cpp/*.{hpp,cpp,c,h}", "ios/generated/*.{h,cpp,mm}"
17
+ # JiebaDictAndroid.{cpp,h} depend on fbjni and are Android-only.
18
+ s.exclude_files = "cpp/JiebaDictAndroid.{cpp,h}"
17
19
  s.private_header_files = "ios/**/*.h"
18
20
 
19
21
  s.resources = ["cpp/cppjieba/dict/*.utf8"]
package/README.md CHANGED
@@ -5,6 +5,7 @@ Native Chinese text segmentation for React Native, powered by [cppjieba](https:/
5
5
  - Runs on the New Architecture as a C++ Turbo Module — no bridge overhead, no JS port of jieba.
6
6
  - Ships the cppjieba dictionaries (`jieba.dict.utf8`, `hmm_model.utf8`, `idf.utf8`, `stop_words.utf8`, `user.dict.utf8`) inside the package, bundled as iOS pod resources and Android assets.
7
7
  - Supports the standard jieba modes: precise (`cut`), full (`cutAll`), search engine (`cutForSearch`), HMM (`cutHMM`), small-word (`cutSmall`), POS tagging (`tag`), and TF-IDF keyword extraction (`extract`).
8
+ - Works on **web** (react-native-web) via [`jieba-wasm`](https://github.com/fengkx/jieba-wasm), with the same API.
8
9
 
9
10
  ## Requirements
10
11
 
@@ -28,13 +29,40 @@ cd ios && pod install
28
29
 
29
30
  Android: no extra steps — autolinking picks up the AAR.
30
31
 
32
+ Web (react-native-web): install the optional `jieba-wasm` peer dependency, which provides the WebAssembly backend.
33
+
34
+ ```sh
35
+ npm install jieba-wasm
36
+ ```
37
+
38
+ Any bundler that handles `.wasm` assets (Vite, Webpack 5, Metro-web, etc.) will bundle the binary automatically.
39
+
40
+ If you use **Vite**, exclude `jieba-wasm` from dependency pre-bundling so its `.wasm` is served from the package directory (otherwise the binary isn't copied into `.vite/deps` and the fetch 404s):
41
+
42
+ ```js
43
+ // vite.config.js
44
+ export default {
45
+ optimizeDeps: { exclude: ['jieba-wasm'] },
46
+ };
47
+ ```
48
+
31
49
  ## Usage
32
50
 
33
- Call `initJieba()` once before the first segmentation call. On iOS the dictionary path is resolved from the app bundle automatically; on Android it extracts the bundled assets to `filesDir/jieba-dict/` on first run.
51
+ On **native (iOS/Android)** there is no setup just call `cut`, `tag`, etc. The dictionary is located/extracted automatically: on iOS the path is resolved from the app bundle, and on Android the bundled assets are extracted to `filesDir/jieba-dict/` on the first call.
52
+
53
+ ```ts
54
+ import { cut } from 'react-native-jieba';
55
+
56
+ cut('我来到北京清华大学');
57
+ // ['我', '来到', '北京', '清华大学']
58
+ ```
59
+
60
+ `prepareJieba()` is an **optional async warm-up** on native: on Android's first run the asset extraction happens off the call site (asynchronously), avoiding a one-time blocking pause on the first `cut`/`tag`. On **web** it is **required** — it loads the `jieba-wasm` binary, which cannot happen synchronously. Use `isJiebaReady()` to check synchronously whether segmentation can run.
34
61
 
35
62
  ```ts
36
63
  import {
37
- initJieba,
64
+ prepareJieba,
65
+ isJiebaReady,
38
66
  cut,
39
67
  cutAll,
40
68
  cutForSearch,
@@ -42,7 +70,10 @@ import {
42
70
  extract,
43
71
  } from 'react-native-jieba';
44
72
 
45
- await initJieba();
73
+ // Native: optional warm-up. Web: required (loads wasm).
74
+ await prepareJieba();
75
+
76
+ isJiebaReady(); // true
46
77
 
47
78
  cut('我来到北京清华大学');
48
79
  // ['我', '来到', '北京', '清华大学']
@@ -62,11 +93,12 @@ extract('我是拖拉机学院手扶拖拉机专业的。', 5);
62
93
 
63
94
  ## API
64
95
 
65
- All segmentation calls are synchronous JSI calls — no Promises.
96
+ On native, all segmentation calls are synchronous JSI calls — no Promises. On web they are synchronous too, but `prepareJieba()` must finish first (it loads the wasm module).
66
97
 
67
98
  | Function | Signature | Notes |
68
99
  | --- | --- | --- |
69
- | `initJieba()` | `() => Promise<void>` | Must be awaited once before any other call. Idempotent. |
100
+ | `prepareJieba(options?)` | `(options?: { wasmUrl?: string \| URL \| Request }) => Promise<void>` | **Web:** must be awaited once before any other call. **Native:** optional warm-up — calling segmentation directly works too. Idempotent. `wasmUrl` is web-only and rarely needed (override where the `.wasm` is served). |
101
+ | `isJiebaReady()` | `() => boolean` | Synchronous check for whether segmentation can run now. Native-backed (reflects the real engine state, not whether `prepareJieba` was called): `true` on iOS from launch, `true` on Android once the dict is resolved (eagerly via `prepareJieba` or lazily on the first call). |
70
102
  | `cut(sentence, hmm?)` | `(string, boolean) => string[]` | Precise mode. `hmm` defaults to `true`. |
71
103
  | `cutAll(sentence)` | `(string) => string[]` | Full mode (every possible word). |
72
104
  | `cutForSearch(sentence, hmm?)` | `(string, boolean) => string[]` | Search-engine mode. |
@@ -77,12 +109,91 @@ All segmentation calls are synchronous JSI calls — no Promises.
77
109
  | `insertUserWord(word, tag?)` | `(string, string) => boolean` | Adds a user dictionary word at runtime. |
78
110
  | `find(word)` | `(string) => boolean` | Tests whether a word is in the dictionary. |
79
111
 
112
+ ### Web support
113
+
114
+ On web the implementation is backed by [`jieba-wasm`](https://github.com/fengkx/jieba-wasm) (jieba-rs compiled to WebAssembly) instead of cppjieba. The core API is identical, with a few caveats because jieba-wasm exposes a smaller surface:
115
+
116
+ | Function | Web behavior |
117
+ | --- | --- |
118
+ | `cut`, `cutAll`, `cutForSearch`, `tag` | Fully supported. |
119
+ | `insertUserWord` | Supported (maps to jieba-wasm `add_word`); the `tag` argument is honored, frequency is auto-assigned. |
120
+ | `cutHMM` | Falls back to `cut(sentence, true)` (jieba-wasm has no HMM-only mode). |
121
+ | `cutSmall` | Falls back to `cut`; the `maxWordLen` argument is accepted for parity but ignored. |
122
+ | `find` | Approximated: returns `true` when the word survives precise (non-HMM) segmentation as a single token. |
123
+ | `extract` | **Not supported** — throws. jieba-wasm does not ship the IDF dictionary needed for TF-IDF extraction. |
124
+
125
+ #### Example (Vite + react-native-web)
126
+
127
+ ```js
128
+ // vite.config.js
129
+ import { defineConfig } from 'vite';
130
+ import react from '@vitejs/plugin-react';
131
+
132
+ export default defineConfig({
133
+ plugins: [react()],
134
+ resolve: {
135
+ alias: { 'react-native': 'react-native-web' },
136
+ },
137
+ // jieba-wasm resolves its `.wasm` via `import.meta.url`; pre-bundling into
138
+ // `.vite/deps` would leave the binary behind, so exclude it.
139
+ optimizeDeps: { exclude: ['jieba-wasm'] },
140
+ });
141
+ ```
142
+
143
+ ```tsx
144
+ // App.tsx
145
+ import { useEffect, useState } from 'react';
146
+ import { Platform, Text, View } from 'react-native';
147
+ import { prepareJieba, cut, cutForSearch, tag, extract } from 'react-native-jieba';
148
+
149
+ export default function App() {
150
+ const [words, setWords] = useState<string[] | null>(null);
151
+
152
+ useEffect(() => {
153
+ // On web this loads + instantiates the wasm module (async) and is required.
154
+ // On native you can skip this and call cut() directly.
155
+ prepareJieba()
156
+ .then(() => {
157
+ setWords(cut('我来到北京清华大学'));
158
+ // → ['我', '来到', '北京', '清华大学']
159
+
160
+ cutForSearch('小明硕士毕业于中国科学院计算所');
161
+ // → ['小明', '硕士', '毕业', '于', '中国', '科学', '学院', ...]
162
+
163
+ tag('我爱北京天安门');
164
+ // → [{ word: '我', tag: 'r' }, { word: '爱', tag: 'v' }, ...]
165
+
166
+ // extract() throws on web — guard it for cross-platform code.
167
+ const keywords = Platform.OS === 'web' ? [] : extract('…', 5);
168
+ })
169
+ .catch((e) => console.error(e));
170
+ }, []);
171
+
172
+ if (!words) return <Text>Loading…</Text>;
173
+ return (
174
+ <View>
175
+ {words.map((w, i) => (
176
+ <Text key={i}>{w}</Text>
177
+ ))}
178
+ </View>
179
+ );
180
+ }
181
+ ```
182
+
183
+ To self-host the binary (CDN or custom path), pass `wasmUrl`:
184
+
185
+ ```ts
186
+ await prepareJieba({ wasmUrl: 'https://cdn.example.com/jieba_rs_wasm_bg.wasm' });
187
+ ```
188
+
80
189
  ## How it works
81
190
 
82
191
  - The Turbo Module lives in `cpp/JiebaImpl.{h,cpp}` and wraps `cppjieba::Jieba` as a JSI Cxx module.
83
- - iOS resolves the dictionary directory from `NSBundle` in `ios/OnLoad.mm` before the module is registered, so `initJieba()` is a no-op on iOS.
84
- - Android ships the dictionary as AAR assets and extracts them on demand via `JiebaAndroidHelperModule`, which then hands the path to the C++ module via the codegen-exposed `setDictPath` JSI method.
192
+ - iOS resolves the dictionary directory from `NSBundle` in `ios/OnLoad.mm` before the module is registered, so segmentation works immediately and `prepareJieba()` is a no-op on iOS.
193
+ - Android ships the dictionary as AAR assets and extracts them to `filesDir/jieba-dict/`. This happens automatically: if `prepareJieba()` is never called, the C++ module extracts them synchronously on the first segmentation call via an fbjni call into `JiebaDict.extractDictDirFromNative` (a one-time cost). `prepareJieba()` does the same extraction asynchronously up front (through `JiebaAndroidHelperModule` → the codegen-exposed `setDictPath` JSI method) so that first call doesn't block.
194
+ - `isJiebaReady()` is backed by the codegen-exposed `isReady()` JSI method, which reads the native engine state directly — so it stays correct even when the dictionary is resolved lazily on the first call.
85
195
  - cppjieba and its `limonp` dependency are vendored as git submodules under `cpp/cppjieba/`. The published npm tarball contains only the headers and dictionaries that are actually needed at build time.
196
+ - On web, `react-native-web`'s bundler resolution picks up `src/NativeJieba.web.ts` and `src/init.web.ts`. `prepareJieba()` lazily imports `jieba-wasm`, awaits its async wasm initializer, and routes the JS API to the wasm exports.
86
197
 
87
198
  ## Contributing
88
199
 
@@ -6,6 +6,7 @@ set (CMAKE_VERBOSE_MAKEFILE ON)
6
6
  add_library(
7
7
  react-native-jieba STATIC
8
8
  ../cpp/JiebaImpl.cpp
9
+ ../cpp/JiebaDictAndroid.cpp
9
10
  )
10
11
 
11
12
  set_target_properties(
@@ -26,4 +27,5 @@ target_link_libraries(
26
27
  react-native-jieba jsi
27
28
  reactnative
28
29
  react_codegen_JiebaSpec
30
+ fbjni
29
31
  )
@@ -32,6 +32,10 @@ public abstract class NativeJiebaSpec extends ReactContextBaseJavaModule impleme
32
32
  return NAME;
33
33
  }
34
34
 
35
+ @ReactMethod(isBlockingSynchronousMethod = true)
36
+ @DoNotStrip
37
+ public abstract boolean isReady();
38
+
35
39
  @ReactMethod
36
40
  @DoNotStrip
37
41
  public abstract void setDictPath(String path);
@@ -12,6 +12,11 @@
12
12
 
13
13
  namespace facebook::react {
14
14
 
15
+ static facebook::jsi::Value __hostFunction_NativeJiebaSpecJSI_isReady(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
16
+ static jmethodID cachedMethodId = nullptr;
17
+ return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, BooleanKind, "isReady", "()Z", args, count, cachedMethodId);
18
+ }
19
+
15
20
  static facebook::jsi::Value __hostFunction_NativeJiebaSpecJSI_setDictPath(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
16
21
  static jmethodID cachedMethodId = nullptr;
17
22
  return static_cast<JavaTurboModule &>(turboModule).invokeJavaMethod(rt, VoidKind, "setDictPath", "(Ljava/lang/String;)V", args, count, cachedMethodId);
@@ -64,6 +69,7 @@ static facebook::jsi::Value __hostFunction_NativeJiebaSpecJSI_find(facebook::jsi
64
69
 
65
70
  NativeJiebaSpecJSI::NativeJiebaSpecJSI(const JavaTurboModule::InitParams &params)
66
71
  : JavaTurboModule(params) {
72
+ methodMap_["isReady"] = MethodMetadata {0, __hostFunction_NativeJiebaSpecJSI_isReady};
67
73
  methodMap_["setDictPath"] = MethodMetadata {1, __hostFunction_NativeJiebaSpecJSI_setDictPath};
68
74
  methodMap_["cut"] = MethodMetadata {2, __hostFunction_NativeJiebaSpecJSI_cut};
69
75
  methodMap_["cutAll"] = MethodMetadata {1, __hostFunction_NativeJiebaSpecJSI_cutAll};
@@ -22,6 +22,7 @@ public:
22
22
 
23
23
  protected:
24
24
  NativeJiebaCxxSpec(std::shared_ptr<CallInvoker> jsInvoker) : TurboModule(std::string{NativeJiebaCxxSpec::kModuleName}, jsInvoker) {
25
+ methodMap_["isReady"] = MethodMetadata {.argCount = 0, .invoker = __isReady};
25
26
  methodMap_["setDictPath"] = MethodMetadata {.argCount = 1, .invoker = __setDictPath};
26
27
  methodMap_["cut"] = MethodMetadata {.argCount = 2, .invoker = __cut};
27
28
  methodMap_["cutAll"] = MethodMetadata {.argCount = 1, .invoker = __cutAll};
@@ -35,6 +36,13 @@ protected:
35
36
  }
36
37
 
37
38
  private:
39
+ static jsi::Value __isReady(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* /*args*/, size_t /*count*/) {
40
+ static_assert(
41
+ bridging::getParameterCount(&T::isReady) == 1,
42
+ "Expected isReady(...) to have 1 parameters");
43
+ return bridging::callFromJs<bool>(rt, &T::isReady, static_cast<NativeJiebaCxxSpec*>(&turboModule)->jsInvoker_, static_cast<T*>(&turboModule));
44
+ }
45
+
38
46
  static jsi::Value __setDictPath(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
39
47
  static_assert(
40
48
  bridging::getParameterCount(&T::setDictPath) == 2,
@@ -3,31 +3,24 @@ package com.jieba
3
3
  import com.facebook.react.bridge.Promise
4
4
  import com.facebook.react.bridge.ReactApplicationContext
5
5
  import com.facebook.react.module.annotations.ReactModule
6
- import java.io.File
7
- import java.io.FileOutputStream
8
6
 
9
7
  @ReactModule(name = JiebaAndroidHelperModule.NAME)
10
8
  class JiebaAndroidHelperModule(reactContext: ReactApplicationContext) :
11
9
  NativeJiebaAndroidHelperSpec(reactContext) {
12
10
 
11
+ init {
12
+ // Capture an application context so the C++ side can lazily extract the
13
+ // dictionaries on first use, even if JS never calls prepareJieba().
14
+ JiebaDict.setContext(reactContext)
15
+ }
16
+
13
17
  override fun getName(): String = NAME
14
18
 
15
19
  override fun prepareDictDir(promise: Promise) {
16
20
  val context = reactApplicationContext
17
21
  Thread {
18
22
  try {
19
- val dictDir = File(context.filesDir, DICT_DIR_NAME)
20
- if (!dictDir.exists()) dictDir.mkdirs()
21
- for (filename in DICT_FILES) {
22
- val target = File(dictDir, filename)
23
- if (target.exists() && target.length() > 0) continue
24
- context.assets.open(filename).use { input ->
25
- FileOutputStream(target).use { output ->
26
- input.copyTo(output)
27
- }
28
- }
29
- }
30
- promise.resolve(dictDir.absolutePath)
23
+ promise.resolve(JiebaDict.extractDictDir(context))
31
24
  } catch (t: Throwable) {
32
25
  promise.reject("E_JIEBA_DICT_EXTRACT", t.message, t)
33
26
  }
@@ -36,13 +29,5 @@ class JiebaAndroidHelperModule(reactContext: ReactApplicationContext) :
36
29
 
37
30
  companion object {
38
31
  const val NAME = "JiebaAndroidHelper"
39
- private const val DICT_DIR_NAME = "jieba-dict"
40
- private val DICT_FILES = arrayOf(
41
- "jieba.dict.utf8",
42
- "hmm_model.utf8",
43
- "user.dict.utf8",
44
- "idf.utf8",
45
- "stop_words.utf8",
46
- )
47
32
  }
48
33
  }
@@ -0,0 +1,72 @@
1
+ package com.jieba
2
+
3
+ import android.content.Context
4
+ import java.io.File
5
+ import java.io.FileOutputStream
6
+
7
+ /**
8
+ * Shared dictionary extraction logic for Android.
9
+ *
10
+ * The cppjieba dictionaries ship as compressed APK assets, but cppjieba needs
11
+ * real filesystem paths. This object extracts them once into `filesDir/jieba-dict`
12
+ * and returns the directory.
13
+ *
14
+ * It is used from two places:
15
+ * - [JiebaAndroidHelperModule.prepareDictDir] — the async warm-up exposed to JS
16
+ * via `prepareJieba()`.
17
+ * - [extractDictDirFromNative] — a synchronous entry point invoked from C++
18
+ * (`JiebaImpl::getJieba`) on the very first segmentation call if `prepareJieba()`
19
+ * was never awaited, so that callers don't have to call `prepareJieba` at all.
20
+ */
21
+ object JiebaDict {
22
+ const val DICT_DIR_NAME = "jieba-dict"
23
+
24
+ val DICT_FILES = arrayOf(
25
+ "jieba.dict.utf8",
26
+ "hmm_model.utf8",
27
+ "user.dict.utf8",
28
+ "idf.utf8",
29
+ "stop_words.utf8",
30
+ )
31
+
32
+ /**
33
+ * Application context captured at module construction so the native (C++)
34
+ * fallback can extract assets without a `ReactApplicationContext` handle.
35
+ */
36
+ @Volatile
37
+ private var appContext: Context? = null
38
+
39
+ @JvmStatic
40
+ fun setContext(context: Context) {
41
+ if (appContext == null) {
42
+ appContext = context.applicationContext
43
+ }
44
+ }
45
+
46
+ /** Synchronously extract the dictionary assets and return the directory path. */
47
+ @JvmStatic
48
+ fun extractDictDir(context: Context): String {
49
+ val dictDir = File(context.filesDir, DICT_DIR_NAME)
50
+ if (!dictDir.exists()) dictDir.mkdirs()
51
+ for (filename in DICT_FILES) {
52
+ val target = File(dictDir, filename)
53
+ if (target.exists() && target.length() > 0) continue
54
+ context.assets.open(filename).use { input ->
55
+ FileOutputStream(target).use { output ->
56
+ input.copyTo(output)
57
+ }
58
+ }
59
+ }
60
+ return dictDir.absolutePath
61
+ }
62
+
63
+ /**
64
+ * Called from C++ via JNI. Returns the dict directory path, or an empty
65
+ * string if no context has been captured yet (extraction not possible).
66
+ */
67
+ @JvmStatic
68
+ fun extractDictDirFromNative(): String {
69
+ val context = appContext ?: return ""
70
+ return extractDictDir(context)
71
+ }
72
+ }
@@ -8,6 +8,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider
8
8
 
9
9
  class JiebaPackage : BaseReactPackage() {
10
10
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ // Capture an application context as early as possible so the C++ module can
12
+ // lazily extract the bundled dictionaries on first use without requiring a
13
+ // call to prepareJieba().
14
+ JiebaDict.setContext(reactContext)
11
15
  return if (name == JiebaAndroidHelperModule.NAME) {
12
16
  JiebaAndroidHelperModule(reactContext)
13
17
  } else {
@@ -0,0 +1,26 @@
1
+ #include "JiebaDictAndroid.h"
2
+
3
+ #include <fbjni/fbjni.h>
4
+
5
+ namespace facebook::react {
6
+
7
+ std::string extractJiebaDictDirAndroid() {
8
+ using namespace facebook::jni;
9
+
10
+ // Calling a JNI method requires a thread attached to the JVM. cut()/tag()
11
+ // run on the JS thread, which fbjni attaches; ThreadScope makes this robust
12
+ // for any caller thread.
13
+ ThreadScope ts;
14
+
15
+ static const auto cls = findClassStatic("com/jieba/JiebaDict");
16
+ static const auto method =
17
+ cls->getStaticMethod<jstring()>("extractDictDirFromNative");
18
+
19
+ local_ref<jstring> result = method(cls);
20
+ if (!result) {
21
+ return "";
22
+ }
23
+ return result->toStdString();
24
+ }
25
+
26
+ }
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ namespace facebook::react {
6
+
7
+ // Android-only: synchronously extracts the bundled dictionary assets (via a
8
+ // JNI call into Kotlin) and returns the directory path. Returns an empty string
9
+ // if extraction is not possible yet (e.g. no application context captured).
10
+ //
11
+ // Defined in JiebaDictAndroid.cpp and only compiled on Android.
12
+ std::string extractJiebaDictDirAndroid();
13
+
14
+ }
package/cpp/JiebaImpl.cpp CHANGED
@@ -1,5 +1,9 @@
1
1
  #include "JiebaImpl.h"
2
2
 
3
+ #ifdef __ANDROID__
4
+ #include "JiebaDictAndroid.h"
5
+ #endif
6
+
3
7
  #include <cppjieba/Jieba.hpp>
4
8
 
5
9
  #include <memory>
@@ -58,6 +62,14 @@ void JiebaImpl::setDictPathFromNative(const std::string& dictPath) {
58
62
  }
59
63
  }
60
64
 
65
+ bool JiebaImpl::isReady(jsi::Runtime& rt) {
66
+ std::lock_guard<std::mutex> lock(dictMutex());
67
+ // Ready once the engine is constructed, or once a dict path is resolved (the
68
+ // engine is then built lazily on first use). On iOS the path is set at +load;
69
+ // on Android it is set eagerly by prepareJieba or lazily on the first call.
70
+ return jiebaStorage() != nullptr || !dictPathStorage().empty();
71
+ }
72
+
61
73
  void JiebaImpl::setDictPath(jsi::Runtime& rt, jsi::String path) {
62
74
  setDictPathFromNative(path.utf8(rt));
63
75
  }
@@ -65,6 +77,17 @@ void JiebaImpl::setDictPath(jsi::Runtime& rt, jsi::String path) {
65
77
  cppjieba::Jieba& JiebaImpl::getJieba() {
66
78
  std::lock_guard<std::mutex> lock(dictMutex());
67
79
  if (!jiebaStorage()) {
80
+ #ifdef __ANDROID__
81
+ // If prepareJieba() was never awaited, extract the bundled assets now (a
82
+ // one-time, synchronous cost on the very first call) so callers don't have
83
+ // to call prepareJieba at all.
84
+ if (dictPathStorage().empty()) {
85
+ std::string extracted = extractJiebaDictDirAndroid();
86
+ if (!extracted.empty()) {
87
+ dictPathStorage() = extracted;
88
+ }
89
+ }
90
+ #endif
68
91
  const std::string& base = dictPathStorage();
69
92
  if (base.empty()) {
70
93
  throw std::runtime_error(
package/cpp/JiebaImpl.h CHANGED
@@ -19,6 +19,7 @@ public:
19
19
 
20
20
  static void setDictPathFromNative(const std::string& dictPath);
21
21
 
22
+ bool isReady(jsi::Runtime& rt);
22
23
  void setDictPath(jsi::Runtime& rt, jsi::String path);
23
24
  jsi::Array cut(jsi::Runtime& rt, jsi::String sentence, bool hmm);
24
25
  jsi::Array cutAll(jsi::Runtime& rt, jsi::String sentence);
@@ -0,0 +1,18 @@
1
+ # CHANGELOG
2
+
3
+ ## v5.6.7
4
+
5
+ + docs: document dictionary file formats
6
+ + test: add `了解` segmentation regression coverage
7
+ + test: replace copyrighted benchmark test data with synthetic text
8
+
9
+ ## v5.6.6
10
+
11
+ + fix: add `习总书记` to the main dictionary for the reported `和习` segmentation case
12
+ + chore: remove stray `韩玉鉴赏` entry from the default user dictionary
13
+
14
+ ## v5.6.5
15
+
16
+ + deps: upgrade limonp to v1.0.2
17
+
18
+ Older release history is preserved in git tags.