{"version":3,"sources":["../src/asYouType.ts","../src/metadata.ts"],"sourcesContent":["import { getMetadata, type FormatRule } from './metadata.js';\n\nconst SANITIZE_PATTERN = /[^\\d]/g;\n\n// Long enough that any real leadingDigits pattern can complete a full\n// match attempt against typed digits, however few there are.\nconst PREFIX_TEST_PADDING = '0'.repeat(20);\n\n// Tests whether `typed` (a prefix of an eventual number) is consistent\n// with `leadingDigitsPattern` - not the same as a normal regex test,\n// since `typed` may be too short for the pattern to match on its own yet.\n// Padding lets the pattern attempt a full match; because the regex is\n// anchored at the start and the padded string's first `typed.length`\n// characters are exactly what was typed, any successful match proves the\n// real typed digits are consistent with the pattern - whether it matched\n// entirely within them already, or needed padding to finish. A failed\n// match means the real digits already contradict the pattern.\nconst isPrefixCompatible = (typed: string, leadingDigitsPattern: string): boolean =>\n  new RegExp(`^(?:${leadingDigitsPattern})`).test(typed + PREFIX_TEST_PADDING);\n\n// Parses \"(\\d{3})(\\d{4})\" into fixed group lengths [3, 4]. Returns\n// undefined for patterns that don't decompose into simple fixed-length\n// digit groups - rather than guess, those rules just aren't used for\n// live formatting (falls back to plain digits if nothing else matches).\nconst parseGroupLengths = (pattern: string): number[] | undefined => {\n  const groups = [...pattern.matchAll(/\\(\\\\d\\{(\\d+)\\}\\)/g)];\n  return groups.length > 0 ? groups.map((g) => Number(g[1])) : undefined;\n};\n\n// Among rules consistent with the digits typed so far, prefers the one\n// with the LARGEST total capacity - not simply the first declared. e.g.\n// US has both a 7-digit local-only rule and the standard 10-digit rule;\n// a number starting with digits compatible with both should format\n// progressively as the 10-digit one (confirmed against a documented real\n// example: formatting \"2133734\" produces \"(213) 373-4\", the partial\n// 10-digit grouping, not the 7-digit dash format) - only falling back to\n// a shorter/more specific rule once it's the only one still compatible\n// (e.g. once more than 7 digits have been typed).\nconst findCandidateRule = (typedDigits: string, rules: FormatRule[]): FormatRule | undefined => {\n  let best: { rule: FormatRule; capacity: number } | undefined;\n\n  for (const rule of rules) {\n    const groupLengths = parseGroupLengths(rule.pattern);\n    if (!groupLengths) {\n      continue;\n    }\n\n    // Reject rules that can't hold what's already been typed -\n    // leadingDigits compatibility alone isn't enough, since a short\n    // leadingDigits pattern (e.g. \"310\") can be satisfied well before the\n    // digit count exceeds that rule's total capacity.\n    const capacity = groupLengths.reduce((sum, n) => sum + n, 0);\n    if (typedDigits.length > capacity) {\n      continue;\n    }\n\n    // A rule with no leadingDigits at all is always compatible (some\n    // rules don't have any). Otherwise, only the *last* entry is tested -\n    // upstream documents each as a stricter refinement of the one\n    // before it, not an independent alternative.\n    const leadingDigits = rule.leadingDigits;\n    const compatible =\n      !leadingDigits || leadingDigits.length === 0 || isPrefixCompatible(typedDigits, leadingDigits.at(-1) ?? '');\n    if (!compatible) {\n      continue;\n    }\n\n    if (!best || capacity > best.capacity) {\n      best = { rule, capacity };\n    }\n  }\n\n  return best?.rule;\n};\n\n// Splits a format template like \"($1) $2-$3\" into the literal text\n// around each placeholder: [\"(\", \") \", \"-\"] - index 0 is whatever\n// precedes $1, each following entry is what comes between that group and\n// the next.\nconst parseSeparators = (format: string): string[] => format.split(/\\$\\d/);\n\nconst buildLiveFormat = (typedDigits: string, rule: FormatRule, nationalPrefix: string | undefined): string => {\n  const groupLengths = parseGroupLengths(rule.pattern) ?? [];\n  const separators = parseSeparators(rule.format);\n\n  let result = separators[0] ?? '';\n  let remaining = typedDigits;\n\n  for (let i = 0; i < groupLengths.length && remaining.length > 0; i++) {\n    let chunk = remaining.slice(0, groupLengths[i]);\n    remaining = remaining.slice(groupLengths[i]);\n\n    if (i === 0 && rule.nationalPrefixFormattingRule) {\n      chunk = rule.nationalPrefixFormattingRule.replace('$NP', nationalPrefix ?? '').replace('$FG', chunk);\n    }\n\n    result += chunk;\n    if (remaining.length > 0) {\n      result += separators[i + 1] ?? '';\n    }\n  }\n\n  return result;\n};\n\n// A pure function, not a stateful class: called fresh with the full\n// accumulated input each time (like a controlled input's current value),\n// not incrementally fed one digit at a time with hidden state to manage.\n// Deletion needs no special handling as a result - calling this with a\n// shorter string just recomputes correctly, since there's nothing to\n// reset.\nexport const asYouType = (inputSoFar: string, region: string): string => {\n  const typedDigits = inputSoFar.replace(SANITIZE_PATTERN, '');\n  const metadata = getMetadata()[region];\n  const rules = metadata?.formats ?? [];\n\n  const rule = findCandidateRule(typedDigits, rules);\n  if (!rule) {\n    return typedDigits;\n  }\n\n  return buildLiveFormat(typedDigits, rule, metadata?.nationalPrefix);\n};\n","export interface TypePattern {\n  nationalNumberPattern: string;\n  possibleLengths: number[];\n}\n\nexport interface FormatRule {\n  // Full-match regex with capture groups, e.g. \"(\\d{3})(\\d{4})\".\n  pattern: string;\n  // Template using $1/$2/... for the capture groups, e.g. \"$1 $2\".\n  format: string;\n  // How the national prefix combines with the first group in NATIONAL\n  // format specifically, e.g. \"$NP$FG\" ($NP = nationalPrefix, $FG = the\n  // raw first group) - omitted means NATIONAL format has no prefix at all.\n  // Never applies to INTERNATIONAL format.\n  nationalPrefixFormattingRule?: string;\n  // Overrides `format` for INTERNATIONAL style specifically. The literal\n  // value \"NA\" means this rule should be skipped entirely for\n  // INTERNATIONAL (try the next matching rule) - e.g. local-only formats\n  // that omit the area code aren't valid once you need to dial\n  // internationally. Omitted (not \"NA\") means reuse `format` as-is.\n  intlFormat?: string;\n  // Upstream declaration order, least to most specific - each is a\n  // stricter refinement of the one before it, not an independent\n  // alternative. Only used by asYouType() to pick a candidate rule before\n  // enough digits exist for `pattern` to fully match; `format()` doesn't\n  // need this since it only ever formats complete numbers.\n  leadingDigits?: string[];\n}\n\nexport interface CountryMetadata {\n  // ISO 3166-1 alpha-2, e.g. \"US\". Kept on the value itself (not just as\n  // the object key) because callers group entries by calling code - once\n  // grouped, the original object key is gone.\n  region: string;\n  callingCode: string;\n  // General/fallback pattern - used directly when a type-specific match\n  // isn't available (e.g. a region without per-type data yet).\n  nationalNumberPattern: string;\n  possibleLengths: number[];\n  // Per-type patterns, each optional since not every region's upstream\n  // data distinguishes all of these (e.g. many small territories have no\n  // separate pager/uan/voicemail block at all).\n  types?: {\n    MOBILE?: TypePattern;\n    FIXED?: TypePattern;\n    TOLL_FREE?: TypePattern;\n    PREMIUM_RATE?: TypePattern;\n    SHARED_COST?: TypePattern;\n    PERSONAL_NUMBER?: TypePattern;\n    VOIP?: TypePattern;\n    PAGER?: TypePattern;\n    UAN?: TypePattern;\n    VOICEMAIL?: TypePattern;\n  };\n  // e.g. \"0\" for GB, \"1\" for NANP. Absent for regions with no national\n  // dialing prefix.\n  nationalPrefix?: string;\n  // In upstream declaration order - first full match against the national\n  // number wins, same resolution order as Google's own algorithm.\n  formats?: FormatRule[];\n}\n\nexport interface Metadata {\n  // Keyed by region, not calling code: a calling code can map to several\n  // regions (e.g. NANP's \"1\" covers US, Canada, and ~19 Caribbean\n  // territories), so calling code can't be a unique key.\n  [region: string]: CountryMetadata;\n}\n\n// Global state for injected metadata, initialized empty\nlet _metadata: Metadata = {};\n\nexport const setup = (config: { metadata: Metadata }) => {\n  _metadata = config.metadata;\n};\n\nexport const getMetadata = (): Metadata => _metadata;\n\n// These three all read whatever's currently injected via setup() - not\n// the full set of countries DialSense ships in data/, which this module\n// has no knowledge of at runtime. A country is only \"supported\" once its\n// metadata has actually been passed to setup().\nexport const getCountries = (): string[] => Object.keys(_metadata);\n\nexport const getCountryCallingCode = (country: string): number | undefined => {\n  const region = _metadata[country];\n  return region ? Number(region.callingCode) : undefined;\n};\n\nexport const isSupportedCountry = (country: string): boolean => country in _metadata;"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACsEA,IAAI,YAAsB,CAAC;AAMpB,IAAM,cAAc,MAAgB;;;AD1E3C,IAAM,mBAAmB;AAIzB,IAAM,sBAAsB,IAAI,OAAO,EAAE;AAWzC,IAAM,qBAAqB,CAAC,OAAe,yBACzC,IAAI,OAAO,OAAO,oBAAoB,GAAG,EAAE,KAAK,QAAQ,mBAAmB;AAM7E,IAAM,oBAAoB,CAAC,YAA0C;AACnE,QAAM,SAAS,CAAC,GAAG,QAAQ,SAAS,mBAAmB,CAAC;AACxD,SAAO,OAAO,SAAS,IAAI,OAAO,IAAI,CAAC,MAAM,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;AAC/D;AAWA,IAAM,oBAAoB,CAAC,aAAqB,UAAgD;AAC9F,MAAI;AAEJ,aAAW,QAAQ,OAAO;AACxB,UAAM,eAAe,kBAAkB,KAAK,OAAO;AACnD,QAAI,CAAC,cAAc;AACjB;AAAA,IACF;AAMA,UAAM,WAAW,aAAa,OAAO,CAAC,KAAK,MAAM,MAAM,GAAG,CAAC;AAC3D,QAAI,YAAY,SAAS,UAAU;AACjC;AAAA,IACF;AAMA,UAAM,gBAAgB,KAAK;AAC3B,UAAM,aACJ,CAAC,iBAAiB,cAAc,WAAW,KAAK,mBAAmB,aAAa,cAAc,GAAG,EAAE,KAAK,EAAE;AAC5G,QAAI,CAAC,YAAY;AACf;AAAA,IACF;AAEA,QAAI,CAAC,QAAQ,WAAW,KAAK,UAAU;AACrC,aAAO,EAAE,MAAM,SAAS;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO,MAAM;AACf;AAMA,IAAM,kBAAkB,CAAC,WAA6B,OAAO,MAAM,MAAM;AAEzE,IAAM,kBAAkB,CAAC,aAAqB,MAAkB,mBAA+C;AAC7G,QAAM,eAAe,kBAAkB,KAAK,OAAO,KAAK,CAAC;AACzD,QAAM,aAAa,gBAAgB,KAAK,MAAM;AAE9C,MAAI,SAAS,WAAW,CAAC,KAAK;AAC9B,MAAI,YAAY;AAEhB,WAAS,IAAI,GAAG,IAAI,aAAa,UAAU,UAAU,SAAS,GAAG,KAAK;AACpE,QAAI,QAAQ,UAAU,MAAM,GAAG,aAAa,CAAC,CAAC;AAC9C,gBAAY,UAAU,MAAM,aAAa,CAAC,CAAC;AAE3C,QAAI,MAAM,KAAK,KAAK,8BAA8B;AAChD,cAAQ,KAAK,6BAA6B,QAAQ,OAAO,kBAAkB,EAAE,EAAE,QAAQ,OAAO,KAAK;AAAA,IACrG;AAEA,cAAU;AACV,QAAI,UAAU,SAAS,GAAG;AACxB,gBAAU,WAAW,IAAI,CAAC,KAAK;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAQO,IAAM,YAAY,CAAC,YAAoB,WAA2B;AACvE,QAAM,cAAc,WAAW,QAAQ,kBAAkB,EAAE;AAC3D,QAAM,WAAW,YAAY,EAAE,MAAM;AACrC,QAAM,QAAQ,UAAU,WAAW,CAAC;AAEpC,QAAM,OAAO,kBAAkB,aAAa,KAAK;AACjD,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAEA,SAAO,gBAAgB,aAAa,MAAM,UAAU,cAAc;AACpE;","names":[]}