LCOV - code coverage report
Current view: top level - src - composite_backend.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 24 24
Test Date: 2026-08-24 07:45:26 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2026 The Authors.
       2              : //
       3              : // Licensed under the Apache License, Version 2.0 (the "License");
       4              : // you may not use this file except in compliance with the License.
       5              : // You may obtain a copy of the License at
       6              : //
       7              : //     https://www.apache.org/licenses/LICENSE-2.0
       8              : //
       9              : // Unless required by applicable law or agreed to in writing, software
      10              : // distributed under the License is distributed on an "AS IS" BASIS,
      11              : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12              : // See the License for the specific language governing permissions and
      13              : // limitations under the License.
      14              : 
      15              : import 'backend.dart';
      16              : import 'guess.dart';
      17              : import 'ngram/ngram_backend.dart';
      18              : import 'ngram/profiles.g.dart' show ngramProfiles;
      19              : import 'script/script_candidates.dart';
      20              : import 'script/script_filter.dart';
      21              : 
      22              : /// The default [LanguageDetectorBackend]: a Unicode script pre-filter that
      23              : /// short-circuits for scripts exclusive to one of [allLanguages], falling
      24              : /// back to [NgramBackend] for the rest.
      25              : ///
      26              : /// Orchestration:
      27              : ///
      28              : /// 1. A single [scanScript] pass over the input yields both the dominant
      29              : ///    script and whether any kana codepoint was present.
      30              : /// 2. If kana was present and `'ja'` is a candidate, the result is Japanese
      31              : ///    — immediately, with no n-gram scoring.
      32              : /// 3. Else if the dominant script is Han with no kana and `'zh'` is a
      33              : ///    candidate, the result is Chinese.
      34              : /// 4. Else if the dominant script maps to exactly one candidate language via
      35              : ///    [scriptExclusiveLanguage], that language is the result.
      36              : /// 5. Otherwise, the n-gram stage runs: the candidate set is narrowed via
      37              : ///    [scriptCandidates] if the dominant script identifies a relevant script
      38              : ///    family (Latin, Cyrillic, Arabic, Devanagari), then scored by
      39              : ///    [NgramBackend].
      40              : ///
      41              : /// Every input is handled. Script-less text, or a `restrictTo` that excludes
      42              : /// every candidate a script suggests, narrows the n-gram stage down to zero
      43              : /// profiles — [NgramBackend] returns `[]` for that, which
      44              : /// [LanguageDetector.detect] turns into [Undetermined]. A `restrictTo` that
      45              : /// narrows to exactly *one* plausible language instead reaches the n-gram
      46              : /// stage with a single profile, which [NgramBackend] scores at confidence
      47              : /// `1.0` by definition (there is nothing else to compare against) — this can
      48              : /// be surprising: it says "the only candidate you allowed" regardless of how
      49              : /// well the text actually fits that language.
      50              : final class CompositeBackend implements LanguageDetectorBackend {
      51              :   /// Creates a backend limited to [restrictTo] (intersected with
      52              :   /// [allLanguages]), or all 58 languages if [restrictTo] is `null`.
      53            2 :   CompositeBackend({Set<String>? restrictTo})
      54              :     : _candidates = restrictTo == null
      55              :           ? allLanguages
      56            2 :           : restrictTo.intersection(allLanguages);
      57              : 
      58              :   final Set<String> _candidates;
      59              : 
      60            1 :   @override
      61            1 :   Set<String> get supportedLanguages => _candidates;
      62              : 
      63            2 :   @override
      64              :   List<LanguageGuess> score(String text) {
      65            2 :     final scan = scanScript(text);
      66            2 :     final scriptGuess = _scriptStage(scan);
      67            1 :     if (scriptGuess != null) return [scriptGuess];
      68            4 :     return _ngramStage(text, scan.dominantScript);
      69              :   }
      70              : 
      71            2 :   LanguageGuess? _scriptStage(ScriptScan scan) {
      72            6 :     if (scan.hasKana && _candidates.contains('ja')) {
      73              :       return const LanguageGuess('ja', 1.0);
      74              :     }
      75              : 
      76            2 :     final script = scan.dominantScript;
      77              :     if (script == null) return null;
      78              : 
      79            4 :     if (script == hanScript && _candidates.contains('zh')) {
      80              :       return const LanguageGuess('zh', 1.0);
      81              :     }
      82              : 
      83            2 :     final exclusive = scriptExclusiveLanguage[script];
      84            2 :     if (exclusive != null && _candidates.contains(exclusive)) {
      85            1 :       return LanguageGuess(exclusive, 1.0);
      86              :     }
      87              : 
      88              :     return null;
      89              :   }
      90              : 
      91            2 :   List<LanguageGuess> _ngramStage(String text, String? script) {
      92            2 :     final scriptCandidateSet = script == null ? null : scriptCandidates[script];
      93              :     final narrowed = scriptCandidateSet == null
      94            2 :         ? _candidates
      95            4 :         : _candidates.intersection(scriptCandidateSet);
      96              : 
      97              :     // No explicit "fewer than 2 candidates" guard here: NgramBackend already
      98              :     // handles both edge cases correctly on its own — an empty profile map
      99              :     // scores to `[]` (see NgramBackend.score's `_profiles.isEmpty` check),
     100              :     // and a single-entry map trivially scores that one candidate at
     101              :     // confidence 1.0 (there is nothing to compare against, so it "wins" by
     102              :     // definition — the same documented behaviour NgramBackend already
     103              :     // guarantees for any single-candidate set, restrictTo-narrowed or not).
     104            2 :     final profiles = {
     105            2 :       for (final code in narrowed)
     106            6 :         if (ngramProfiles.containsKey(code)) code: ngramProfiles[code]!,
     107              :     };
     108              : 
     109            4 :     return NgramBackend(profiles).score(text);
     110              :   }
     111              : }
        

Generated by: LCOV version 2.0-1