@gukhanmun/wasm 0.3.1 → 0.4.0-dev.236

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/dist/index.d.ts CHANGED
@@ -153,13 +153,39 @@ type HomophoneDetection = "context-local" | "dictionary-wide";
153
153
  */
154
154
  type Recovery = "strict" | "lenient";
155
155
  /**
156
- * A single dictionary entry returned by a dictionary lookup.
156
+ * A single dictionary entry: a hanja key with one hangul reading.
157
+ *
158
+ * Callers pass these in {@link MapDictionarySource.entries} to build a
159
+ * dictionary at runtime. Fields mirror the Rust `ReadingCandidate` type plus
160
+ * the hanja key.
157
161
  */
158
162
  interface DictionaryEntry {
159
- /** The hanja form (key), e.g. `"漢字"`. */
163
+ /** The hanja form (dictionary key), e.g. `"漢字"`. */
160
164
  readonly hanja: string;
161
- /** The hangul reading, e.g. `"한자"`. */
165
+ /** The word-initial hangul reading, e.g. `"한자"`. */
162
166
  readonly reading: string;
167
+ /**
168
+ * The reading used when the match is not word-initial, when it differs from
169
+ * {@link reading}. For example a `年代` entry with `reading: "연대"` and
170
+ * `suffixReading: "년대"` reads `1990年代` as `1990년대` but `年代` alone as
171
+ * `연대`.
172
+ *
173
+ * Corresponds to Rust `ReadingCandidate::suffix_reading`.
174
+ */
175
+ readonly suffixReading?: string;
176
+ /**
177
+ * Relative default preference for this reading within its hanja key, a
178
+ * non-negative integer. Larger values win, so an entry listed twice with
179
+ * different preferences keeps competing readings and uses the higher one by
180
+ * default. Absent means `0`.
181
+ *
182
+ * List order never selects the default: when two readings share a preference
183
+ * the tie is broken by reading spelling. Set distinct preferences to choose
184
+ * a default deliberately.
185
+ *
186
+ * Corresponds to Rust `ReadingCandidate::preference`.
187
+ */
188
+ readonly preference?: number;
163
189
  /**
164
190
  * When `true`, the renderer should always show the original hanja
165
191
  * alongside the hangul reading, regardless of ambiguity.
@@ -187,7 +213,8 @@ interface DictionaryEntry {
187
213
  * Deno 2.0+, and Bun only. Throws in browser environments.
188
214
  *
189
215
  * At runtime, a `FileDictionarySource` is distinguished from other values by
190
- * the presence of a `format` property (`"format" in source`).
216
+ * the absence of an `entries` property (`"entries" in source` selects a
217
+ * {@link MapDictionarySource}).
191
218
  */
192
219
  interface FileDictionarySource {
193
220
  /**
@@ -208,16 +235,59 @@ interface FileDictionarySource {
208
235
  * or in-memory bytes; supported in Node-API and (with `from_bytes`) in
209
236
  * WASM builds that include the `cdb` feature.
210
237
  *
211
- * The `"tsv"` format is reserved for future use; passing it throws
212
- * `GukhanmunError` with code `"unsupported-content-type"`.
238
+ * The `"tsv"` format is reserved and never accepted. Pass entries in memory
239
+ * through {@link MapDictionarySource} instead of compiling a table.
213
240
  */
214
241
  readonly format: "fst" | "cdb";
215
242
  }
243
+ /**
244
+ * A dictionary built from entries computed at runtime, such as per-document
245
+ * reading overrides.
246
+ *
247
+ * Use this instead of compiling a TSV file with `gukhanmun-mkdict` when the
248
+ * entries do not exist on disk, for example in a browser, an edge runtime, or a
249
+ * site generator that derives entries per document.
250
+ *
251
+ * A source is distinguished from a {@link FileDictionarySource} by the presence
252
+ * of an `entries` property.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import { stdictFst } from "@gukhanmun/stdict-fst";
257
+ * const g = await load({
258
+ * dictionaries: [
259
+ * {
260
+ * entries: [
261
+ * { hanja: "高橋", reading: "다카하시" },
262
+ * { hanja: "東京", reading: "도쿄", preference: 10 },
263
+ * { hanja: "年代", reading: "연대", suffixReading: "년대" },
264
+ * ],
265
+ * },
266
+ * await stdictFst(),
267
+ * ],
268
+ * });
269
+ * ```
270
+ */
271
+ interface MapDictionarySource {
272
+ /**
273
+ * The entries of this dictionary.
274
+ *
275
+ * Repeated `hanja` keys append competing readings instead of replacing each
276
+ * other, and {@link DictionaryEntry.preference} decides which reading is the
277
+ * context-free default. Several readings for one key also let the engine
278
+ * pick a reading from a following particle (`東京을` versus `東京를`).
279
+ *
280
+ * An empty list contributes nothing, which is a convenient way to express
281
+ * "this document has no overrides".
282
+ */
283
+ readonly entries: readonly DictionaryEntry[];
284
+ }
216
285
  /**
217
286
  * A dictionary source accepted by {@link GukhanmunOptions.dictionaries}.
218
287
  *
219
- * Currently only {@link FileDictionarySource} (binary file / URL / path) is
220
- * supported. Sources are tried in array order; the first match wins.
288
+ * Sources are tried in array order; the first match for a hanja key wins, and
289
+ * every source listed before a compiled dictionary overrides that dictionary
290
+ * for the keys it carries.
221
291
  *
222
292
  * @example
223
293
  * ```ts
@@ -225,7 +295,7 @@ interface FileDictionarySource {
225
295
  * const g = await load({ dictionaries: [await stdictFst()] });
226
296
  * ```
227
297
  */
228
- type DictionarySource = FileDictionarySource;
298
+ type DictionarySource = FileDictionarySource | MapDictionarySource;
229
299
  /**
230
300
  * Fine-grained HTML preservation rules passed in
231
301
  * {@link GukhanmunOptions.html}.
@@ -447,6 +517,10 @@ interface GukhanmunOptions {
447
517
  * `@gukhanmun/loanwords-fst` or `@gukhanmun/loanwords-cdb`. Put user
448
518
  * dictionaries first, then loanwords, then the preset dictionaries.
449
519
  *
520
+ * Entries computed at runtime need no package and no `gukhanmun-mkdict` run:
521
+ * pass {@link MapDictionarySource} (`{ entries: [...] }`) at the position
522
+ * where those overrides should take precedence.
523
+ *
450
524
  * @see {@link DictionarySource}
451
525
  */
452
526
  readonly dictionaries?: readonly DictionarySource[];
@@ -648,9 +722,10 @@ declare class GukhanmunError extends Error {
648
722
  * Creates a Gukhanmun converter with the given options.
649
723
  *
650
724
  * Initialises the WASM module on the first call (subsequent calls reuse the
651
- * cached module). Dictionaries supplied via
652
- * `{@link GukhanmunOptions.dictionaries}` are fetched and passed to the Rust
653
- * engine as `FileDictionarySource` values.
725
+ * cached module). Compiled dictionaries supplied via
726
+ * `{@link GukhanmunOptions.dictionaries}` are fetched, while
727
+ * `{@link MapDictionarySource}` entry lists are serialised and built in memory
728
+ * as Rust `MapDictionary` values.
654
729
  *
655
730
  * Note: unlike the Rust `ko-kr` preset, the JavaScript preset never includes a
656
731
  * bundled dictionary. Pass `dictionaries: [await stdictFst()]` to include the
@@ -663,4 +738,4 @@ declare class GukhanmunError extends Error {
663
738
  */
664
739
  declare function load(options?: GukhanmunOptions): Promise<Gukhanmun>;
665
740
  //#endregion
666
- export { type ContextWindow, type CustomPunctuationProfile, type DelimiterStyle, type DictionaryEntry, type DictionarySource, type Directives, type ErrorCode, type FileDictionarySource, type Format, type Gukhanmun, GukhanmunError, type GukhanmunFactory, type GukhanmunOptions, type HanjaVariantSet, type HomophoneDetection, type HtmlOptions, type NumeralStrategy, type OriginalGloss, type Preset, type PunctuationPreset, type Recovery, type RenderMode, type ResolvedPunctuationProfile, type Segmentation, type SpaceAfter, type StopStyle, load };
741
+ export { type ContextWindow, type CustomPunctuationProfile, type DelimiterStyle, type DictionaryEntry, type DictionarySource, type Directives, type ErrorCode, type FileDictionarySource, type Format, type Gukhanmun, GukhanmunError, type GukhanmunFactory, type GukhanmunOptions, type HanjaVariantSet, type HomophoneDetection, type HtmlOptions, type MapDictionarySource, type NumeralStrategy, type OriginalGloss, type Preset, type PunctuationPreset, type Recovery, type RenderMode, type ResolvedPunctuationProfile, type Segmentation, type SpaceAfter, type StopStyle, load };
package/dist/index.js CHANGED
@@ -77,7 +77,54 @@ function ensureWasm() {
77
77
  }
78
78
  return wasmInit;
79
79
  }
80
+ /** Rejects a source that is not an object before any property is read. */
81
+ function isRecord(value) {
82
+ return typeof value === "object" && value !== null;
83
+ }
84
+ /** The entry fields {@link DictionaryEntry} declares, in serialisation order. */
85
+ const DICTIONARY_ENTRY_FIELDS = [
86
+ "hanja",
87
+ "reading",
88
+ "suffixReading",
89
+ "preference",
90
+ "requireHanja",
91
+ "requireHangul"
92
+ ];
93
+ /**
94
+ * Encodes runtime dictionary entries for the WASM boundary.
95
+ *
96
+ * Each entry is copied field by field, so a class instance exposing its
97
+ * properties through getters serialises the same values it presents, and any
98
+ * extra property a caller carries is dropped rather than rejected. The copy
99
+ * also keeps `NaN` and `±Infinity` preferences visible: `JSON.stringify` would
100
+ * erase them to `null`, which the Rust side reads as "absent".
101
+ */
102
+ function encodeEntries(entries) {
103
+ if (!Array.isArray(entries)) throw new GukhanmunError("invalid-input", "Dictionary 'entries' must be an array");
104
+ const records = [];
105
+ for (const [index, entry] of entries.entries()) {
106
+ if (!isRecord(entry)) {
107
+ records.push(entry);
108
+ continue;
109
+ }
110
+ const record = {};
111
+ for (const field of DICTIONARY_ENTRY_FIELDS) record[field] = entry[field];
112
+ const preference = record["preference"];
113
+ if (preference != null && !Number.isInteger(preference)) throw new GukhanmunError("invalid-input", `dictionary entry ${index} has a non-integer \`preference\`: ${String(preference)}`);
114
+ records.push(record);
115
+ }
116
+ try {
117
+ return JSON.stringify(records);
118
+ } catch (e) {
119
+ throw new GukhanmunError("invalid-input", `Dictionary entries are not serialisable: ${e instanceof Error ? e.message : String(e)}`);
120
+ }
121
+ }
80
122
  async function resolveDictionary(source) {
123
+ if (typeof source !== "object" || source === null) throw new GukhanmunError("invalid-input", "Each dictionary must be an object");
124
+ if ("entries" in source) {
125
+ if ("format" in source || "data" in source) throw new GukhanmunError("invalid-input", "A dictionary must not combine 'entries' with 'format' or 'data'");
126
+ return { entriesJson: encodeEntries(source.entries) };
127
+ }
81
128
  if (source.data instanceof ArrayBuffer) return {
82
129
  format: source.format,
83
130
  bytes: new Uint8Array(source.data)
@@ -223,9 +270,10 @@ function resolveOptions(opts = {}) {
223
270
  * Creates a Gukhanmun converter with the given options.
224
271
  *
225
272
  * Initialises the WASM module on the first call (subsequent calls reuse the
226
- * cached module). Dictionaries supplied via
227
- * `{@link GukhanmunOptions.dictionaries}` are fetched and passed to the Rust
228
- * engine as `FileDictionarySource` values.
273
+ * cached module). Compiled dictionaries supplied via
274
+ * `{@link GukhanmunOptions.dictionaries}` are fetched, while
275
+ * `{@link MapDictionarySource}` entry lists are serialised and built in memory
276
+ * as Rust `MapDictionary` values.
229
277
  *
230
278
  * Note: unlike the Rust `ko-kr` preset, the JavaScript preset never includes a
231
279
  * bundled dictionary. Pass `dictionaries: [await stdictFst()]` to include the
@@ -301,6 +301,10 @@ function __wbg_get_imports() {
301
301
  const ret = arg0[arg1];
302
302
  return ret;
303
303
  },
304
+ __wbg_has_73740b27f436fed3: function() { return handleError(function (arg0, arg1) {
305
+ const ret = Reflect.has(arg0, arg1);
306
+ return ret;
307
+ }, arguments); },
304
308
  __wbg_instanceof_ArrayBuffer_8f49811467741499: function(arg0) {
305
309
  let result;
306
310
  try {
@@ -303,6 +303,10 @@ function __wbg_get_imports() {
303
303
  const ret = arg0[arg1];
304
304
  return ret;
305
305
  },
306
+ __wbg_has_73740b27f436fed3: function() { return handleError(function (arg0, arg1) {
307
+ const ret = Reflect.has(arg0, arg1);
308
+ return ret;
309
+ }, arguments); },
306
310
  __wbg_instanceof_ArrayBuffer_8f49811467741499: function(arg0) {
307
311
  let result;
308
312
  try {
@@ -4,7 +4,7 @@
4
4
  "Hong Minhe (洪 民憙) <hong@minhee.org>"
5
5
  ],
6
6
  "description": "WebAssembly binding for Gukhanmun via wasm-bindgen.",
7
- "version": "0.3.1",
7
+ "version": "0.4.0",
8
8
  "license": "GPL-3.0-only",
9
9
  "repository": {
10
10
  "type": "git",
@@ -301,6 +301,10 @@ function __wbg_get_imports() {
301
301
  const ret = arg0[arg1];
302
302
  return ret;
303
303
  },
304
+ __wbg_has_73740b27f436fed3: function() { return handleError(function (arg0, arg1) {
305
+ const ret = Reflect.has(arg0, arg1);
306
+ return ret;
307
+ }, arguments); },
304
308
  __wbg_instanceof_ArrayBuffer_8f49811467741499: function(arg0) {
305
309
  let result;
306
310
  try {
@@ -5,7 +5,7 @@
5
5
  "Hong Minhe (洪 民憙) <hong@minhee.org>"
6
6
  ],
7
7
  "description": "WebAssembly binding for Gukhanmun via wasm-bindgen.",
8
- "version": "0.3.1",
8
+ "version": "0.4.0",
9
9
  "license": "GPL-3.0-only",
10
10
  "repository": {
11
11
  "type": "git",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gukhanmun/wasm",
3
- "version": "0.3.1",
3
+ "version": "0.4.0-dev.236",
4
4
  "description": "WebAssembly implementation of the Gukhanmun hanja-to-hangul converter.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -54,7 +54,7 @@
54
54
  "@gukhanmun/types": "*"
55
55
  },
56
56
  "devDependencies": {
57
- "@gukhanmun/types": "0.3.1"
57
+ "@gukhanmun/types": "0.4.0-dev.236+75afc34"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown && node -e \"const fs=require('node:fs'),p=require('node:path');fs.cpSync('wasm','dist/wasm',{recursive:true,force:true,filter:(s)=>p.basename(s)!=='.gitignore'})\""