@gukhanmun/wasm 0.1.0-dev.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.
- package/LICENSE +674 -0
- package/dist/index.d.ts +575 -0
- package/dist/index.js +248 -0
- package/dist/wasm/deno/gukhanmun_wasm.d.ts +68 -0
- package/dist/wasm/deno/gukhanmun_wasm.js +576 -0
- package/dist/wasm/deno/gukhanmun_wasm_bg.wasm +0 -0
- package/dist/wasm/deno/gukhanmun_wasm_bg.wasm.d.ts +18 -0
- package/dist/wasm/nodejs/gukhanmun_wasm.d.ts +68 -0
- package/dist/wasm/nodejs/gukhanmun_wasm.js +590 -0
- package/dist/wasm/nodejs/gukhanmun_wasm_bg.wasm +0 -0
- package/dist/wasm/nodejs/gukhanmun_wasm_bg.wasm.d.ts +18 -0
- package/dist/wasm/nodejs/package.json +21 -0
- package/dist/wasm/web/gukhanmun_wasm.d.ts +111 -0
- package/dist/wasm/web/gukhanmun_wasm.js +683 -0
- package/dist/wasm/web/gukhanmun_wasm_bg.wasm +0 -0
- package/dist/wasm/web/gukhanmun_wasm_bg.wasm.d.ts +18 -0
- package/dist/wasm/web/package.json +25 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
//#region index.ts
|
|
2
|
+
function isNodeLike() {
|
|
3
|
+
return typeof globalThis.process?.versions?.node === "string";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown by `{@link load}`, `{@link Gukhanmun.convert}`, and
|
|
7
|
+
* `{@link Gukhanmun.stream}` when the Rust engine reports a failure.
|
|
8
|
+
*
|
|
9
|
+
* `code` identifies the failure class; `chain` carries the full causal chain
|
|
10
|
+
* materialised at the FFI boundary so callers do not need additional round
|
|
11
|
+
* trips.
|
|
12
|
+
*/
|
|
13
|
+
var GukhanmunError = class extends Error {
|
|
14
|
+
/**
|
|
15
|
+
* Machine-readable error code.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link ErrorCode}
|
|
18
|
+
*/
|
|
19
|
+
code;
|
|
20
|
+
/**
|
|
21
|
+
* Full causal chain from the Rust `Error::source()` traversal, materialised
|
|
22
|
+
* at the FFI boundary. The first element is the root cause; the last is
|
|
23
|
+
* the immediate error.
|
|
24
|
+
*/
|
|
25
|
+
chain;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new `GukhanmunError`.
|
|
28
|
+
*
|
|
29
|
+
* @param code - Machine-readable error code.
|
|
30
|
+
* @param message - Human-readable description.
|
|
31
|
+
* @param chain - Optional causal chain.
|
|
32
|
+
*/
|
|
33
|
+
constructor(code, message, chain = []) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "GukhanmunError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.chain = chain;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
let wasmInit;
|
|
41
|
+
/**
|
|
42
|
+
* Loads and caches the WASM module. The module URL is resolved relative to
|
|
43
|
+
* this source file so it works with Deno's module graph and `import.meta.url`.
|
|
44
|
+
*
|
|
45
|
+
* Node.js `fetch()` does not support `file://` URLs, so on Node.js the WASM
|
|
46
|
+
* binary is read via `node:fs/promises` and passed directly as a `Uint8Array`
|
|
47
|
+
* to the web-target init function, bypassing the streaming-fetch path.
|
|
48
|
+
* Deno and Bun support `fetch()` for `file://` URLs and use the default path.
|
|
49
|
+
*/
|
|
50
|
+
function ensureWasm() {
|
|
51
|
+
if (!wasmInit) {
|
|
52
|
+
const glueHref = new URL("./wasm/web/gukhanmun_wasm.js", import.meta.url).href;
|
|
53
|
+
wasmInit = (async () => {
|
|
54
|
+
const mod = await import(glueHref);
|
|
55
|
+
if (isNodeLike()) {
|
|
56
|
+
const wasmUrl = new URL("./wasm/web/gukhanmun_wasm_bg.wasm", import.meta.url);
|
|
57
|
+
const buf = await (await import("node:fs/promises")).readFile(wasmUrl);
|
|
58
|
+
const bytes = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
59
|
+
await mod.default({ module_or_path: bytes });
|
|
60
|
+
} else await mod.default();
|
|
61
|
+
return mod;
|
|
62
|
+
})();
|
|
63
|
+
}
|
|
64
|
+
return wasmInit;
|
|
65
|
+
}
|
|
66
|
+
async function resolveDictionary(source) {
|
|
67
|
+
if (source.data instanceof ArrayBuffer) return {
|
|
68
|
+
format: source.format,
|
|
69
|
+
bytes: new Uint8Array(source.data)
|
|
70
|
+
};
|
|
71
|
+
if (ArrayBuffer.isView(source.data)) {
|
|
72
|
+
const view = source.data;
|
|
73
|
+
return {
|
|
74
|
+
format: source.format,
|
|
75
|
+
bytes: new Uint8Array(view.buffer, view.byteOffset, view.byteLength)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let url;
|
|
79
|
+
if (source.data instanceof URL) url = source.data;
|
|
80
|
+
else {
|
|
81
|
+
const str = String(source.data);
|
|
82
|
+
if (str.includes("://")) url = new URL(str);
|
|
83
|
+
else if (isNodeLike()) url = (await import("node:url")).pathToFileURL(str);
|
|
84
|
+
else throw new GukhanmunError("invalid-input", "File path strings require a Node.js environment; use URL or ArrayBuffer in browsers");
|
|
85
|
+
}
|
|
86
|
+
if (isNodeLike() && url.protocol === "file:") {
|
|
87
|
+
const fs = await import("node:fs/promises");
|
|
88
|
+
let buf;
|
|
89
|
+
try {
|
|
90
|
+
buf = await fs.readFile(url);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw new GukhanmunError("dictionary-load", `Failed to read dictionary: ${e instanceof Error ? e.message : String(e)}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
format: source.format,
|
|
96
|
+
bytes: new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await fetch(url);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
throw new GukhanmunError("dictionary-load", `Failed to fetch dictionary: ${e instanceof Error ? e.message : String(e)}`);
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) throw new GukhanmunError("dictionary-load", `Failed to fetch dictionary: HTTP ${response.status}`);
|
|
106
|
+
return {
|
|
107
|
+
format: source.format,
|
|
108
|
+
bytes: new Uint8Array(await response.arrayBuffer())
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Internal implementation of the `Gukhanmun` contract. */
|
|
112
|
+
var GukhanmunImpl = class {
|
|
113
|
+
#handle;
|
|
114
|
+
options;
|
|
115
|
+
constructor(handle, resolvedOpts) {
|
|
116
|
+
this.#handle = handle;
|
|
117
|
+
this.options = resolvedOpts;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Converts `source` in one shot.
|
|
121
|
+
*
|
|
122
|
+
* @param source - Input string.
|
|
123
|
+
* @param format - `"text"` (default), `"html"`, `"markdown"`, or
|
|
124
|
+
* `{ format: "markdown"; gfm?: boolean }`.
|
|
125
|
+
* @returns Converted string.
|
|
126
|
+
* @throws {@link GukhanmunError} on conversion failure.
|
|
127
|
+
*/
|
|
128
|
+
convert(source, format) {
|
|
129
|
+
try {
|
|
130
|
+
return this.#handle.convert(source, format ?? null);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
throw liftError(e);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns a `TransformStream` that converts string chunks.
|
|
137
|
+
*
|
|
138
|
+
* Chunks are buffered internally and the full conversion runs on `flush`.
|
|
139
|
+
* This satisfies batch-equivalence: the output of any chunk partition equals
|
|
140
|
+
* the output of `{@link convert}` on the concatenated input.
|
|
141
|
+
*
|
|
142
|
+
* @param format - Same as {@link convert}.
|
|
143
|
+
* @returns A `TransformStream<string, string>`.
|
|
144
|
+
*/
|
|
145
|
+
stream(format) {
|
|
146
|
+
let streamHandle;
|
|
147
|
+
try {
|
|
148
|
+
streamHandle = this.#handle.open_stream(format ?? null);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
throw liftError(e);
|
|
151
|
+
}
|
|
152
|
+
return new TransformStream({
|
|
153
|
+
transform(chunk, controller) {
|
|
154
|
+
const out = streamHandle.push(chunk);
|
|
155
|
+
if (out) controller.enqueue(out);
|
|
156
|
+
},
|
|
157
|
+
flush(controller) {
|
|
158
|
+
try {
|
|
159
|
+
const out = streamHandle.finish();
|
|
160
|
+
if (out) controller.enqueue(out);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
throw liftError(e);
|
|
163
|
+
} finally {
|
|
164
|
+
streamHandle.free();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/** Converts a raw error object thrown from the WASM boundary into a `GukhanmunError`. */
|
|
171
|
+
function liftError(raw) {
|
|
172
|
+
if (raw instanceof GukhanmunError) return raw;
|
|
173
|
+
if (raw != null && typeof raw === "object") {
|
|
174
|
+
const obj = raw;
|
|
175
|
+
return new GukhanmunError(typeof obj["code"] === "string" ? obj["code"] : "internal", typeof obj["message"] === "string" ? obj["message"] : String(raw), Array.isArray(obj["chain"]) ? obj["chain"] : []);
|
|
176
|
+
}
|
|
177
|
+
return new GukhanmunError("internal", String(raw));
|
|
178
|
+
}
|
|
179
|
+
function resolveOptions(opts = {}) {
|
|
180
|
+
const preset = opts.preset ?? "ko-kr";
|
|
181
|
+
const koKp = preset === "ko-kp";
|
|
182
|
+
return {
|
|
183
|
+
preset,
|
|
184
|
+
rendering: opts.rendering ?? "hangul-only",
|
|
185
|
+
segmentation: opts.segmentation ?? "lattice",
|
|
186
|
+
numerals: opts.numerals ?? "hangul-phonetic",
|
|
187
|
+
initialSoundLaw: opts.initialSoundLaw ?? (koKp ? false : true),
|
|
188
|
+
homophoneWindow: opts.homophoneWindow ?? (koKp ? "off" : "per-block"),
|
|
189
|
+
firstOccurrenceWindow: opts.firstOccurrenceWindow ?? "off",
|
|
190
|
+
recovery: opts.recovery ?? "strict"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Creates a Gukhanmun converter with the given options.
|
|
195
|
+
*
|
|
196
|
+
* Initialises the WASM module on the first call (subsequent calls reuse the
|
|
197
|
+
* cached module). Dictionaries supplied via
|
|
198
|
+
* `{@link GukhanmunOptions.dictionaries}` are fetched and passed to the Rust
|
|
199
|
+
* engine as `FileDictionarySource` values.
|
|
200
|
+
*
|
|
201
|
+
* Note: unlike the Rust `ko-kr` preset, the JavaScript preset never includes a
|
|
202
|
+
* bundled dictionary. Pass `dictionaries: [await stdictFst()]` to include the
|
|
203
|
+
* Standard Korean Language Dictionary.
|
|
204
|
+
*
|
|
205
|
+
* @param options - Conversion options. All fields are optional; defaults match
|
|
206
|
+
* the `ko-kr` preset.
|
|
207
|
+
* @returns A `{@link Gukhanmun}` instance.
|
|
208
|
+
* @throws {@link GukhanmunError} on invalid options or dictionary load failure.
|
|
209
|
+
*/
|
|
210
|
+
async function load(options = {}) {
|
|
211
|
+
const wasm = await ensureWasm();
|
|
212
|
+
const resolved = resolveOptions(options);
|
|
213
|
+
const dicts = await Promise.all((options.dictionaries ?? []).map(resolveDictionary));
|
|
214
|
+
const rawOpts = buildRawOptions(options, resolved);
|
|
215
|
+
let handle;
|
|
216
|
+
try {
|
|
217
|
+
handle = new wasm.WasmGukhanmun(rawOpts, dicts);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
throw liftError(e);
|
|
220
|
+
}
|
|
221
|
+
return new GukhanmunImpl(handle, resolved);
|
|
222
|
+
}
|
|
223
|
+
/** Builds the plain-object options passed across the WASM boundary. */
|
|
224
|
+
function buildRawOptions(opts, resolved) {
|
|
225
|
+
const raw = {
|
|
226
|
+
preset: resolved.preset,
|
|
227
|
+
rendering: resolved.rendering,
|
|
228
|
+
segmentation: resolved.segmentation,
|
|
229
|
+
numerals: resolved.numerals,
|
|
230
|
+
initialSoundLaw: resolved.initialSoundLaw,
|
|
231
|
+
homophoneWindow: resolved.homophoneWindow,
|
|
232
|
+
firstOccurrenceWindow: resolved.firstOccurrenceWindow,
|
|
233
|
+
recovery: resolved.recovery
|
|
234
|
+
};
|
|
235
|
+
if (resolved.rendering === "original" && opts.originalGloss != null) raw["originalGloss"] = opts.originalGloss;
|
|
236
|
+
if (opts.directives != null) raw["directives"] = {
|
|
237
|
+
requireHanja: opts.directives.requireHanja ?? [],
|
|
238
|
+
requireHangul: opts.directives.requireHangul ?? [],
|
|
239
|
+
skipAnnotation: opts.directives.skipAnnotation ?? []
|
|
240
|
+
};
|
|
241
|
+
if (opts.html != null) raw["html"] = {
|
|
242
|
+
preserveClasses: opts.html.preserveClasses ?? [],
|
|
243
|
+
preserveAttributes: opts.html.preserveAttributes ?? []
|
|
244
|
+
};
|
|
245
|
+
return raw;
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
export { GukhanmunError, load };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Owning hanja-to-hangul converter exposed to JavaScript.
|
|
6
|
+
*
|
|
7
|
+
* Construct via the `constructor` and keep the instance alive for the
|
|
8
|
+
* duration of all conversions. Each call to [`WasmGukhanmun::open_stream`]
|
|
9
|
+
* borrows this instance via a reference count; calling `free()` on the JS
|
|
10
|
+
* side drops the Rust value when the last stream is also freed.
|
|
11
|
+
*/
|
|
12
|
+
export class WasmGukhanmun {
|
|
13
|
+
free(): void;
|
|
14
|
+
[Symbol.dispose](): void;
|
|
15
|
+
/**
|
|
16
|
+
* Converts `source` in one shot and returns the result string.
|
|
17
|
+
*
|
|
18
|
+
* `format` is `"text"` (default), `"html"`, `"markdown"`, or
|
|
19
|
+
* `{ format: "markdown", gfm?: boolean }`. Throws a `GukhanmunError`
|
|
20
|
+
* on conversion failure.
|
|
21
|
+
*/
|
|
22
|
+
convert(source: string, format: any): string;
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new converter from a JS options object and an array of
|
|
25
|
+
* dictionary records.
|
|
26
|
+
*
|
|
27
|
+
* `options` is a JSON-serialisable `GukhanmunOptions` object (may be
|
|
28
|
+
* `null` / `undefined` for all-defaults). `dictionaries` is a JS `Array`
|
|
29
|
+
* where each element is `{ format: "fst", bytes: Uint8Array }`.
|
|
30
|
+
*
|
|
31
|
+
* Throws a `GukhanmunError`-shaped object on invalid input or a failed
|
|
32
|
+
* dictionary load.
|
|
33
|
+
*/
|
|
34
|
+
constructor(options: any, dictionaries: any);
|
|
35
|
+
/**
|
|
36
|
+
* Opens a streaming handle for chunked conversion.
|
|
37
|
+
*
|
|
38
|
+
* The caller must feed string chunks via [`WasmStream::push`] and call
|
|
39
|
+
* [`WasmStream::finish`] to flush the final output. The batch-equivalence
|
|
40
|
+
* invariant holds: concatenating all `push` and `finish` return values
|
|
41
|
+
* equals the result of a single `convert` call on the concatenated input.
|
|
42
|
+
*/
|
|
43
|
+
open_stream(format: any): WasmStream;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Chunked streaming handle produced by [`WasmGukhanmun::open_stream`].
|
|
48
|
+
*
|
|
49
|
+
* Accumulates string chunks and performs the full conversion at
|
|
50
|
+
* [`WasmStream::finish`]. This satisfies the batch-equivalence invariant:
|
|
51
|
+
* every arbitrary chunk partition of an input produces the same final output
|
|
52
|
+
* as a single [`WasmGukhanmun::convert`] call.
|
|
53
|
+
*/
|
|
54
|
+
export class WasmStream {
|
|
55
|
+
private constructor();
|
|
56
|
+
free(): void;
|
|
57
|
+
[Symbol.dispose](): void;
|
|
58
|
+
/**
|
|
59
|
+
* Converts the buffered input and returns the result. Clears the buffer
|
|
60
|
+
* so the stream handle can be reused.
|
|
61
|
+
*/
|
|
62
|
+
finish(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Appends `chunk` to the internal buffer. Returns an empty string; all
|
|
65
|
+
* output is deferred to [`WasmStream::finish`].
|
|
66
|
+
*/
|
|
67
|
+
push(chunk: string): string;
|
|
68
|
+
}
|