lyric-romanizer 0.1.1 → 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 — 2026-07-25
4
+
5
+ Architecture pass: every piece of per-script knowledge now lives in one table per side of the light/heavy packaging seam, engines are lazy and injectable, and the universal fallback is observable. Public interface changes are **additive**; the two deliberate output-behavior changes are listed below with their rationale.
6
+
7
+ ### Added
8
+
9
+ - **Engine adapters** — `createRomanizer({ engines })` overrides any built-in engine or plugs an adapter for externally-romanized scripts (`arabic`, `hebrew`, `malayalam`, `bengali`, `other`). The library still performs zero network I/O by default; `UnsupportedRomanizationError` is now precisely "no engine for this script".
10
+ - **`RomanizeResult.fallbacks`** — per-line flags, `true` where the script engine failed and the line was universally transliterated as a last resort. Previously this degradation was silent and indistinguishable from success.
11
+ - **`requiresExternalRomanization` on the detector subpath** — pure classification moved to the light, engine-free side; `lyric-romanizer/detector` can now answer the full pre-romanization triage (which script → is it latin → needs external) with zero engine payload. Still re-exported from the main entry.
12
+ - **`RomanizeEngine`, `RomanizeEngineContext`, `ExternalScript`** exported types.
13
+ - **Plain Node ESM support** — `import 'lyric-romanizer'` previously crashed outside bundlers (`@romanize/korean` named-export interop); engines now load through interop-safe dynamic imports.
14
+ - **Lazy engine loading** — every engine loads on first use. Importing the main entry no longer executes any engine package; a Korean-only consumer never loads pinyin-pro or kuroshiro. Failed loads retry on the next call and degrade to the universal fallback (flagged) rather than crashing playback.
15
+
16
+ ### Changed — deliberate output-behavior changes
17
+
18
+ 1. **Latin guard under a pinned script.** `romanizeLines` pins one script for the whole array; lines with ASCII letters and no character of any detectable script are now returned unchanged instead of being fed to the pinned engine. Before: `romanizeLines(['Hello', '世界'])` → `['H e l l o', 'shì jiè']` (pinyin-pro spaces out Latin text). After: `['Hello', 'shì jiè']`. Rationale: the engine cannot romanize what isn't its script; the old output was mangled.
19
+ 2. **Out-of-contract script strings throw.** Untyped callers pinning a string outside `ScriptType` (e.g. `{ script: 'greek' }`) now get `UnsupportedRomanizationError` instead of a silent universal transliteration. This includes names inherited from `Object.prototype`: `{ script: 'toString' }` previously resolved to the inherited method and returned `'[object Undefined]'`. Unaffected: auto-detected scripts (detection only ever returns `ScriptType` members), and empty or letterless lines, which short-circuit to a no-op before any script is consulted — as they always did.
20
+
21
+ All romanized output strings are byte-identical to 0.2.0 apart from those two cases, verified against a golden corpus across every supported script, dialect, preset, and error path. Two further notes for anyone diffing behavior:
22
+
23
+ - `romanizeLine` with an engineless pinned script (e.g. `{ script: 'arabic' }`) still throws even when the line is pure Latin — the engine check precedes the latin guard, matching `romanizeLines`, which rejects such a script before looking at any line.
24
+ - `NON_LATIN_SCRIPT_RE` is now derived from the range table, so its `.source` text changed: ranges are emitted in ascending code-point order, and the adjacent Hebrew and Arabic ranges collapse into one (8 groups → 7). The matching contract is unchanged — `.flags` is still `''` and `.test()` agrees with the old literal for every code point in the BMP, asserted by a test sweep. Only a consumer that snapshots, hashes, or string-compares the pattern text is affected; one that uses it as a matcher is not.
25
+
26
+ ### Fixed
27
+
28
+ - README Quick Start example was impossible: `romanizeLines(['你好世界', 'こんにちは'])` returns `{ script: 'japanese' }` (kana is definitive), not `{ script: 'chinese' }` as documented. The example and the detection-granularity contract are now documented accurately.
29
+ - Documented romanization outputs corrected against the actual engines (in all nine READMEs): Thai `สวัสดี` → `swasdi` (was `sawatdi`), Gurmukhi `ਨਮਸਤੇ` → `namasate` (was `namaste`; standard Punjabi spelling has no virama, so both consonants keep the inherent vowel), and the Kannada sample is now the correctly-spelled `ನಮಸ್ತೇ` → `namaste`. The feature list said "7 Indic scripts"; there are 6.
30
+ - Dead defensive throws in the Sanscript branch (unreachable behind the catch-all fallback) removed; the silent `default:` switch arm is gone — the engine table is total over the script union, so a forgotten engine row is a **compile error** instead of silent garbage.
31
+ - `NON_LATIN_SCRIPT_RE` is now derived from the same per-script range table that drives detection (previously a hand-maintained duplicate with a "keep aligned" comment). Verified byte-for-byte identical across the BMP.
32
+
33
+ ### Internal
34
+
35
+ - Per-script knowledge concentrated: detection ranges + external classification in `SCRIPT_METADATA` (detector side), engine bindings in one total record (romanizer side). Adding a script is one row per side, compiler-enforced.
36
+ - `selectCyrillicPreset` extracted as a pure, unit-tested function (the Ukrainian-detection headline feature previously had zero coverage).
37
+ - Dependencies updated to latest (TypeScript 7, vitest 4.1.10, pinyin-pro 3.28.2). CI, Dependabot (daily, grouped), and a weekly latest-deps canary keep every engine on its newest release.
38
+ - Test suite grew from 19 to 44 tests: injection seam, fallback observability, lazy-init retry, preset selection, latin guard, regex-derivation equivalence, tie-break regression, BMP-range invariant.
39
+ - `pnpm run smoke` (`scripts/smoke.mjs`) exercises the built package under plain Node ESM: every engine's interop chain, the fallback path, both entry points, and an assertion that `dist/detector.js` imports nothing. Runs in CI and in the canary; `pnpm run verify` chains typecheck + build + test + smoke.
40
+ - `package.json` declares `"sideEffects": false` (safe now that engines are lazy) and ships `src` so the published source maps resolve.
package/README.md CHANGED
@@ -3,10 +3,28 @@
3
3
  [![npm version](https://img.shields.io/npm/v/lyric-romanizer.svg)](https://www.npmjs.com/package/lyric-romanizer)
4
4
  [![license](https://img.shields.io/npm/l/lyric-romanizer.svg)](https://github.com/thedavidweng/lyric-romanizer/blob/main/LICENSE)
5
5
 
6
- Script detection and local romanization engine for lyrics. Supports 13 scripts across Japanese, Chinese, Korean, Cyrillic, Indic, Tamil, Thai, and Latin — all running locally with zero API calls.
6
+ [English](https://github.com/thedavidweng/lyric-romanizer#readme) | [日本語](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ja.md) | [中文(简体)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-CN.md) | [中文(粵語)](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.zh-yue.md) | [한국어](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ko.md) | [Русский](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ru.md) | [हिन्दी](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.hi.md) | [தமிழ்](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.ta.md) | [ไทย](https://github.com/thedavidweng/lyric-romanizer/blob/main/docs/README.th.md)
7
+
8
+ > **Philosophy: Don't reinvent the wheel.**
9
+ > This project deliberately avoids building romanization logic from scratch. Instead, it composes best-in-class, community-maintained libraries — one for each script — and focuses on the orchestration layer: script detection, engine routing, dialect handling, and a unified API. Every romanization engine in the dependency list is a dedicated, battle-tested library maintained by domain experts. That's the point.
10
+
11
+ Script detection and local romanization engine for lyrics. Supports 12 scripts across Japanese, Chinese (Mandarin and Cantonese), Korean, Cyrillic, Indic, Tamil, and Thai — all running locally with zero API calls.
7
12
 
8
13
  Extracted from [Spotify Karaoke](https://github.com/haroldalan/spotify-karaoke). Used by [OpenKara](https://github.com/thedavidweng/openkara).
9
14
 
15
+ ## Features
16
+
17
+ - **Zero API calls** — all romanization runs locally
18
+ - **Auto script detection** — pass in text, get back the detected script
19
+ - **12+ scripts** — Japanese, Chinese, Korean, Cyrillic, 6 Indic scripts, Tamil, Thai
20
+ - **Cantonese support** — Jyutping alongside default Mandarin Pinyin
21
+ - **Lightweight detector subpath** — import only script detection (and external-script classification) without pulling in romanization engines
22
+ - **Lazy engines** — every engine loads on first use; importing the main entry costs nothing until you romanize
23
+ - **Pluggable engines** — override any built-in engine, or plug your own adapter for externally-romanized scripts
24
+ - **Observable fallbacks** — per-line flags tell you when an engine failed and a line was transliterated as a last resort
25
+ - **Ukrainian-aware Cyrillic** — auto-detects Ukrainian-specific characters and applies the correct transliteration preset
26
+ - **Runs in plain Node ESM** — no bundler required
27
+
10
28
  ## Installation
11
29
 
12
30
  ```bash
@@ -29,14 +47,16 @@ import { createRomanizer, detectScript } from 'lyric-romanizer';
29
47
  const romanizer = createRomanizer();
30
48
 
31
49
  // Auto-detect script and romanize
32
- const result = await romanizer.romanizeLines(['你好世界', 'こんにちは']);
33
- // { script: 'chinese', lines: ['nǐ hǎo shì jiè', 'こんにちは'] }
50
+ const result = await romanizer.romanizeLines(['你好世界', '很高兴认识你']);
51
+ // { script: 'chinese', lines: ['nǐ hǎo shì jiè', 'hěn gāo xìng rèn shi nǐ'], fallbacks: [false, false] }
34
52
 
35
53
  // Romanize a single line
36
54
  const line = await romanizer.romanizeLine('안녕하세요');
37
55
  // 'annyeonghaseyo'
38
56
  ```
39
57
 
58
+ > **Detection granularity** — `romanizeLines` detects the dominant script **once across all lines** and pins it for every line (deliberate: kanji-only lines inside a Japanese song must reach the Japanese engine, and any kana in the array pins `japanese`). Calling `romanizeLine` in a loop instead detects **per line** and may route each line to a different engine. Pure-Latin lines under a pinned script are returned unchanged. See [Mixed-script arrays](#mixed-script-arrays).
59
+
40
60
  ## API
41
61
 
42
62
  ### Imports
@@ -52,7 +72,12 @@ import {
52
72
  } from 'lyric-romanizer';
53
73
 
54
74
  // Detector-only subpath — lightweight, no romanization dependencies
55
- import { detectScript, isLatinScript, NON_LATIN_SCRIPT_RE } from 'lyric-romanizer/detector';
75
+ import {
76
+ detectScript,
77
+ isLatinScript,
78
+ requiresExternalRomanization,
79
+ NON_LATIN_SCRIPT_RE,
80
+ } from 'lyric-romanizer/detector';
56
81
  ```
57
82
 
58
83
  ### Types
@@ -70,16 +95,26 @@ interface Romanizer {
70
95
  romanizeLines(lines: readonly string[], options?: RomanizeOptions): Promise<RomanizeResult>;
71
96
  }
72
97
 
73
- type RomanizeOptions = { script?: ScriptType };
74
- type RomanizeResult = { script: ScriptType; lines: string[] };
75
- type RomanizerOptions = { japaneseDictPath?: string };
76
- ```
98
+ // `dialect` is only honored for 'chinese'; every other script ignores it.
99
+ type RomanizeOptions = { script?: ScriptType; dialect?: 'mandarin' | 'cantonese' };
100
+
101
+ // `fallbacks` is aligned with `lines`: true where the engine failed and the
102
+ // line was universally transliterated as a last resort.
103
+ type RomanizeResult = { script: ScriptType; lines: string[]; fallbacks?: boolean[] };
77
104
 
78
- ### Functions
105
+ // An engine adapter: romanizes one line of its script. Throwing (or
106
+ // rejecting) triggers the universal transliteration fallback.
107
+ type RomanizeEngine = (line: string, context: { dialect: 'mandarin' | 'cantonese' }) => string | Promise<string>;
79
108
 
80
- #### `createRomanizer(options?)`
109
+ type RomanizerOptions = {
110
+ japaneseDictPath?: string;
111
+ engines?: Partial<Record<ScriptType, RomanizeEngine>>;
112
+ };
113
+ ```
81
114
 
82
- Factory that returns a `Romanizer` instance. The Kuroshiro engine (Japanese) is lazily initialized on first use and cached.
115
+ ### `createRomanizer(options?)`
116
+
117
+ Factory that returns a `Romanizer` instance. Every engine is lazily loaded on first use and cached — a failed load retries on the next call.
83
118
 
84
119
  ```ts
85
120
  const romanizer = createRomanizer();
@@ -90,7 +125,27 @@ const romanizer = createRomanizer({
90
125
  });
91
126
  ```
92
127
 
93
- #### `detectScript(lines)`
128
+ #### Engine adapters
129
+
130
+ `options.engines` overrides the built-in engine for a script — or plugs an engine into a script that has none built in (`arabic`, `hebrew`, `malayalam`, `bengali`, `other`), so `romanizeLines` can handle every script through one interface. By default the library performs **zero network I/O**; plugging a remote adapter is an explicit caller decision.
131
+
132
+ ```ts
133
+ const romanizer = createRomanizer({
134
+ engines: {
135
+ // Bring your own external romanization for scripts without a local engine.
136
+ arabic: async (line) => myTransliterationApi(line),
137
+ // Or replace a built-in engine (e.g. with a fake in tests).
138
+ korean: (line) => myKoreanRomanizer(line),
139
+ },
140
+ });
141
+
142
+ await romanizer.romanizeLines(['مرحبا']);
143
+ // { script: 'arabic', lines: [...], fallbacks: [false] } — no longer throws
144
+ ```
145
+
146
+ Scripts without an engine — built-in or injected — still throw `UnsupportedRomanizationError`.
147
+
148
+ ### `detectScript(lines)`
94
149
 
95
150
  Detects the dominant script in the given text lines. Checks for Japanese kana first (definitive), then scores all other scripts by character count.
96
151
 
@@ -102,7 +157,7 @@ detectScript(['Hello world']); // 'latin'
102
157
  detectScript(['123 ???']); // 'other'
103
158
  ```
104
159
 
105
- #### `isLatinScript(lines)`
160
+ ### `isLatinScript(lines)`
106
161
 
107
162
  Fast check — returns `true` if the text contains only Latin letters (no CJK, Cyrillic, Indic, etc.). Useful for skipping romanization entirely.
108
163
 
@@ -112,9 +167,9 @@ isLatinScript(['안녕하세요']); // false
112
167
  isLatinScript(['♪♪♪']); // false (no letters)
113
168
  ```
114
169
 
115
- #### `requiresExternalRomanization(script)`
170
+ ### `requiresExternalRomanization(script)`
116
171
 
117
- Returns `true` for scripts that cannot be romanized locally and require an external API.
172
+ Returns `true` for scripts that have no built-in engine and require an external API. Importable from the lightweight `lyric-romanizer/detector` subpath, so answering "should I branch to an external service?" costs zero engine payload.
118
173
 
119
174
  ```ts
120
175
  requiresExternalRomanization('chinese'); // false
@@ -122,17 +177,20 @@ requiresExternalRomanization('arabic'); // true
122
177
  requiresExternalRomanization('malayalam'); // true
123
178
  ```
124
179
 
125
- ### `Romanizer` Interface
180
+ ### `romanizer.romanizeLine(line, options?)`
126
181
 
127
- #### `romanizer.romanizeLine(line, options?)`
182
+ Romanizes a single line. If `script` is omitted, it is auto-detected **per line** via `detectScript` — looping over `romanizeLine` may route each line to a different engine, unlike `romanizeLines`, which pins one script for the whole array. Returns the original line unchanged for Latin text or non-letter content.
128
183
 
129
- Romanizes a single line. If `script` is omitted, it is auto-detected via `detectScript`. Returns the original line unchanged for Latin text or non-letter content.
184
+ For Chinese text, the `dialect` option controls the romanization system: `'mandarin'` (default) uses Pinyin, `'cantonese'` uses [Jyutping](https://github.com/CanCLID/to-jyutping). Other scripts ignore `dialect`.
130
185
 
131
- **Throws** `UnsupportedRomanizationError` for external scripts.
186
+ **Throws** `UnsupportedRomanizationError` for scripts without an engine (built-in or injected).
132
187
 
133
188
  ```ts
134
189
  await romanizer.romanizeLine('你好世界');
135
- // 'nǐ hǎo shì jiè'
190
+ // 'nǐ hǎo shì jiè' (default: Mandarin/Pinyin)
191
+
192
+ await romanizer.romanizeLine('你好', { dialect: 'cantonese' });
193
+ // 'nei5 hou2' (Jyutping)
136
194
 
137
195
  await romanizer.romanizeLine('Привет мир');
138
196
  // 'Privet mir'
@@ -144,21 +202,21 @@ await romanizer.romanizeLine('مرحبا');
144
202
  // throws UnsupportedRomanizationError { script: 'arabic' }
145
203
  ```
146
204
 
147
- #### `romanizer.romanizeLines(lines, options?)`
205
+ ### `romanizer.romanizeLines(lines, options?)`
148
206
 
149
- Romanizes multiple lines in parallel. Returns the detected script and romanized lines.
207
+ Romanizes multiple lines in parallel. Detects the dominant script **once across all lines** and pins it for every line (see [Mixed-script arrays](#mixed-script-arrays)). Returns the script, the romanized lines, and per-line `fallbacks` flags — `true` where the engine failed and the line was universally transliterated as a last resort.
150
208
 
151
209
  ```ts
152
- const { script, lines } = await romanizer.romanizeLines([
210
+ const { script, lines, fallbacks } = await romanizer.romanizeLines([
153
211
  'สวัสดี',
154
212
  'ชาวโลก',
155
213
  ]);
156
- // { script: 'thai', lines: ['sawatdi', 'chaolok'] }
214
+ // { script: 'thai', lines: ['swasdi', 'chaolok'], fallbacks: [false, false] }
157
215
  ```
158
216
 
159
217
  ### `UnsupportedRomanizationError`
160
218
 
161
- Thrown when attempting to romanize a script that requires an external API. Has a `script` property for programmatic handling.
219
+ Thrown when attempting to romanize a script that has no engine — built in or injected via `options.engines`. Has a `script` property for programmatic handling.
162
220
 
163
221
  ```ts
164
222
  try {
@@ -177,28 +235,62 @@ try {
177
235
 
178
236
  | Script | Engine | Example |
179
237
  |--------|--------|---------|
238
+ | Universal *(fallback)* | [transliteration](https://github.com/nickclaw/transliteration) | `Привет` → `Privet` |
180
239
  | Japanese | [kuroshiro](https://github.com/sglkc/kuroshiro-ts) + [kuromoji](https://github.com/takuyaa/kuromoji.js) | `こんにちは` → `konnichiha` |
181
- | Chinese | [pinyin-pro](https://github.com/zh-lx/pinyin-pro) | `你好` → `nǐ hǎo` |
240
+ | Mandarin | [pinyin-pro](https://github.com/zh-lx/pinyin-pro) | `你好` → `nǐ hǎo` |
241
+ | Cantonese | [to-jyutping](https://github.com/CanCLID/to-jyutping) | `佢冇` → `keoi5 mou5` |
182
242
  | Korean | [@romanize/korean](https://github.com/kntng/romanize) | `안녕` → `annyeong` |
183
243
  | Cyrillic | [cyrillic-to-translit-js](https://github.com/greybax/CyrillicToTranslitJS) | `Привет` → `Privet` |
184
- | Devanagari | [sanscript](https://github.com/indic-transliteration/sanscript.js) | `नमस्ते` → `namaste` |
185
- | Gujarati | sanscript | `નમસ્તે` → `namaste` |
186
- | Gurmukhi | sanscript | `ਨਮਸਤੇ` → `namaste` |
187
- | Telugu | sanscript | `నమస్తే` → `namaste` |
188
- | Kannada | sanscript | `ನಮಸ್ತೆ` → `namaste` |
189
- | Odia | sanscript | `ନମସ୍ତେ` → `namaste` |
244
+ | Devanagari | [sanscript](https://github.com/indic-transliteration/sanscript) | `नमस्ते` → `namaste` |
245
+ | Gujarati | [sanscript](https://github.com/indic-transliteration/sanscript) | `નમસ્તે` → `namaste` |
246
+ | Gurmukhi | [sanscript](https://github.com/indic-transliteration/sanscript) | `ਨਮਸਤੇ` → `namasate` |
247
+ | Telugu | [sanscript](https://github.com/indic-transliteration/sanscript) | `నమస్తే` → `namaste` |
248
+ | Kannada | [sanscript](https://github.com/indic-transliteration/sanscript) | `ನಮಸ್ತೇ` → `namaste` |
249
+ | Odia | [sanscript](https://github.com/indic-transliteration/sanscript) | `ନମସ୍ତେ` → `namaste` |
190
250
  | Tamil | [tamil-romanizer](https://github.com/haroldalan/tamil-romanizer) | `வணக்கம்` → `vanakkam` |
191
- | Thai | [@dehoist/romanize-thai](https://github.com/Dehoist/Open-Source) | `สวัสดี` → `sawatdi` |
192
- | Latin | *(no-op)* | `Hello` → `Hello` |
251
+ | Thai | [@dehoist/romanize-thai](https://github.com/Dehoist/Open-Source) | `สวัสดี` → `swasdi` |
193
252
 
194
253
  ### External (requires API)
195
254
 
196
- `malayalam`, `bengali`, `arabic`, `hebrew`, `other` — use `requiresExternalRomanization()` to detect these and branch to your preferred API.
255
+ | Script | Method |
256
+ |--------|--------|
257
+ | Malayalam | Google Translate `dt=rm` |
258
+ | Bengali | Google Translate `dt=rm` |
259
+ | Arabic | Google Translate `dt=rm` |
260
+ | Hebrew | Google Translate `dt=rm` |
261
+ | Other | Google Translate `dt=rm` |
262
+
263
+ Use `requiresExternalRomanization()` to detect these and branch to your preferred API — or plug the API in once as an [engine adapter](#engine-adapters) and let `romanizeLines` handle every script.
264
+
265
+ ## Script-Specific Notes
266
+
267
+ ### Mixed-script arrays
268
+
269
+ `romanizeLines` pins the **dominant** script of the whole array to every line. This is deliberate: a kanji-only line inside a Japanese song is indistinguishable from Chinese on its own (kanji and hanzi share the same Unicode block), so only whole-array context routes it to the right engine. Consequences worth knowing:
197
270
 
198
- ## Cyrillic Detection
271
+ - Any kana anywhere in the array pins the whole array to `japanese` — kana is definitive proof.
272
+ - A line of a *different* non-Latin script inside the array is still fed to the pinned engine.
273
+ - Pure-Latin lines (an English chorus inside a CJK song) are **returned unchanged** instead of being fed to the pinned engine.
274
+ - Per-line `fallbacks` flags report when an engine failed and a line degraded to universal transliteration.
275
+
276
+ If you need true per-line engine routing, call `romanizeLine` in a loop — it detects per line.
277
+
278
+ ### Cyrillic Detection
199
279
 
200
280
  Cyrillic auto-detects Ukrainian-specific characters (`і`, `ї`, `є`, `ґ`) and applies the Ukrainian transliteration preset. All other Cyrillic text defaults to Russian.
201
281
 
282
+ ### Cantonese Support
283
+
284
+ Chinese text defaults to Mandarin (Pinyin). Pass `dialect: 'cantonese'` in `RomanizeOptions` to romanize Chinese text to [Jyutping](https://github.com/CanCLID/to-jyutping) instead.
285
+
286
+ ```ts
287
+ const { lines } = await romanizer.romanizeLines(['你好世界', '食飯'], {
288
+ script: 'chinese',
289
+ dialect: 'cantonese',
290
+ });
291
+ // ['nei5 hou2 sai3 gaai3', 'sik6 faan6']
292
+ ```
293
+
202
294
  ## License
203
295
 
204
296
  MIT
@@ -1,5 +1,79 @@
1
1
  import type { ScriptType } from './types.js';
2
+ declare const SCRIPT_METADATA: {
3
+ readonly japanese: {
4
+ readonly ranges: readonly [readonly [12352, 12543]];
5
+ readonly definitive: true;
6
+ };
7
+ readonly chinese: {
8
+ readonly ranges: readonly [readonly [19968, 40959]];
9
+ };
10
+ readonly korean: {
11
+ readonly ranges: readonly [readonly [44032, 55215]];
12
+ };
13
+ readonly cyrillic: {
14
+ readonly ranges: readonly [readonly [1024, 1279]];
15
+ };
16
+ readonly devanagari: {
17
+ readonly ranges: readonly [readonly [2304, 2431]];
18
+ };
19
+ readonly gujarati: {
20
+ readonly ranges: readonly [readonly [2688, 2815]];
21
+ };
22
+ readonly gurmukhi: {
23
+ readonly ranges: readonly [readonly [2560, 2687]];
24
+ };
25
+ readonly telugu: {
26
+ readonly ranges: readonly [readonly [3072, 3199]];
27
+ };
28
+ readonly kannada: {
29
+ readonly ranges: readonly [readonly [3200, 3327]];
30
+ };
31
+ readonly odia: {
32
+ readonly ranges: readonly [readonly [2816, 2943]];
33
+ };
34
+ readonly tamil: {
35
+ readonly ranges: readonly [readonly [2944, 3071]];
36
+ };
37
+ readonly malayalam: {
38
+ readonly ranges: readonly [readonly [3328, 3455]];
39
+ readonly external: true;
40
+ };
41
+ readonly bengali: {
42
+ readonly ranges: readonly [readonly [2432, 2559]];
43
+ readonly external: true;
44
+ };
45
+ readonly arabic: {
46
+ readonly ranges: readonly [readonly [1536, 1791]];
47
+ readonly external: true;
48
+ };
49
+ readonly hebrew: {
50
+ readonly ranges: readonly [readonly [1424, 1535]];
51
+ readonly external: true;
52
+ };
53
+ readonly thai: {
54
+ readonly ranges: readonly [readonly [3584, 3711]];
55
+ };
56
+ readonly latin: {
57
+ readonly ranges: readonly [];
58
+ };
59
+ readonly other: {
60
+ readonly ranges: readonly [];
61
+ readonly external: true;
62
+ };
63
+ };
64
+ /**
65
+ * Scripts classified as external in SCRIPT_METADATA. The engine table in
66
+ * romanizer.ts excludes exactly these members, so reclassifying a script here
67
+ * surfaces as a compile error there.
68
+ */
69
+ export type ExternalScript = {
70
+ [K in ScriptType]: (typeof SCRIPT_METADATA)[K] extends {
71
+ external: true;
72
+ } ? K : never;
73
+ }[ScriptType];
2
74
  export declare const NON_LATIN_SCRIPT_RE: RegExp;
3
75
  export declare function isLatinScript(lines: readonly string[]): boolean;
4
76
  export declare function detectScript(lines: readonly string[]): ScriptType;
77
+ export declare function requiresExternalRomanization(script: ScriptType): boolean;
78
+ export {};
5
79
  //# sourceMappingURL=detector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwB7C,eAAO,MAAM,mBAAmB,QAC8E,CAAC;AAE/G,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAG/D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAcjE"}
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwB7C,QAAA,MAAM,eAAe;;iBAGP,MAAM;iBAAsB,UAAU;;;iBACvC,MAAM;;;iBACP,MAAM;;;iBACJ,MAAM;;;iBACJ,MAAM;;;iBACR,MAAM;;;iBACN,MAAM;;;iBACR,MAAM;;;iBACL,MAAM;;;iBACT,MAAM;;;iBACL,MAAM;;;iBACF,MAAM;iBAAsB,QAAQ;;;iBACtC,MAAM;iBAAsB,QAAQ;;;iBACrC,MAAM;iBAAsB,QAAQ;;;iBACpC,MAAM;iBAAsB,QAAQ;;;iBACtC,MAAM;;;iBACL,MAAM;;;iBACN,MAAM;iBAAM,QAAQ;;CACoB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;KAC1B,CAAC,IAAI,UAAU,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK;CACtF,CAAC,UAAU,CAAC,CAAC;AAwCd,eAAO,MAAM,mBAAmB,QAE/B,CAAC;AAEF,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAG/D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CAejE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAMxE"}
package/dist/detector.js CHANGED
@@ -1,33 +1,75 @@
1
1
  const LETTER_RE = /\p{L}/u;
2
- const SCRIPT_SCORES = [
3
- ['chinese', /[\u4E00-\u9FFF]/g],
4
- ['korean', /[\uAC00-\uD7AF]/g],
5
- ['cyrillic', /[\u0400-\u04FF]/g],
6
- ['devanagari', /[\u0900-\u097F]/g],
7
- ['gujarati', /[\u0A80-\u0AFF]/g],
8
- ['gurmukhi', /[\u0A00-\u0A7F]/g],
9
- ['telugu', /[\u0C00-\u0C7F]/g],
10
- ['kannada', /[\u0C80-\u0CFF]/g],
11
- ['odia', /[\u0B00-\u0B7F]/g],
12
- ['tamil', /[\u0B80-\u0BFF]/g],
13
- ['malayalam', /[\u0D00-\u0D7F]/g],
14
- ['bengali', /[\u0980-\u09FF]/g],
15
- ['arabic', /[\u0600-\u06FF]/g],
16
- ['hebrew', /[\u0590-\u05FF]/g],
17
- ['thai', /[\u0E00-\u0E7F]/g],
18
- ];
19
- // Keep this range aligned with extension content script behavior for the
20
- // "latin fast path" check used during mode switching.
21
- export const NON_LATIN_SCRIPT_RE = /[\u3040-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF\u0400-\u04FF\u0900-\u0D7F\u0600-\u06FF\u0590-\u05FF\u0E00-\u0E7F]/;
2
+ // Single source of truth for per-script knowledge on the light (engine-free)
3
+ // side of the packaging seam: detection ranges and local/external
4
+ // classification. SCRIPT_SCORES, NON_LATIN_SCRIPT_RE, and
5
+ // requiresExternalRomanization are all derived from this table, and the
6
+ // engine table in romanizer.ts is compile-time checked against the
7
+ // classification, so adding a script here is the single detector-side edit.
8
+ //
9
+ // Entry order is load-bearing: it is the tie-break priority of detectScript —
10
+ // on an equal character count, the earlier entry wins.
11
+ const SCRIPT_METADATA = {
12
+ // Kana is definitive: kanji shares the Han block with chinese, so any kana
13
+ // must decide 'japanese' before Han scoring runs.
14
+ japanese: { ranges: [[0x3040, 0x30ff]], definitive: true },
15
+ chinese: { ranges: [[0x4e00, 0x9fff]] },
16
+ korean: { ranges: [[0xac00, 0xd7af]] },
17
+ cyrillic: { ranges: [[0x0400, 0x04ff]] },
18
+ devanagari: { ranges: [[0x0900, 0x097f]] },
19
+ gujarati: { ranges: [[0x0a80, 0x0aff]] },
20
+ gurmukhi: { ranges: [[0x0a00, 0x0a7f]] },
21
+ telugu: { ranges: [[0x0c00, 0x0c7f]] },
22
+ kannada: { ranges: [[0x0c80, 0x0cff]] },
23
+ odia: { ranges: [[0x0b00, 0x0b7f]] },
24
+ tamil: { ranges: [[0x0b80, 0x0bff]] },
25
+ malayalam: { ranges: [[0x0d00, 0x0d7f]], external: true },
26
+ bengali: { ranges: [[0x0980, 0x09ff]], external: true },
27
+ arabic: { ranges: [[0x0600, 0x06ff]], external: true },
28
+ hebrew: { ranges: [[0x0590, 0x05ff]], external: true },
29
+ thai: { ranges: [[0x0e00, 0x0e7f]] },
30
+ latin: { ranges: [] },
31
+ other: { ranges: [], external: true },
32
+ };
33
+ function escapeCodePoint(codePoint) {
34
+ return `\\u${codePoint.toString(16).toUpperCase().padStart(4, '0')}`;
35
+ }
36
+ function characterClass(ranges) {
37
+ return ranges
38
+ .map(([start, end]) => (start === end ? escapeCodePoint(start) : `${escapeCodePoint(start)}-${escapeCodePoint(end)}`))
39
+ .join('');
40
+ }
41
+ function mergeRanges(ranges) {
42
+ const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
43
+ const merged = [];
44
+ for (const [start, end] of sorted) {
45
+ const last = merged[merged.length - 1];
46
+ if (last && start <= last[1] + 1) {
47
+ last[1] = Math.max(last[1], end);
48
+ }
49
+ else {
50
+ merged.push([start, end]);
51
+ }
52
+ }
53
+ return merged;
54
+ }
55
+ const METADATA_ENTRIES = Object.entries(SCRIPT_METADATA);
56
+ const DEFINITIVE_SCRIPTS = METADATA_ENTRIES.filter(([, meta]) => meta.definitive).map(([script, meta]) => [script, new RegExp(`[${characterClass(meta.ranges)}]`)]);
57
+ const SCRIPT_SCORES = METADATA_ENTRIES.filter(([, meta]) => meta.ranges.length > 0 && !meta.definitive).map(([script, meta]) => [script, new RegExp(`[${characterClass(meta.ranges)}]`, 'g')]);
58
+ // Derived union of every detectable range. Also consumed by the extension
59
+ // content script's "latin fast path" during mode switching — deriving it from
60
+ // SCRIPT_METADATA keeps it aligned with detection structurally instead of by
61
+ // comment.
62
+ export const NON_LATIN_SCRIPT_RE = new RegExp(`[${characterClass(mergeRanges(METADATA_ENTRIES.flatMap(([, meta]) => meta.ranges)))}]`);
22
63
  export function isLatinScript(lines) {
23
64
  const text = lines.join('');
24
65
  return !NON_LATIN_SCRIPT_RE.test(text) && LETTER_RE.test(text);
25
66
  }
26
67
  export function detectScript(lines) {
27
68
  const text = lines.join('');
28
- // Japanese: any kana is definitive and must be checked before CJK scoring.
29
- if (/[\u3040-\u30FF]/.test(text))
30
- return 'japanese';
69
+ for (const [script, pattern] of DEFINITIVE_SCRIPTS) {
70
+ if (pattern.test(text))
71
+ return script;
72
+ }
31
73
  let best = ['other', 0];
32
74
  for (const [script, pattern] of SCRIPT_SCORES) {
33
75
  const score = (text.match(pattern) ?? []).length;
@@ -38,4 +80,12 @@ export function detectScript(lines) {
38
80
  return best[0];
39
81
  return LETTER_RE.test(text) ? 'latin' : 'other';
40
82
  }
83
+ export function requiresExternalRomanization(script) {
84
+ // Own-property check so an out-of-contract string from untyped callers
85
+ // (including inherited names like 'toString') returns false rather than
86
+ // resolving up the prototype chain.
87
+ if (!Object.hasOwn(SCRIPT_METADATA, script))
88
+ return false;
89
+ return SCRIPT_METADATA[script].external === true;
90
+ }
41
91
  //# sourceMappingURL=detector.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,QAAQ,CAAC;AAE3B,MAAM,aAAa,GAAyC;IAC1D,CAAC,SAAS,EAAE,kBAAkB,CAAC;IAC/B,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAC9B,CAAC,UAAU,EAAE,kBAAkB,CAAC;IAChC,CAAC,YAAY,EAAE,kBAAkB,CAAC;IAClC,CAAC,UAAU,EAAE,kBAAkB,CAAC;IAChC,CAAC,UAAU,EAAE,kBAAkB,CAAC;IAChC,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAC9B,CAAC,SAAS,EAAE,kBAAkB,CAAC;IAC/B,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAC5B,CAAC,OAAO,EAAE,kBAAkB,CAAC;IAC7B,CAAC,WAAW,EAAE,kBAAkB,CAAC;IACjC,CAAC,SAAS,EAAE,kBAAkB,CAAC;IAC/B,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAC9B,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAC9B,CAAC,MAAM,EAAE,kBAAkB,CAAC;CAC7B,CAAC;AAEF,yEAAyE;AACzE,sDAAsD;AACtD,MAAM,CAAC,MAAM,mBAAmB,GAC9B,4GAA4G,CAAC;AAE/G,MAAM,UAAU,aAAa,CAAC,KAAwB;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5B,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE5B,2EAA2E;IAC3E,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,UAAU,CAAC;IAEpD,IAAI,IAAI,GAAkC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACvD,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,aAAa,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACjD,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;YAAE,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IAChC,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAClD,CAAC"}
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAEA,MAAM,SAAS,GAAG,QAAQ,CAAC;AAa3B,6EAA6E;AAC7E,kEAAkE;AAClE,0DAA0D;AAC1D,wEAAwE;AACxE,mEAAmE;AACnE,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,uDAAuD;AACvD,MAAM,eAAe,GAAG;IACtB,2EAA2E;IAC3E,kDAAkD;IAClD,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE;IAC1D,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACvC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACtC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACxC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IAC1C,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACxC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACxC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACtC,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACvC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACpC,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACrC,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACzD,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACvD,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtD,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtD,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE;IACpC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;IACrB,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;CACY,CAAC;AAWpD,SAAS,eAAe,CAAC,SAAiB;IACxC,OAAO,MAAM,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,SAAS,cAAc,CAAC,MAA4B;IAClD,OAAO,MAAM;SACV,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SACrH,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,MAA4B;IAC/C,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvD,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,MAAM,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,gBAAgB,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,CAAoC,CAAC;AAE5F,MAAM,kBAAkB,GAAyC,gBAAgB,CAAC,MAAM,CACtF,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAC9B,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAU,CAAC,CAAC;AAE7F,MAAM,aAAa,GAAyC,gBAAgB,CAAC,MAAM,CACjF,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CACzD,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAU,CAAC,CAAC;AAElG,0EAA0E;AAC1E,8EAA8E;AAC9E,6EAA6E;AAC7E,WAAW;AACX,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,MAAM,CAC3C,IAAI,cAAc,CAAC,WAAW,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CACxF,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,KAAwB;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5B,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAwB;IACnD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE5B,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,kBAAkB,EAAE,CAAC;QACnD,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO,MAAM,CAAC;IACxC,CAAC;IAED,IAAI,IAAI,GAAkC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IACvD,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,aAAa,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACjD,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC;YAAE,IAAI,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IAChC,OAAO,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,MAAkB;IAC7D,uEAAuE;IACvE,wEAAwE;IACxE,oCAAoC;IACpC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAQ,eAA8C,CAAC,MAAM,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC;AACnF,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { detectScript, isLatinScript, NON_LATIN_SCRIPT_RE } from './detector.js';
2
- export { createRomanizer, requiresExternalRomanization } from './romanizer.js';
3
- export { UnsupportedRomanizationError, type RomanizeOptions, type RomanizeResult, type Romanizer, type RomanizerOptions, type ScriptType, } from './types.js';
1
+ export { detectScript, isLatinScript, requiresExternalRomanization, NON_LATIN_SCRIPT_RE, type ExternalScript, } from './detector.js';
2
+ export { createRomanizer } from './romanizer.js';
3
+ export { UnsupportedRomanizationError, type RomanizeEngine, type RomanizeEngineContext, type RomanizeOptions, type RomanizeResult, type Romanizer, type RomanizerOptions, type ScriptType, } from './types.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EACL,4BAA4B,EAC5B,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,4BAA4B,EAC5B,mBAAmB,EACnB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,4BAA4B,EAC5B,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { detectScript, isLatinScript, NON_LATIN_SCRIPT_RE } from './detector.js';
2
- export { createRomanizer, requiresExternalRomanization } from './romanizer.js';
1
+ export { detectScript, isLatinScript, requiresExternalRomanization, NON_LATIN_SCRIPT_RE, } from './detector.js';
2
+ export { createRomanizer } from './romanizer.js';
3
3
  export { UnsupportedRomanizationError, } from './types.js';
4
4
  //# 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":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,EACL,4BAA4B,GAM7B,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,4BAA4B,EAC5B,mBAAmB,GAEpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,4BAA4B,GAQ7B,MAAM,YAAY,CAAC"}
@@ -1,4 +1,9 @@
1
- import type { Romanizer, RomanizerOptions, ScriptType } from './types.js';
2
- export declare function requiresExternalRomanization(script: ScriptType): boolean;
1
+ import type { Romanizer, RomanizerOptions } from './types.js';
2
+ /**
3
+ * Ukrainian-specific characters pick the Ukrainian preset; all other Cyrillic
4
+ * romanizes as Russian. Applied per line, so a mixed-language song switches
5
+ * preset line by line.
6
+ */
7
+ export declare function selectCyrillicPreset(line: string): 'ru' | 'uk';
3
8
  export declare function createRomanizer(options?: RomanizerOptions): Romanizer;
4
9
  //# sourceMappingURL=romanizer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"romanizer.d.ts","sourceRoot":"","sources":["../src/romanizer.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAGV,SAAS,EACT,gBAAgB,EAChB,UAAU,EACX,MAAM,YAAY,CAAC;AAwBpB,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAExE;AAsFD,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAErE"}
1
+ {"version":3,"file":"romanizer.d.ts","sourceRoot":"","sources":["../src/romanizer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAKV,SAAS,EACT,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AA4EpB;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE9D;AA+ID,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAErE"}