langtell 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -13
- package/dist/chrome-ai.d.ts +1 -1
- package/dist/chunk-2Z7REIOE.js +41 -0
- package/dist/chunk-2Z7REIOE.js.map +1 -0
- package/dist/{chunk-7TDGJOIJ.js → chunk-7N545CET.js} +3 -3
- package/dist/{chunk-7TDGJOIJ.js.map → chunk-7N545CET.js.map} +1 -1
- package/dist/{chunk-6PWEE3SR.js → chunk-LUSZIBTZ.js} +19 -9
- package/dist/chunk-LUSZIBTZ.js.map +1 -0
- package/dist/{chunk-BL627TWI.js → chunk-SOTZVW3I.js} +47 -20
- package/dist/chunk-SOTZVW3I.js.map +1 -0
- package/dist/classify.d.ts +87 -4
- package/dist/classify.js +2 -1
- package/dist/cyrillic.d.ts +2 -29
- package/dist/cyrillic.js +27 -2
- package/dist/cyrillic.js.map +1 -1
- package/dist/franc.d.ts +1 -1
- package/dist/franc.js +2 -1
- package/dist/franc.js.map +1 -1
- package/dist/fuse.d.ts +1 -1
- package/dist/fuse.js +3 -2
- package/dist/headers.d.ts +1 -1
- package/dist/html.d.ts +1 -1
- package/dist/index.d.ts +37 -3
- package/dist/index.js +43 -8
- package/dist/index.js.map +1 -1
- package/dist/profiles.d.ts +5 -2
- package/dist/profiles.js +431 -25
- package/dist/profiles.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.js +3 -2
- package/dist/{types-BIXrkuAr.d.ts → types-DkB4436E.d.ts} +27 -4
- package/package.json +1 -1
- package/dist/chunk-6PWEE3SR.js.map +0 -1
- package/dist/chunk-BL627TWI.js.map +0 -1
package/dist/cyrillic.d.ts
CHANGED
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `langtell/cyrillic` — a cheap, roster-free Cyrillic language fast-path.
|
|
3
|
-
*
|
|
4
|
-
* Where {@link classifyBySnippet} (`langtell/classify`) scores a snippet
|
|
5
|
-
* *relative to a candidate roster you pass in*, this is the opposite trade: a
|
|
6
|
-
* fixed, zero-config discriminator for the four Cyrillic languages langtell
|
|
7
|
-
* profiles — Ukrainian, Russian, Belarusian, Bulgarian — decided purely by
|
|
8
|
-
* letters distinctive to each, with no profiles, no tokenization, and no franc.
|
|
9
|
-
* Reach for it when you just need "is this Russian / is this Ukrainian?" on a
|
|
10
|
-
* hot path and don't want to assemble a candidate set.
|
|
11
|
-
*
|
|
12
|
-
* Each language carries letters the others (mostly) don't:
|
|
13
|
-
* Ukrainian — і ї є ґ
|
|
14
|
-
* Russian — ы ё (ъ э are shared with bg/be and handled separately)
|
|
15
|
-
* Belarusian — ў (uniquely Belarusian)
|
|
16
|
-
* Bulgarian — ъ used as a vowel in nearly every word
|
|
17
|
-
*
|
|
18
|
-
* The shared letters `ъ` and `э` need care: `подъезд` is Russian, `съм
|
|
19
|
-
* българин` is Bulgarian — both contain `ъ`. We disambiguate by density and
|
|
20
|
-
* length: a single `ъ` in a short snippet leans RU; multiple `ъ` in longer text
|
|
21
|
-
* leans BG. A lone `э` with no other distinctives stays `"unknown"` rather than
|
|
22
|
-
* silently guessing (Russian uses it in loanwords/`Это`, Belarusian in
|
|
23
|
-
* `гэта`/`сэрца`).
|
|
24
|
-
*
|
|
25
|
-
* Zero-dependency and side-effect-free. The cheap heuristic stays cheap; if a
|
|
26
|
-
* use case needs more than letter signals can give, escalate to
|
|
27
|
-
* {@link classifyBySnippet} or a franc-backed source.
|
|
28
|
-
*/
|
|
29
1
|
/** The four Cyrillic languages this fast-path tells apart, plus the `"unknown"`
|
|
30
2
|
* sentinel when letter signals are insufficient. */
|
|
31
3
|
type CyrillicLanguage = "uk" | "ru" | "be" | "bg" | "unknown";
|
|
@@ -40,7 +12,8 @@ interface CyrillicVerdict {
|
|
|
40
12
|
/**
|
|
41
13
|
* Identify the Cyrillic language of `text` by distinctive letters, returning the
|
|
42
14
|
* chosen language and the uk/ru tallies behind it. `"unknown"` when there is no
|
|
43
|
-
* Cyrillic evidence, on a uk/ru tie,
|
|
15
|
+
* Cyrillic evidence, on a uk/ru tie, when only an ambiguous `э` is present, or
|
|
16
|
+
* when the text contradicts the call — see {@link FOREIGN}.
|
|
44
17
|
*/
|
|
45
18
|
declare function detectCyrillicLanguage(text: string): CyrillicVerdict;
|
|
46
19
|
/** Convenience predicate: `true` iff {@link detectCyrillicLanguage} calls `text`
|
package/dist/cyrillic.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import { stripQuoted } from './chunk-2Z7REIOE.js';
|
|
2
|
+
|
|
1
3
|
// src/cyrillic.ts
|
|
2
4
|
var UK_DISTINCTIVE = /[іїєґ]/gi;
|
|
3
5
|
var RU_DISTINCTIVE = /[ыё]/gi;
|
|
4
6
|
var BE_DISTINCTIVE = /ў/gi;
|
|
5
7
|
var HARD_SIGN = /ъ/gi;
|
|
6
8
|
var E_OBOROT = /э/gi;
|
|
9
|
+
var SIBLING_DISTINCTIVE = /[ђћџѓќѕљњјәғқңөұүһ]/gi;
|
|
7
10
|
var CYRILLIC = /[\u0400-\u04FF]/g;
|
|
11
|
+
var FOREIGN = {
|
|
12
|
+
uk: /[ыёъэў]/gi,
|
|
13
|
+
ru: /[іїєґў]/gi,
|
|
14
|
+
be: /[ищъїєґ]/gi,
|
|
15
|
+
bg: /[ыёэіїєґў]/gi
|
|
16
|
+
};
|
|
17
|
+
var MAX_FOREIGN_SHARE = 0.02;
|
|
8
18
|
var MIN_CYRILLIC_FOR_FALLBACK = 10;
|
|
9
19
|
var MIN_LEN_FOR_BG = 10;
|
|
10
20
|
function count(text, re) {
|
|
@@ -17,14 +27,29 @@ function countSignals(text) {
|
|
|
17
27
|
beScore: count(text, BE_DISTINCTIVE),
|
|
18
28
|
hardSigns: count(text, HARD_SIGN),
|
|
19
29
|
eOborot: count(text, E_OBOROT),
|
|
20
|
-
cyrillicCount: count(text, CYRILLIC)
|
|
30
|
+
cyrillicCount: count(text, CYRILLIC),
|
|
31
|
+
siblingDistinctive: count(text, SIBLING_DISTINCTIVE)
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
34
|
function detectCyrillicLanguage(text) {
|
|
24
|
-
const
|
|
35
|
+
const verdict = decide(text);
|
|
36
|
+
if (verdict.language === "unknown" || !contradicted(text, verdict.language)) return verdict;
|
|
37
|
+
return { ...verdict, language: "unknown" };
|
|
38
|
+
}
|
|
39
|
+
function contradicted(text, language) {
|
|
40
|
+
const own = stripQuoted(text);
|
|
41
|
+
const cyrillic = count(own, CYRILLIC);
|
|
42
|
+
if (cyrillic === 0) return false;
|
|
43
|
+
return count(own, FOREIGN[language]) / cyrillic >= MAX_FOREIGN_SHARE;
|
|
44
|
+
}
|
|
45
|
+
function decide(text) {
|
|
46
|
+
const { ukScore, ruDistinctive, beScore, hardSigns, eOborot, cyrillicCount, siblingDistinctive } = countSignals(text);
|
|
25
47
|
if (beScore > 0) {
|
|
26
48
|
return { language: "be", ukScore, ruScore: ruDistinctive };
|
|
27
49
|
}
|
|
50
|
+
if (siblingDistinctive > 0) {
|
|
51
|
+
return { language: "unknown", ukScore, ruScore: ruDistinctive };
|
|
52
|
+
}
|
|
28
53
|
if (ukScore > 0 && ruDistinctive > 0) {
|
|
29
54
|
if (ukScore === ruDistinctive) {
|
|
30
55
|
return { language: "unknown", ukScore, ruScore: ruDistinctive };
|
package/dist/cyrillic.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cyrillic.ts"],"names":[],"mappings":"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","file":"cyrillic.js","sourcesContent":["/**\n * `langtell/cyrillic` — a cheap, roster-free Cyrillic language fast-path.\n *\n * Where {@link classifyBySnippet} (`langtell/classify`) scores a snippet\n * *relative to a candidate roster you pass in*, this is the opposite trade: a\n * fixed, zero-config discriminator for the four Cyrillic languages langtell\n * profiles — Ukrainian, Russian, Belarusian, Bulgarian — decided purely by\n * letters distinctive to each, with no profiles, no tokenization, and no franc.\n * Reach for it when you just need \"is this Russian / is this Ukrainian?\" on a\n * hot path and don't want to assemble a candidate set.\n *\n * Each language carries letters the others (mostly) don't:\n * Ukrainian — і ї є ґ\n * Russian — ы ё (ъ э are shared with bg/be and handled separately)\n * Belarusian — ў (uniquely Belarusian)\n * Bulgarian — ъ used as a vowel in nearly every word\n *\n * The shared letters `ъ` and `э` need care: `подъезд` is Russian, `съм\n * българин` is Bulgarian — both contain `ъ`. We disambiguate by density and\n * length: a single `ъ` in a short snippet leans RU; multiple `ъ` in longer text\n * leans BG. A lone `э` with no other distinctives stays `\"unknown\"` rather than\n * silently guessing (Russian uses it in loanwords/`Это`, Belarusian in\n * `гэта`/`сэрца`).\n *\n * Zero-dependency and side-effect-free. The cheap heuristic stays cheap; if a\n * use case needs more than letter signals can give, escalate to\n * {@link classifyBySnippet} or a franc-backed source.\n */\n\n/** The four Cyrillic languages this fast-path tells apart, plus the `\"unknown\"`\n * sentinel when letter signals are insufficient. */\nexport type CyrillicLanguage = \"uk\" | \"ru\" | \"be\" | \"bg\" | \"unknown\";\n\nconst UK_DISTINCTIVE = /[іїєґ]/gi;\nconst RU_DISTINCTIVE = /[ыё]/gi;\nconst BE_DISTINCTIVE = /ў/gi;\nconst HARD_SIGN = /ъ/gi;\nconst E_OBOROT = /э/gi;\n// U+0400–U+04FF is the Cyrillic Unicode block; written as explicit \\u escapes\n// so the range bounds are unambiguous (regexp/no-obscure-range).\nconst CYRILLIC = /[\\u0400-\\u04FF]/g;\n\n/** Minimum Cyrillic-letter count before the fallback guesses a language. Below\n * this, a short snippet (`Привет`, `Хочу`) is too ambiguous to act on. */\nconst MIN_CYRILLIC_FOR_FALLBACK = 10;\n/** Minimum text length before `ъ`-density is read as Bulgarian. Single-word\n * Russian samples like `подъезд` (7 chars) would otherwise misclassify. */\nconst MIN_LEN_FOR_BG = 10;\n\n/** The verdict from {@link detectCyrillicLanguage}: the chosen language plus the\n * raw distinctive-letter tallies that drove a uk-vs-ru decision (informational;\n * `ruScore` carries the deciding count for the ru/bg fallbacks too). */\nexport interface CyrillicVerdict {\n language: CyrillicLanguage;\n ukScore: number;\n ruScore: number;\n}\n\nfunction count(text: string, re: RegExp): number {\n return (text.match(re) ?? []).length;\n}\n\n/** Raw letter-signal tallies over `text`, one pass per distinctive class.\n * Gathered up front so {@link detectCyrillicLanguage} reads as a pure decision\n * cascade over these counts rather than interleaving counting and branching. */\ninterface Signals {\n ukScore: number;\n ruDistinctive: number;\n beScore: number;\n hardSigns: number;\n eOborot: number;\n cyrillicCount: number;\n}\n\nfunction countSignals(text: string): Signals {\n return {\n ukScore: count(text, UK_DISTINCTIVE),\n ruDistinctive: count(text, RU_DISTINCTIVE),\n beScore: count(text, BE_DISTINCTIVE),\n hardSigns: count(text, HARD_SIGN),\n eOborot: count(text, E_OBOROT),\n cyrillicCount: count(text, CYRILLIC),\n };\n}\n\n/**\n * Identify the Cyrillic language of `text` by distinctive letters, returning the\n * chosen language and the uk/ru tallies behind it. `\"unknown\"` when there is no\n * Cyrillic evidence, on a uk/ru tie, or when only an ambiguous `э` is present.\n */\nexport function detectCyrillicLanguage(text: string): CyrillicVerdict {\n const { ukScore, ruDistinctive, beScore, hardSigns, eOborot, cyrillicCount } = countSignals(text);\n\n // ў is uniquely Belarusian — strongest single signal.\n if (beScore > 0) {\n return { language: \"be\", ukScore, ruScore: ruDistinctive };\n }\n\n // Both UA and RU evidence present — a tie is \"unknown\", not a silent UA call\n // (which would misclassify Belarusian/Bulgarian whenever і or ё balances out).\n if (ukScore > 0 && ruDistinctive > 0) {\n if (ukScore === ruDistinctive) {\n return { language: \"unknown\", ukScore, ruScore: ruDistinctive };\n }\n return {\n language: ukScore > ruDistinctive ? \"uk\" : \"ru\",\n ukScore,\n ruScore: ruDistinctive,\n };\n }\n\n // Distinctive RU letters (ы, ё) with no UA evidence — unambiguously RU.\n if (ruDistinctive > 0) {\n return { language: \"ru\", ukScore, ruScore: ruDistinctive };\n }\n\n // Distinctive UA letters with no RU distinctives — UA.\n if (ukScore > 0) {\n return { language: \"uk\", ukScore, ruScore: 0 };\n }\n\n // Bulgarian: ъ as a vowel — multiple occurrences in non-trivial text, no UA /\n // RU distinctives. Length guard keeps short Russian compounds like `подъезд`\n // from sliding into BG.\n if (hardSigns >= 2 && text.length >= MIN_LEN_FOR_BG) {\n return { language: \"bg\", ukScore: 0, ruScore: hardSigns };\n }\n\n // Russian fallback: substantial Cyrillic with no UA, no BE distinctives, and\n // no э (which signals possible Belarusian). Catches short-but-clear Russian\n // text with none of ы/ё/ъ/э (`Привет, мир`, `Здравствуйте, меня зовут …`).\n if (cyrillicCount >= MIN_CYRILLIC_FOR_FALLBACK && eOborot === 0) {\n return { language: \"ru\", ukScore: 0, ruScore: 0 };\n }\n\n // Lone ъ (no э, no other distinctives) in shorter text — RU compound-word\n // pattern (подъезд, объект, съезд).\n if (hardSigns > 0 && eOborot === 0) {\n return { language: \"ru\", ukScore: 0, ruScore: hardSigns };\n }\n\n return { language: \"unknown\", ukScore: 0, ruScore: 0 };\n}\n\n/** Convenience predicate: `true` iff {@link detectCyrillicLanguage} calls `text`\n * Russian. */\nexport function isRussian(text: string): boolean {\n return detectCyrillicLanguage(text).language === \"ru\";\n}\n\n/** Convenience predicate: `true` iff {@link detectCyrillicLanguage} calls `text`\n * Ukrainian. */\nexport function isUkrainian(text: string): boolean {\n return detectCyrillicLanguage(text).language === \"uk\";\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cyrillic.ts"],"names":[],"mappings":";;;AA+CA,IAAM,cAAA,GAAiB,UAAA;AACvB,IAAM,cAAA,GAAiB,QAAA;AACvB,IAAM,cAAA,GAAiB,KAAA;AACvB,IAAM,SAAA,GAAY,KAAA;AAClB,IAAM,QAAA,GAAW,KAAA;AAOjB,IAAM,mBAAA,GAAsB,uBAAA;AAG5B,IAAM,QAAA,GAAW,kBAAA;AAkBjB,IAAM,OAAA,GAA0E;AAAA,EAC9E,EAAA,EAAI,WAAA;AAAA,EACJ,EAAA,EAAI,WAAA;AAAA,EACJ,EAAA,EAAI,YAAA;AAAA,EACJ,EAAA,EAAI;AACN,CAAA;AAaA,IAAM,iBAAA,GAAoB,IAAA;AAI1B,IAAM,yBAAA,GAA4B,EAAA;AAGlC,IAAM,cAAA,GAAiB,EAAA;AAWvB,SAAS,KAAA,CAAM,MAAc,EAAA,EAAoB;AAC/C,EAAA,OAAA,CAAQ,IAAA,CAAK,KAAA,CAAM,EAAE,CAAA,IAAK,EAAC,EAAG,MAAA;AAChC;AAiBA,SAAS,aAAa,IAAA,EAAuB;AAC3C,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA,CAAM,IAAA,EAAM,cAAc,CAAA;AAAA,IACnC,aAAA,EAAe,KAAA,CAAM,IAAA,EAAM,cAAc,CAAA;AAAA,IACzC,OAAA,EAAS,KAAA,CAAM,IAAA,EAAM,cAAc,CAAA;AAAA,IACnC,SAAA,EAAW,KAAA,CAAM,IAAA,EAAM,SAAS,CAAA;AAAA,IAChC,OAAA,EAAS,KAAA,CAAM,IAAA,EAAM,QAAQ,CAAA;AAAA,IAC7B,aAAA,EAAe,KAAA,CAAM,IAAA,EAAM,QAAQ,CAAA;AAAA,IACnC,kBAAA,EAAoB,KAAA,CAAM,IAAA,EAAM,mBAAmB;AAAA,GACrD;AACF;AAQO,SAAS,uBAAuB,IAAA,EAA+B;AACpE,EAAA,MAAM,OAAA,GAAU,OAAO,IAAI,CAAA;AAC3B,EAAA,IAAI,OAAA,CAAQ,aAAa,SAAA,IAAa,CAAC,aAAa,IAAA,EAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG,OAAO,OAAA;AAIpF,EAAA,OAAO,EAAE,GAAG,OAAA,EAAS,QAAA,EAAU,SAAA,EAAU;AAC3C;AAaA,SAAS,YAAA,CAAa,MAAc,QAAA,EAAyD;AAC3F,EAAA,MAAM,GAAA,GAAM,YAAY,IAAI,CAAA;AAC5B,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,GAAA,EAAK,QAAQ,CAAA;AACpC,EAAA,IAAI,QAAA,KAAa,GAAG,OAAO,KAAA;AAC3B,EAAA,OAAO,MAAM,GAAA,EAAK,OAAA,CAAQ,QAAQ,CAAC,IAAI,QAAA,IAAY,iBAAA;AACrD;AAIA,SAAS,OAAO,IAAA,EAA+B;AAC7C,EAAA,MAAM,EAAE,OAAA,EAAS,aAAA,EAAe,OAAA,EAAS,SAAA,EAAW,SAAS,aAAA,EAAe,kBAAA,EAAmB,GAC7F,YAAA,CAAa,IAAI,CAAA;AAGnB,EAAA,IAAI,UAAU,CAAA,EAAG;AACf,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,SAAS,aAAA,EAAc;AAAA,EAC3D;AAUA,EAAA,IAAI,qBAAqB,CAAA,EAAG;AAC1B,IAAA,OAAO,EAAE,QAAA,EAAU,SAAA,EAAW,OAAA,EAAS,SAAS,aAAA,EAAc;AAAA,EAChE;AAIA,EAAA,IAAI,OAAA,GAAU,CAAA,IAAK,aAAA,GAAgB,CAAA,EAAG;AACpC,IAAA,IAAI,YAAY,aAAA,EAAe;AAC7B,MAAA,OAAO,EAAE,QAAA,EAAU,SAAA,EAAW,OAAA,EAAS,SAAS,aAAA,EAAc;AAAA,IAChE;AACA,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,OAAA,GAAU,aAAA,GAAgB,IAAA,GAAO,IAAA;AAAA,MAC3C,OAAA;AAAA,MACA,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AAGA,EAAA,IAAI,gBAAgB,CAAA,EAAG;AACrB,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,SAAS,aAAA,EAAc;AAAA,EAC3D;AAGA,EAAA,IAAI,UAAU,CAAA,EAAG;AACf,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,SAAS,CAAA,EAAE;AAAA,EAC/C;AAKA,EAAA,IAAI,SAAA,IAAa,CAAA,IAAK,IAAA,CAAK,MAAA,IAAU,cAAA,EAAgB;AACnD,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,CAAA,EAAG,SAAS,SAAA,EAAU;AAAA,EAC1D;AAKA,EAAA,IAAI,aAAA,IAAiB,yBAAA,IAA6B,OAAA,KAAY,CAAA,EAAG;AAC/D,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,CAAA,EAAG,SAAS,CAAA,EAAE;AAAA,EAClD;AAIA,EAAA,IAAI,SAAA,GAAY,CAAA,IAAK,OAAA,KAAY,CAAA,EAAG;AAClC,IAAA,OAAO,EAAE,QAAA,EAAU,IAAA,EAAM,OAAA,EAAS,CAAA,EAAG,SAAS,SAAA,EAAU;AAAA,EAC1D;AAEA,EAAA,OAAO,EAAE,QAAA,EAAU,SAAA,EAAW,OAAA,EAAS,CAAA,EAAG,SAAS,CAAA,EAAE;AACvD;AAIO,SAAS,UAAU,IAAA,EAAuB;AAC/C,EAAA,OAAO,sBAAA,CAAuB,IAAI,CAAA,CAAE,QAAA,KAAa,IAAA;AACnD;AAIO,SAAS,YAAY,IAAA,EAAuB;AACjD,EAAA,OAAO,sBAAA,CAAuB,IAAI,CAAA,CAAE,QAAA,KAAa,IAAA;AACnD","file":"cyrillic.js","sourcesContent":["/**\n * `langtell/cyrillic` — a cheap, roster-free Cyrillic language fast-path.\n *\n * Where {@link classifyBySnippet} (`langtell/classify`) scores a snippet\n * *relative to a candidate roster you pass in*, this is the opposite trade: a\n * fixed, zero-config discriminator for the four Cyrillic languages this fast-path\n * positively identifies — Ukrainian, Russian, Belarusian, Bulgarian — decided\n * purely by letters distinctive to each, with no profiles, no tokenization, and\n * no franc. Reach for it when you just need \"is this Russian / is this\n * Ukrainian?\" on a hot path and don't want to assemble a candidate set.\n *\n * Other Cyrillic languages langtell now profiles (Serbian, Macedonian, Kazakh)\n * are deliberately NOT positively detected here — that is the candidate-relative\n * classifier's job. This module only takes care not to MISLABEL them: text\n * carrying letters distinctive to sr/mk/kk (ђ ћ џ ѓ ќ ѕ љ њ ј, or the Kazakh\n * Turkic set ә ғ қ ң ө ұ ү һ) returns `\"unknown\"` so the snippet escalates to\n * the classifier instead of being silently called Russian by the fallbacks.\n *\n * Each language carries letters the others (mostly) don't:\n * Ukrainian — і ї є ґ\n * Russian — ы ё (ъ э are shared with bg/be and handled separately)\n * Belarusian — ў (uniquely Belarusian)\n * Bulgarian — ъ used as a vowel in nearly every word\n *\n * The shared letters `ъ` and `э` need care: `подъезд` is Russian, `съм\n * българин` is Bulgarian — both contain `ъ`. We disambiguate by density and\n * length: a single `ъ` in a short snippet leans RU; multiple `ъ` in longer text\n * leans BG. A lone `э` with no other distinctives stays `\"unknown\"` rather than\n * silently guessing (Russian uses it in loanwords/`Это`, Belarusian in\n * `гэта`/`сэрца`).\n *\n * Every call is checked against the letters that argue with it before it is\n * returned: a verdict whose own alphabet cannot account for the snippet (`ы` in\n * a text called Ukrainian) is withdrawn to `\"unknown\"`. Positive signals alone\n * cannot tell a fifth language built from the same letters — Belarusian without\n * a `ў` — from the four this module claims.\n *\n * Zero-dependency and side-effect-free. The cheap heuristic stays cheap; if a\n * use case needs more than letter signals can give, escalate to\n * {@link classifyBySnippet} or a franc-backed source.\n */\nimport { stripQuoted } from \"./internal/scrub.js\";\n\n/** The four Cyrillic languages this fast-path tells apart, plus the `\"unknown\"`\n * sentinel when letter signals are insufficient. */\nexport type CyrillicLanguage = \"uk\" | \"ru\" | \"be\" | \"bg\" | \"unknown\";\n\nconst UK_DISTINCTIVE = /[іїєґ]/gi;\nconst RU_DISTINCTIVE = /[ыё]/gi;\nconst BE_DISTINCTIVE = /ў/gi;\nconst HARD_SIGN = /ъ/gi;\nconst E_OBOROT = /э/gi;\n// Letters distinctive to Serbian / Macedonian / Kazakh — none of which this\n// fast-path positively detects. Their presence only triggers a bail to\n// `\"unknown\"` (escalate to the classifier) so the Russian fallbacks below never\n// mislabel them. ј/љ/њ/џ are shared by sr+mk; ђ/ћ are Serbian, ѓ/ќ/ѕ Macedonian,\n// and ә/ғ/қ/ң/ө/ұ/ү/һ the distinctive Kazakh Turkic set (і is excluded here — it\n// is already Ukrainian-distinctive and handled above).\nconst SIBLING_DISTINCTIVE = /[ђћџѓќѕљњјәғқңөұүһ]/gi;\n// U+0400–U+04FF is the Cyrillic Unicode block; written as explicit \\u escapes\n// so the range bounds are unambiguous (regexp/no-obscure-range).\nconst CYRILLIC = /[\\u0400-\\u04FF]/g;\n\n/**\n * Per language, the letters its alphabet does NOT contain — evidence against a\n * call, whoever else the letter might belong to.\n *\n * The signal sets above only argue FOR a language, and a cascade of positive\n * signals has no way to notice that it is reading a fifth language built out of\n * the same letters. Belarusian is exactly that: `і` from the Ukrainian set, `ы`\n * and `ё` from the Russian one, and — in a snippet that happens to carry no `ў` —\n * nothing at all to say it is neither. `Мова і культура Беларусі маюць багатую\n * гісторыю` came back Ukrainian on four `і`s while a `ы` Ukrainian does not have\n * sat in the same sentence. These are the sets that see that.\n *\n * Each is the complement of that language's alphabet within the four, so every\n * entry is checkable against the profiles in `./profiles.ts`: uk has no ы/ё/ъ/э/ў,\n * ru no і/ї/є/ґ/ў, be no и/щ/ъ/ї/є/ґ, bg no ы/ё/э/і/ї/є/ґ/ў.\n */\nconst FOREIGN: Readonly<Record<Exclude<CyrillicLanguage, \"unknown\">, RegExp>> = {\n uk: /[ыёъэў]/gi,\n ru: /[іїєґў]/gi,\n be: /[ищъїєґ]/gi,\n bg: /[ыёэіїєґў]/gi,\n};\n\n/**\n * Share of a snippet's Cyrillic letters that may be foreign to a verdict before\n * the verdict is withdrawn.\n *\n * Set from the same measurement as the roster-relative classifier's\n * `CONTRADICTION_SHARE`, and deliberately identical to it: a snippet that is too\n * contradicted for one of langtell's detectors is too contradicted for the other,\n * and two thresholds would be two answers to one question. On this corpus a\n * genuine sibling citation inside monolingual prose sits at 0.6–0.9 %, while\n * Belarusian misread as Ukrainian sits at 2.3–5 %.\n */\nconst MAX_FOREIGN_SHARE = 0.02;\n\n/** Minimum Cyrillic-letter count before the fallback guesses a language. Below\n * this, a short snippet (`Привет`, `Хочу`) is too ambiguous to act on. */\nconst MIN_CYRILLIC_FOR_FALLBACK = 10;\n/** Minimum text length before `ъ`-density is read as Bulgarian. Single-word\n * Russian samples like `подъезд` (7 chars) would otherwise misclassify. */\nconst MIN_LEN_FOR_BG = 10;\n\n/** The verdict from {@link detectCyrillicLanguage}: the chosen language plus the\n * raw distinctive-letter tallies that drove a uk-vs-ru decision (informational;\n * `ruScore` carries the deciding count for the ru/bg fallbacks too). */\nexport interface CyrillicVerdict {\n language: CyrillicLanguage;\n ukScore: number;\n ruScore: number;\n}\n\nfunction count(text: string, re: RegExp): number {\n return (text.match(re) ?? []).length;\n}\n\n/** Raw letter-signal tallies over `text`, one pass per distinctive class.\n * Gathered up front so {@link detectCyrillicLanguage} reads as a pure decision\n * cascade over these counts rather than interleaving counting and branching. */\ninterface Signals {\n ukScore: number;\n ruDistinctive: number;\n beScore: number;\n hardSigns: number;\n eOborot: number;\n cyrillicCount: number;\n /** Count of letters distinctive to sr/mk/kk (see {@link SIBLING_DISTINCTIVE}).\n * Non-zero ⇒ bail to `\"unknown\"` before the Russian fallbacks. */\n siblingDistinctive: number;\n}\n\nfunction countSignals(text: string): Signals {\n return {\n ukScore: count(text, UK_DISTINCTIVE),\n ruDistinctive: count(text, RU_DISTINCTIVE),\n beScore: count(text, BE_DISTINCTIVE),\n hardSigns: count(text, HARD_SIGN),\n eOborot: count(text, E_OBOROT),\n cyrillicCount: count(text, CYRILLIC),\n siblingDistinctive: count(text, SIBLING_DISTINCTIVE),\n };\n}\n\n/**\n * Identify the Cyrillic language of `text` by distinctive letters, returning the\n * chosen language and the uk/ru tallies behind it. `\"unknown\"` when there is no\n * Cyrillic evidence, on a uk/ru tie, when only an ambiguous `э` is present, or\n * when the text contradicts the call — see {@link FOREIGN}.\n */\nexport function detectCyrillicLanguage(text: string): CyrillicVerdict {\n const verdict = decide(text);\n if (verdict.language === \"unknown\" || !contradicted(text, verdict.language)) return verdict;\n // The tallies are kept: they are the account of a call that was withdrawn, and\n // a caller escalating to the roster-relative classifier is better served by\n // \"here is what the letters said, and it did not hold up\" than by zeroes.\n return { ...verdict, language: \"unknown\" };\n}\n\n/**\n * Whether `text` carries enough letters `language` does not have to withdraw the\n * call. Counted as a share of Cyrillic letters, so a proper noun borrowed from a\n * neighbour (`Ђоковић` in a Russian sentence, ~1.5 %) reads as the loanword it is\n * while a whole sentence in another language does not.\n *\n * Quotations are left out of the count — someone else's words, in whatever\n * language they said them, are not evidence against this author. The positive\n * cascade above still reads the text whole; only this measurement excludes them,\n * exactly as the roster-relative classifier splits the two.\n */\nfunction contradicted(text: string, language: Exclude<CyrillicLanguage, \"unknown\">): boolean {\n const own = stripQuoted(text);\n const cyrillic = count(own, CYRILLIC);\n if (cyrillic === 0) return false;\n return count(own, FOREIGN[language]) / cyrillic >= MAX_FOREIGN_SHARE;\n}\n\n/** The positive-signal cascade, before {@link detectCyrillicLanguage} checks the\n * winner against the letters that argue with it. */\nfunction decide(text: string): CyrillicVerdict {\n const { ukScore, ruDistinctive, beScore, hardSigns, eOborot, cyrillicCount, siblingDistinctive } =\n countSignals(text);\n\n // ў is uniquely Belarusian — strongest single signal.\n if (beScore > 0) {\n return { language: \"be\", ukScore, ruScore: ruDistinctive };\n }\n\n // Mislabel guard: letters distinctive to Serbian/Macedonian/Kazakh, none of\n // which this fast-path positively detects. Bail to `\"unknown\"` so the snippet\n // escalates to the candidate-relative classifier instead of being silently\n // labelled by the uk/ru/bg logic below. This fires BEFORE the uk/ru checks on\n // purpose: Kazakh shares і with Ukrainian and ы with Russian (тілі, тым), so a\n // Kazakh Turkic letter (ә/ғ/қ/…) must outrank a stray і/ы rather than letting\n // the text be called Ukrainian or Russian. Genuine uk/ru/be/bg text carries\n // none of these letters, so their detection above/below is unaffected.\n if (siblingDistinctive > 0) {\n return { language: \"unknown\", ukScore, ruScore: ruDistinctive };\n }\n\n // Both UA and RU evidence present — a tie is \"unknown\", not a silent UA call\n // (which would misclassify Belarusian/Bulgarian whenever і or ё balances out).\n if (ukScore > 0 && ruDistinctive > 0) {\n if (ukScore === ruDistinctive) {\n return { language: \"unknown\", ukScore, ruScore: ruDistinctive };\n }\n return {\n language: ukScore > ruDistinctive ? \"uk\" : \"ru\",\n ukScore,\n ruScore: ruDistinctive,\n };\n }\n\n // Distinctive RU letters (ы, ё) with no UA evidence — unambiguously RU.\n if (ruDistinctive > 0) {\n return { language: \"ru\", ukScore, ruScore: ruDistinctive };\n }\n\n // Distinctive UA letters with no RU distinctives — UA.\n if (ukScore > 0) {\n return { language: \"uk\", ukScore, ruScore: 0 };\n }\n\n // Bulgarian: ъ as a vowel — multiple occurrences in non-trivial text, no UA /\n // RU distinctives. Length guard keeps short Russian compounds like `подъезд`\n // from sliding into BG.\n if (hardSigns >= 2 && text.length >= MIN_LEN_FOR_BG) {\n return { language: \"bg\", ukScore: 0, ruScore: hardSigns };\n }\n\n // Russian fallback: substantial Cyrillic with no UA, no BE distinctives, and\n // no э (which signals possible Belarusian). Catches short-but-clear Russian\n // text with none of ы/ё/ъ/э (`Привет, мир`, `Здравствуйте, меня зовут …`).\n if (cyrillicCount >= MIN_CYRILLIC_FOR_FALLBACK && eOborot === 0) {\n return { language: \"ru\", ukScore: 0, ruScore: 0 };\n }\n\n // Lone ъ (no э, no other distinctives) in shorter text — RU compound-word\n // pattern (подъезд, объект, съезд).\n if (hardSigns > 0 && eOborot === 0) {\n return { language: \"ru\", ukScore: 0, ruScore: hardSigns };\n }\n\n return { language: \"unknown\", ukScore: 0, ruScore: 0 };\n}\n\n/** Convenience predicate: `true` iff {@link detectCyrillicLanguage} calls `text`\n * Russian. */\nexport function isRussian(text: string): boolean {\n return detectCyrillicLanguage(text).language === \"ru\";\n}\n\n/** Convenience predicate: `true` iff {@link detectCyrillicLanguage} calls `text`\n * Ukrainian. */\nexport function isUkrainian(text: string): boolean {\n return detectCyrillicLanguage(text).language === \"uk\";\n}\n"]}
|
package/dist/franc.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LanguageProfile, S as SyncSource, a as LanguageEvidence } from './types-
|
|
1
|
+
import { L as LanguageProfile, S as SyncSource, a as LanguageEvidence } from './types-DkB4436E.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Producer: the franc trigram backstop over `text`, scoped to `candidates`.
|
package/dist/franc.js
CHANGED
package/dist/franc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/franc.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/franc.ts"],"names":[],"mappings":"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","file":"franc.js","sourcesContent":["/**\n * `langtell/franc` — the opt-in franc engine (trigram-based detection, ~187\n * languages). Importing this module statically pulls `franc` and its trigram\n * tables, so it lives behind its own subpath; the zero-dependency core never\n * imports it (enforced by an ESLint boundary rule). `franc` is declared as an\n * OPTIONAL peer dependency — install it only if you use this engine.\n *\n * The engine is a candidate-relative *backstop*: franc is scoped to the\n * candidates' ISO 639-3 codes (`only`), runs only on text past a length floor\n * where trigrams are reliable, and emits `kind: \"franc\"` evidence with franc's\n * own score-gap as the confidence. It abstains (emits nothing) when fewer than\n * two candidates carry an `iso6393`, when franc returns `und`, or when the\n * sample is too short.\n */\nimport { francAll } from \"franc\";\nimport type { LanguageEvidence, LanguageProfile, SyncSource } from \"./types.js\";\nimport { scopeCandidates } from \"./internal/classify.js\";\n\n/** Minimum sample length, in characters. Below this trigrams are too noisy. */\nconst RUNG_MIN_LENGTH = 24;\n/** Floor franc itself uses to bail to `und` rather than guess. */\nconst FRANC_MIN_LENGTH = 10;\n/** Cap on text length sent to franc (longer adds cost, not accuracy). */\nconst DEFAULT_MAX_CHARS = 2000;\n\n/**\n * Run franc scoped to the candidates' ISO 639-3 codes, mapping the winner back\n * to its BCP-47 code. Returns `null` when fewer than two candidates carry an\n * `iso6393` code or franc abstains (`und`). The margin is franc's own score-gap\n * (top1 − top2, 0..1).\n */\nfunction francScore(\n text: string,\n scoped: readonly LanguageProfile[],\n): { language: string; margin: number } | null {\n const byIso = new Map<string, string>();\n for (const c of scoped) if (c.iso6393 !== undefined) byIso.set(c.iso6393, c.code);\n if (byIso.size < 2) return null;\n const sample = text.slice(0, DEFAULT_MAX_CHARS);\n const ranked = francAll(sample, { only: [...byIso.keys()], minLength: FRANC_MIN_LENGTH });\n const top = ranked[0];\n if (!top || top[0] === \"und\") return null;\n const language = byIso.get(top[0]);\n if (language === undefined) return null;\n return { language, margin: top[1] - (ranked[1]?.[1] ?? 0) };\n}\n\n/**\n * Producer: the franc trigram backstop over `text`, scoped to `candidates`.\n * Synchronous — franc itself is sync. Emits at most one `kind: \"franc\"` item.\n */\nexport function evidenceFromFranc(\n text: string | undefined,\n candidates: readonly LanguageProfile[] | undefined,\n): LanguageEvidence[] {\n if (text === undefined || text.trim().length < RUNG_MIN_LENGTH) return [];\n if (candidates === undefined || candidates.length === 0) return [];\n\n const scoped = scopeCandidates(text, candidates);\n const scored = francScore(text, scoped);\n if (scored === null) return [];\n\n return [\n {\n kind: \"franc\",\n language: scored.language,\n // franc's score-gap is 0..1; lift it into a usable confidence band.\n confidence: clamp01(0.4 + scored.margin * 0.5),\n source: \"franc\",\n value: scored.language,\n },\n ];\n}\n\n/**\n * Build a franc {@link SyncSource} bound to a candidate roster, for use in\n * `compile({ engines: [createFrancEngine(candidates)] })`. franc needs the\n * roster to scope its `only` restriction, so it is bound at construction (the\n * same shape `compile` uses to bind the built-in text producer).\n *\n * A `SyncSource` (not async): franc runs in-process and synchronously, so the\n * compiled `detect` stays synchronous — no `await` ceremony on the hot path.\n */\nexport function createFrancEngine(candidates: readonly LanguageProfile[]): SyncSource {\n return {\n id: \"franc\",\n sync: true,\n inputs: [\"text\"],\n detect: (input) => evidenceFromFranc(input.text, candidates),\n };\n}\n\n/**\n * A bare franc engine with no bound roster — it abstains until given candidates.\n * Prefer {@link createFrancEngine} with your roster; this default exists so the\n * engine has a stable named export and a no-config import works.\n */\nexport const francEngine: SyncSource = {\n id: \"franc\",\n sync: true,\n inputs: [\"text\"],\n detect: (input) => evidenceFromFranc(input.text, undefined),\n};\n\nfunction clamp01(value: number): number {\n if (!Number.isFinite(value)) return 0;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n"]}
|
package/dist/fuse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { W as Weights, L as LanguageProfile, N as NonDiscriminatingScript, a as LanguageEvidence, C as Classification } from './types-
|
|
1
|
+
import { W as Weights, L as LanguageProfile, N as NonDiscriminatingScript, a as LanguageEvidence, C as Classification } from './types-DkB4436E.js';
|
|
2
2
|
|
|
3
3
|
interface FuseOptions {
|
|
4
4
|
weights?: Weights;
|
package/dist/fuse.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { fuse } from './chunk-
|
|
1
|
+
export { fuse } from './chunk-LUSZIBTZ.js';
|
|
2
2
|
import './chunk-YCUSX3GG.js';
|
|
3
|
-
import './chunk-
|
|
3
|
+
import './chunk-SOTZVW3I.js';
|
|
4
|
+
import './chunk-2Z7REIOE.js';
|
|
4
5
|
//# sourceMappingURL=fuse.js.map
|
|
5
6
|
//# sourceMappingURL=fuse.js.map
|
package/dist/headers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HeaderBag, a as LanguageEvidence } from './types-
|
|
1
|
+
import { H as HeaderBag, a as LanguageEvidence } from './types-DkB4436E.js';
|
|
2
2
|
|
|
3
3
|
/** Producer: the HTTP `Content-Language` response header. */
|
|
4
4
|
declare function evidenceFromHeaders(headers: HeaderBag | undefined): LanguageEvidence[];
|
package/dist/html.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as EvidenceSource, D as DetectorConfig, c as DetectFn } from './types-
|
|
2
|
-
export { A as AsyncSource, C as Classification,
|
|
1
|
+
import { E as EvidenceSource, D as DetectorConfig, c as DetectFn, a as LanguageEvidence } from './types-DkB4436E.js';
|
|
2
|
+
export { A as AsyncSource, d as AttrBag, C as Classification, e as DetectContext, f as DetectInput, g as EarlyExit, h as EvidenceKind, i as HasAsync, H as HeaderBag, b as LanguageCode, L as LanguageProfile, N as NonDiscriminatingScript, j as SourceInput, S as SyncSource, W as Weights } from './types-DkB4436E.js';
|
|
3
3
|
export { FuseOptions, fuse } from './fuse.js';
|
|
4
4
|
export { evidenceFromText } from './text.js';
|
|
5
5
|
export { evidenceFromHtml } from './html.js';
|
|
@@ -14,6 +14,40 @@ import './classify.js';
|
|
|
14
14
|
*/
|
|
15
15
|
declare function compile<const E extends readonly EvidenceSource[] = []>(config?: DetectorConfig<E>): DetectFn<E>;
|
|
16
16
|
|
|
17
|
+
/** The pipeline's default declaration attributes: only the standardized
|
|
18
|
+
* global `lang`. Vendor labels (Google's `data-rl` and kin) are page-private
|
|
19
|
+
* `data-*` conventions and must be opted into per detector — see
|
|
20
|
+
* {@link DetectorConfig.nodeLangAttributes}. */
|
|
21
|
+
declare const DEFAULT_NODE_LANG_ATTRIBUTES: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Producer: a per-node declared language — the page's own label for one
|
|
24
|
+
* content node, read from an attribute. The one STANDARDIZED bearer is the
|
|
25
|
+
* global `lang` attribute (BCP-47, inheritable); everything else is a vendor
|
|
26
|
+
* convention the caller opts into — e.g. Google's `data-rl` response-language
|
|
27
|
+
* label on AI-generated answers. No attribute name is built in here: which
|
|
28
|
+
* attributes count as declarations is entirely the caller's choice.
|
|
29
|
+
*
|
|
30
|
+
* The document-scope siblings of this signal live in {@link evidenceFromHtml}
|
|
31
|
+
* (`<html lang>`, `content-language`, `og:locale`); this is the same
|
|
32
|
+
* declaration concept one scope down. Like them it is *context* evidence: the
|
|
33
|
+
* fuser weighs it against text reads, and its script guard keeps a
|
|
34
|
+
* declaration from overriding a confident text read — a mislabeled node loses
|
|
35
|
+
* to clear script evidence, while weak or absent text lets the declaration
|
|
36
|
+
* decide.
|
|
37
|
+
*
|
|
38
|
+
* DOM-free by design — this module never touches an Element. The caller
|
|
39
|
+
* passes the attribute values it extracted, keyed by attribute name
|
|
40
|
+
* (`{ lang: el.getAttribute("lang") }`, `{ "data-rl": … }`). Each recognized
|
|
41
|
+
* value becomes one evidence item whose `source` is the attribute name under
|
|
42
|
+
* the `node-lang:` namespace (`node-lang:data-rl`) — namespaced so an
|
|
43
|
+
* attribute that happens to share a name with an engine id or evidence kind
|
|
44
|
+
* can never collide in the {@link FuseOptions.weights} keyspace. That lets
|
|
45
|
+
* weights target one attribute — trust a curated vendor label above an
|
|
46
|
+
* often-stale inherited `lang`, or silence an attribute entirely — instead
|
|
47
|
+
* of the whole kind. Empty and unrecognized values are dropped.
|
|
48
|
+
*/
|
|
49
|
+
declare function evidenceFromNodeLang(attrs: Readonly<Record<string, string | null | undefined>>): LanguageEvidence[];
|
|
50
|
+
|
|
17
51
|
/**
|
|
18
52
|
* BCP-47 / language-code normalization.
|
|
19
53
|
*
|
|
@@ -74,4 +108,4 @@ declare function normalizeBCP47(input: string | undefined | null, options?: Norm
|
|
|
74
108
|
*/
|
|
75
109
|
declare function primarySubtag(value: string | undefined | null): string | null;
|
|
76
110
|
|
|
77
|
-
export { DetectFn, DetectorConfig, EvidenceSource, type NormalizeBCP47Options, compile, normalizeBCP47, normalizeLanguageCode, primarySubtag };
|
|
111
|
+
export { DEFAULT_NODE_LANG_ATTRIBUTES, DetectFn, DetectorConfig, EvidenceSource, LanguageEvidence, type NormalizeBCP47Options, compile, evidenceFromNodeLang, normalizeBCP47, normalizeLanguageCode, primarySubtag };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
import { evidenceFromText } from './chunk-
|
|
2
|
-
export { evidenceFromText } from './chunk-
|
|
1
|
+
import { evidenceFromText } from './chunk-7N545CET.js';
|
|
2
|
+
export { evidenceFromText } from './chunk-7N545CET.js';
|
|
3
3
|
import { evidenceFromHtml } from './chunk-K4MXTIY7.js';
|
|
4
4
|
export { evidenceFromHtml } from './chunk-K4MXTIY7.js';
|
|
5
5
|
import { evidenceFromHeaders } from './chunk-FEKBPTHK.js';
|
|
6
6
|
export { evidenceFromHeaders } from './chunk-FEKBPTHK.js';
|
|
7
|
-
import { fuse } from './chunk-
|
|
8
|
-
export { fuse } from './chunk-
|
|
7
|
+
import { fuse } from './chunk-LUSZIBTZ.js';
|
|
8
|
+
export { fuse } from './chunk-LUSZIBTZ.js';
|
|
9
|
+
import { normalizeBCP47 } from './chunk-YCUSX3GG.js';
|
|
9
10
|
export { normalizeBCP47, normalizeLanguageCode, primarySubtag } from './chunk-YCUSX3GG.js';
|
|
10
|
-
import './chunk-
|
|
11
|
+
import './chunk-SOTZVW3I.js';
|
|
12
|
+
import './chunk-2Z7REIOE.js';
|
|
13
|
+
|
|
14
|
+
// src/node-lang.ts
|
|
15
|
+
var NODE_LANG_CONFIDENCE = 0.7;
|
|
16
|
+
var DEFAULT_NODE_LANG_ATTRIBUTES = ["lang"];
|
|
17
|
+
function evidenceFromNodeLang(attrs) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const [attribute, raw] of Object.entries(attrs)) {
|
|
20
|
+
if (raw == null || raw === "") continue;
|
|
21
|
+
const language = normalizeBCP47(raw);
|
|
22
|
+
if (language === null) continue;
|
|
23
|
+
out.push({
|
|
24
|
+
kind: "node-lang",
|
|
25
|
+
language,
|
|
26
|
+
confidence: NODE_LANG_CONFIDENCE,
|
|
27
|
+
source: `node-lang:${attribute}`,
|
|
28
|
+
value: raw
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
11
33
|
|
|
12
34
|
// src/compile.ts
|
|
13
|
-
function builtIns(candidates) {
|
|
35
|
+
function builtIns(candidates, nodeLangAttributes) {
|
|
14
36
|
return [
|
|
15
37
|
{
|
|
16
38
|
id: "text",
|
|
@@ -24,6 +46,16 @@ function builtIns(candidates) {
|
|
|
24
46
|
sync: true,
|
|
25
47
|
inputs: ["headers"],
|
|
26
48
|
detect: (i) => evidenceFromHeaders(i.headers)
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "node-lang",
|
|
52
|
+
sync: true,
|
|
53
|
+
inputs: ["attrs"],
|
|
54
|
+
detect: (i) => {
|
|
55
|
+
const declared = {};
|
|
56
|
+
for (const name of nodeLangAttributes) declared[name] = i.attrs?.[name];
|
|
57
|
+
return evidenceFromNodeLang(declared);
|
|
58
|
+
}
|
|
27
59
|
}
|
|
28
60
|
];
|
|
29
61
|
}
|
|
@@ -31,7 +63,10 @@ function applicable(source, input) {
|
|
|
31
63
|
return source.inputs.every((key) => input[key] !== void 0);
|
|
32
64
|
}
|
|
33
65
|
function compile(config = {}) {
|
|
34
|
-
const sources = [
|
|
66
|
+
const sources = [
|
|
67
|
+
...builtIns(config.candidates, config.nodeLangAttributes ?? DEFAULT_NODE_LANG_ATTRIBUTES),
|
|
68
|
+
...config.engines ?? []
|
|
69
|
+
];
|
|
35
70
|
const hasAsync = sources.some((source) => !source.sync);
|
|
36
71
|
const fuseOptions = {
|
|
37
72
|
weights: config.weights,
|
|
@@ -62,6 +97,6 @@ function compile(config = {}) {
|
|
|
62
97
|
return detect;
|
|
63
98
|
}
|
|
64
99
|
|
|
65
|
-
export { compile };
|
|
100
|
+
export { DEFAULT_NODE_LANG_ATTRIBUTES, compile, evidenceFromNodeLang };
|
|
66
101
|
//# sourceMappingURL=index.js.map
|
|
67
102
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/compile.ts"],"names":["detect"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/node-lang.ts","../src/compile.ts"],"names":["detect"],"mappings":"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","file":"index.js","sourcesContent":["import type { LanguageEvidence } from \"./types.js\";\nimport { normalizeBCP47 } from \"./internal/bcp47.js\";\n\n/** Confidence for a node-level declaration: above the page-scope tags (a label\n * on the node itself sits closer to the content than `<html lang>` does), but\n * still context evidence — {@link fuse}'s guard keeps it from flipping clear\n * script evidence. */\nconst NODE_LANG_CONFIDENCE = 0.7;\n\n/** The pipeline's default declaration attributes: only the standardized\n * global `lang`. Vendor labels (Google's `data-rl` and kin) are page-private\n * `data-*` conventions and must be opted into per detector — see\n * {@link DetectorConfig.nodeLangAttributes}. */\nexport const DEFAULT_NODE_LANG_ATTRIBUTES: readonly string[] = [\"lang\"];\n\n/**\n * Producer: a per-node declared language — the page's own label for one\n * content node, read from an attribute. The one STANDARDIZED bearer is the\n * global `lang` attribute (BCP-47, inheritable); everything else is a vendor\n * convention the caller opts into — e.g. Google's `data-rl` response-language\n * label on AI-generated answers. No attribute name is built in here: which\n * attributes count as declarations is entirely the caller's choice.\n *\n * The document-scope siblings of this signal live in {@link evidenceFromHtml}\n * (`<html lang>`, `content-language`, `og:locale`); this is the same\n * declaration concept one scope down. Like them it is *context* evidence: the\n * fuser weighs it against text reads, and its script guard keeps a\n * declaration from overriding a confident text read — a mislabeled node loses\n * to clear script evidence, while weak or absent text lets the declaration\n * decide.\n *\n * DOM-free by design — this module never touches an Element. The caller\n * passes the attribute values it extracted, keyed by attribute name\n * (`{ lang: el.getAttribute(\"lang\") }`, `{ \"data-rl\": … }`). Each recognized\n * value becomes one evidence item whose `source` is the attribute name under\n * the `node-lang:` namespace (`node-lang:data-rl`) — namespaced so an\n * attribute that happens to share a name with an engine id or evidence kind\n * can never collide in the {@link FuseOptions.weights} keyspace. That lets\n * weights target one attribute — trust a curated vendor label above an\n * often-stale inherited `lang`, or silence an attribute entirely — instead\n * of the whole kind. Empty and unrecognized values are dropped.\n */\nexport function evidenceFromNodeLang(\n attrs: Readonly<Record<string, string | null | undefined>>,\n): LanguageEvidence[] {\n const out: LanguageEvidence[] = [];\n for (const [attribute, raw] of Object.entries(attrs)) {\n if (raw == null || raw === \"\") continue;\n const language = normalizeBCP47(raw);\n if (language === null) continue;\n out.push({\n kind: \"node-lang\",\n language,\n confidence: NODE_LANG_CONFIDENCE,\n source: `node-lang:${attribute}`,\n value: raw,\n });\n }\n return out;\n}\n","import { evidenceFromHeaders } from \"./headers.js\";\nimport { evidenceFromHtml } from \"./html.js\";\nimport { DEFAULT_NODE_LANG_ATTRIBUTES, evidenceFromNodeLang } from \"./node-lang.js\";\nimport { evidenceFromText } from \"./text.js\";\nimport { fuse, type FuseOptions } from \"./fuse.js\";\nimport type {\n Classification,\n DetectContext,\n DetectFn,\n DetectInput,\n DetectorConfig,\n EvidenceSource,\n LanguageEvidence,\n LanguageProfile,\n SyncSource,\n} from \"./types.js\";\n\n/** The always-on, zero-dependency producers. The text producer is bound to the\n * configured candidate roster so its scoring is roster-relative (and so it\n * abstains when no roster was supplied — its signals need candidates). The\n * node-lang producer is bound to the configured declaration-attribute list\n * (standard `lang` only, unless the config opts into vendor attributes), and\n * reads them from `input.attrs` in configured order. */\nfunction builtIns(\n candidates: readonly LanguageProfile[] | undefined,\n nodeLangAttributes: readonly string[],\n): SyncSource[] {\n return [\n {\n id: \"text\",\n sync: true,\n inputs: [\"text\"],\n detect: (i) => evidenceFromText(i.text, candidates),\n },\n { id: \"html\", sync: true, inputs: [\"html\"], detect: (i) => evidenceFromHtml(i.html) },\n {\n id: \"headers\",\n sync: true,\n inputs: [\"headers\"],\n detect: (i) => evidenceFromHeaders(i.headers),\n },\n {\n id: \"node-lang\",\n sync: true,\n inputs: [\"attrs\"],\n detect: (i) => {\n const declared: Record<string, string | null | undefined> = {};\n for (const name of nodeLangAttributes) declared[name] = i.attrs?.[name];\n return evidenceFromNodeLang(declared);\n },\n },\n ];\n}\n\n/** Run a source only when every input it declares is present. */\nfunction applicable(source: EvidenceSource, input: DetectInput): boolean {\n return source.inputs.every((key) => input[key] !== undefined);\n}\n\n/**\n * Build a configured detector. Does the per-roster setup once and returns a\n * `detect` function whose sync/async shape is fixed by the registered engines\n * (see {@link DetectFn}). The built-in producers are always registered; opt-in\n * engines (franc, chrome-ai) are added via `config.engines`.\n */\nexport function compile<const E extends readonly EvidenceSource[] = []>(\n config: DetectorConfig<E> = {},\n): DetectFn<E> {\n const sources: EvidenceSource[] = [\n ...builtIns(config.candidates, config.nodeLangAttributes ?? DEFAULT_NODE_LANG_ATTRIBUTES),\n ...(config.engines ?? []),\n ];\n const hasAsync = sources.some((source) => !source.sync);\n const fuseOptions: FuseOptions = {\n weights: config.weights,\n candidates: config.candidates,\n nonDiscriminatingScript: config.nonDiscriminatingScript,\n };\n\n if (!hasAsync) {\n const detect = (input: DetectInput): Classification => {\n const evidence: LanguageEvidence[] = [];\n for (const source of sources) {\n if (source.sync && applicable(source, input)) evidence.push(...source.detect(input));\n }\n return fuse(evidence, fuseOptions);\n };\n return detect as DetectFn<E>;\n }\n\n const detect = async (input: DetectInput, ctx: DetectContext = {}): Promise<Classification> => {\n const evidence: LanguageEvidence[] = [];\n const pending: Promise<LanguageEvidence[]>[] = [];\n for (const source of sources) {\n if (!applicable(source, input)) continue;\n if (source.sync) evidence.push(...source.detect(input));\n else pending.push(Promise.resolve(source.detect(input, ctx)).catch(() => []));\n }\n for (const batch of await Promise.all(pending)) evidence.push(...batch);\n return fuse(evidence, fuseOptions);\n };\n return detect as DetectFn<E>;\n}\n"]}
|
package/dist/profiles.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as LanguageCode, L as LanguageProfile } from './types-
|
|
1
|
+
import { b as LanguageCode, L as LanguageProfile } from './types-DkB4436E.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* `langtell/profiles` — ready-to-use {@link LanguageProfile} data.
|
|
@@ -30,6 +30,9 @@ declare const ru: LanguageProfile;
|
|
|
30
30
|
declare const be: LanguageProfile;
|
|
31
31
|
declare const bg: LanguageProfile;
|
|
32
32
|
declare const en: LanguageProfile;
|
|
33
|
+
declare const sr: LanguageProfile;
|
|
34
|
+
declare const mk: LanguageProfile;
|
|
35
|
+
declare const kk: LanguageProfile;
|
|
33
36
|
|
|
34
37
|
/** Registry of shipped profiles, keyed by BCP-47 code. */
|
|
35
38
|
declare const PROFILES: Readonly<Record<LanguageCode, LanguageProfile>>;
|
|
@@ -44,4 +47,4 @@ declare function hasProfile(code: LanguageCode): boolean;
|
|
|
44
47
|
/** Resolve profiles for the given codes, skipping any without a shipped profile. */
|
|
45
48
|
declare function getProfiles(codes: readonly LanguageCode[]): LanguageProfile[];
|
|
46
49
|
|
|
47
|
-
export { PROFILED_CODES, PROFILES, be, bg, en, getProfiles, hasProfile, ru, uk };
|
|
50
|
+
export { PROFILED_CODES, PROFILES, be, bg, en, getProfiles, hasProfile, kk, mk, ru, sr, uk };
|