{"version":3,"file":"manipulator.js","names":[],"sources":["../types.ts","../manipulator.ts"],"sourcesContent":["import type {NumberFormatOptions} from '#packages/ecma402-abstract/types/number.js'\nimport {type NumberSkeletonToken} from '@formatjs/icu-skeleton-parser'\n\nexport interface ExtendedNumberFormatOptions extends NumberFormatOptions {\n  scale?: number\n}\n\nexport enum TYPE {\n  /**\n   * Raw text\n   */\n  literal,\n  /**\n   * Variable w/o any format, e.g `var` in `this is a {var}`\n   */\n  argument,\n  /**\n   * Variable w/ number format\n   */\n  number,\n  /**\n   * Variable w/ date format\n   */\n  date,\n  /**\n   * Variable w/ time format\n   */\n  time,\n  /**\n   * Variable w/ select format\n   */\n  select,\n  /**\n   * Variable w/ plural format\n   */\n  plural,\n  /**\n   * Only possible within plural argument.\n   * This is the `#` symbol that will be substituted with the count.\n   */\n  pound,\n  /**\n   * XML-like tag\n   */\n  tag,\n}\n\nexport enum SKELETON_TYPE {\n  number,\n  dateTime,\n}\n\nexport interface LocationDetails {\n  offset: number\n  line: number\n  column: number\n}\nexport interface Location {\n  start: LocationDetails\n  end: LocationDetails\n}\n\nexport interface BaseElement<T extends TYPE> {\n  type: T\n  value: string\n  location?: Location\n}\n\nexport type LiteralElement = BaseElement<TYPE.literal>\nexport type ArgumentElement = BaseElement<TYPE.argument>\nexport interface TagElement extends BaseElement<TYPE.tag> {\n  children: MessageFormatElement[]\n}\n\nexport interface SimpleFormatElement<\n  T extends TYPE,\n  S extends Skeleton,\n> extends BaseElement<T> {\n  style?: string | S | null\n}\n\nexport type NumberElement = SimpleFormatElement<TYPE.number, NumberSkeleton>\nexport type DateElement = SimpleFormatElement<TYPE.date, DateTimeSkeleton>\nexport type TimeElement = SimpleFormatElement<TYPE.time, DateTimeSkeleton>\n\nexport type ValidPluralRule =\n  | 'zero'\n  | 'one'\n  | 'two'\n  | 'few'\n  | 'many'\n  | 'other'\n  | string\n\nexport interface PluralOrSelectOption {\n  value: MessageFormatElement[]\n  location?: Location\n}\n\nexport interface SelectElement extends BaseElement<TYPE.select> {\n  options: Record<string, PluralOrSelectOption>\n}\n\nexport interface PluralElement extends BaseElement<TYPE.plural> {\n  options: Record<ValidPluralRule, PluralOrSelectOption>\n  offset: number\n  pluralType: Intl.PluralRulesOptions['type']\n}\n\nexport interface PoundElement {\n  type: TYPE.pound\n  location?: Location\n}\n\nexport type MessageFormatElement =\n  | ArgumentElement\n  | DateElement\n  | LiteralElement\n  | NumberElement\n  | PluralElement\n  | PoundElement\n  | SelectElement\n  | TagElement\n  | TimeElement\n\nexport interface NumberSkeleton {\n  type: SKELETON_TYPE.number\n  tokens: NumberSkeletonToken[]\n  location?: Location\n  parsedOptions: ExtendedNumberFormatOptions\n}\n\nexport interface DateTimeSkeleton {\n  type: SKELETON_TYPE.dateTime\n  pattern: string\n  location?: Location\n  parsedOptions: Intl.DateTimeFormatOptions\n}\n\nexport type Skeleton = NumberSkeleton | DateTimeSkeleton\n\n/**\n * Type Guards\n */\nexport function isLiteralElement(\n  el: MessageFormatElement\n): el is LiteralElement {\n  return el.type === TYPE.literal\n}\nexport function isArgumentElement(\n  el: MessageFormatElement\n): el is ArgumentElement {\n  return el.type === TYPE.argument\n}\nexport function isNumberElement(el: MessageFormatElement): el is NumberElement {\n  return el.type === TYPE.number\n}\nexport function isDateElement(el: MessageFormatElement): el is DateElement {\n  return el.type === TYPE.date\n}\nexport function isTimeElement(el: MessageFormatElement): el is TimeElement {\n  return el.type === TYPE.time\n}\nexport function isSelectElement(el: MessageFormatElement): el is SelectElement {\n  return el.type === TYPE.select\n}\nexport function isPluralElement(el: MessageFormatElement): el is PluralElement {\n  return el.type === TYPE.plural\n}\nexport function isPoundElement(el: MessageFormatElement): el is PoundElement {\n  return el.type === TYPE.pound\n}\nexport function isTagElement(el: MessageFormatElement): el is TagElement {\n  return el.type === TYPE.tag\n}\nexport function isNumberSkeleton(\n  el: NumberElement['style'] | Skeleton\n): el is NumberSkeleton {\n  return !!(el && typeof el === 'object' && el.type === SKELETON_TYPE.number)\n}\nexport function isDateTimeSkeleton(\n  el?: DateElement['style'] | TimeElement['style'] | Skeleton\n): el is DateTimeSkeleton {\n  return !!(el && typeof el === 'object' && el.type === SKELETON_TYPE.dateTime)\n}\n\nexport function createLiteralElement(value: string): LiteralElement {\n  return {\n    type: TYPE.literal,\n    value,\n  }\n}\n\nexport function createNumberElement(\n  value: string,\n  style?: string | null\n): NumberElement {\n  return {\n    type: TYPE.number,\n    value,\n    style,\n  }\n}\n","import {\n  isArgumentElement,\n  isDateElement,\n  isNumberElement,\n  isPluralElement,\n  isPoundElement,\n  isSelectElement,\n  isTagElement,\n  isTimeElement,\n  type MessageFormatElement,\n  type PluralElement,\n  type PluralOrSelectOption,\n  type SelectElement,\n  TYPE,\n} from '#packages/icu-messageformat-parser/types.js'\n\nfunction cloneDeep<T>(obj: T): T {\n  if (Array.isArray(obj)) {\n    // @ts-expect-error meh\n    return obj.map(cloneDeep)\n  }\n  if (obj !== null && typeof obj === 'object') {\n    // @ts-expect-error meh\n    return Object.keys(obj).reduce((cloned, k) => {\n      // @ts-expect-error meh\n      cloned[k] = cloneDeep(obj[k])\n      return cloned\n    }, {})\n  }\n  return obj\n}\n\n/**\n * Replace pound elements with number elements referencing the given variable.\n * This is needed when nesting plurals - the # in the outer plural should become\n * an explicit variable reference when nested inside another plural.\n * GH #4202\n */\nfunction replacePoundWithArgument(\n  ast: MessageFormatElement[],\n  variableName: string\n): MessageFormatElement[] {\n  return ast.map(el => {\n    if (isPoundElement(el)) {\n      // Replace # with {variableName, number}\n      return {\n        type: TYPE.number,\n        value: variableName,\n        style: null,\n        location: el.location,\n      }\n    }\n    if (isPluralElement(el) || isSelectElement(el)) {\n      // Recursively process options\n      const newOptions: Record<string, PluralOrSelectOption> = {}\n      for (const key of Object.keys(el.options)) {\n        newOptions[key] = {\n          value: replacePoundWithArgument(el.options[key].value, variableName),\n        }\n      }\n      return {...el, options: newOptions}\n    }\n    if (isTagElement(el)) {\n      return {\n        ...el,\n        children: replacePoundWithArgument(el.children, variableName),\n      }\n    }\n    return el\n  })\n}\n\nfunction hoistPluralOrSelectElement(\n  ast: MessageFormatElement[],\n  el: PluralElement | SelectElement,\n  positionToInject: number\n) {\n  // pull this out of the ast and move it to the top\n  const cloned = cloneDeep(el)\n  const {options} = cloned\n\n  // GH #4202: Check if there are other plural/select elements after this one\n  const afterElements = ast.slice(positionToInject + 1)\n  const hasSubsequentPluralOrSelect = afterElements.some(\n    isPluralOrSelectElement\n  )\n\n  cloned.options = Object.keys(options).reduce(\n    (all: Record<string, PluralOrSelectOption>, k) => {\n      let optionValue = options[k].value\n\n      // GH #4202: If there are subsequent plurals/selects and this is a plural,\n      // replace # with explicit variable reference to avoid ambiguity\n      if (hasSubsequentPluralOrSelect && isPluralElement(el)) {\n        optionValue = replacePoundWithArgument(optionValue, el.value)\n      }\n\n      const newValue = hoistSelectors([\n        ...ast.slice(0, positionToInject),\n        ...optionValue,\n        ...afterElements,\n      ])\n      all[k] = {\n        value: newValue,\n      }\n      return all\n    },\n    {}\n  )\n  return cloned\n}\n\nfunction isPluralOrSelectElement(\n  el: MessageFormatElement\n): el is PluralElement | SelectElement {\n  return isPluralElement(el) || isSelectElement(el)\n}\n\nfunction findPluralOrSelectElement(ast: MessageFormatElement[]): boolean {\n  return !!ast.find(el => {\n    if (isPluralOrSelectElement(el)) {\n      return true\n    }\n    if (isTagElement(el)) {\n      return findPluralOrSelectElement(el.children)\n    }\n    return false\n  })\n}\n\n/**\n * Hoist all selectors to the beginning of the AST & flatten the\n * resulting options. E.g:\n * \"I have {count, plural, one{a dog} other{many dogs}}\"\n * becomes \"{count, plural, one{I have a dog} other{I have many dogs}}\".\n * If there are multiple selectors, the order of which one is hoisted 1st\n * is non-deterministic.\n * The goal is to provide as many full sentences as possible since fragmented\n * sentences are not translator-friendly\n * @param ast AST\n */\nexport function hoistSelectors(\n  ast: MessageFormatElement[]\n): MessageFormatElement[] {\n  for (let i = 0; i < ast.length; i++) {\n    const el = ast[i]\n    if (isPluralOrSelectElement(el)) {\n      return [hoistPluralOrSelectElement(ast, el, i)]\n    }\n    if (isTagElement(el) && findPluralOrSelectElement([el])) {\n      throw new Error(\n        'Cannot hoist plural/select within a tag element. Please put the tag element inside each plural/select option'\n      )\n    }\n  }\n  return ast\n}\n\n/**\n * Collect all variables in an AST to Record<string, TYPE>\n * @param ast AST to collect variables from\n * @param vars Record of variable name to variable type\n */\nfunction collectVariables(\n  ast: MessageFormatElement[],\n  vars: Map<string, TYPE> = new Map<string, TYPE>()\n): void {\n  ast.forEach(el => {\n    if (\n      isArgumentElement(el) ||\n      isDateElement(el) ||\n      isTimeElement(el) ||\n      isNumberElement(el)\n    ) {\n      // If the variable was already registered as a plural/select, it's normal\n      // for it to also appear inside as number/date/time/argument — not a conflict.\n      if (vars.has(el.value)) {\n        const existingType = vars.get(el.value)!\n        if (\n          existingType !== el.type &&\n          existingType !== TYPE.plural &&\n          existingType !== TYPE.select\n        ) {\n          throw new Error(`Variable ${el.value} has conflicting types`)\n        }\n      } else {\n        vars.set(el.value, el.type)\n      }\n    }\n\n    if (isPluralElement(el) || isSelectElement(el)) {\n      vars.set(el.value, el.type)\n      Object.keys(el.options).forEach(k => {\n        collectVariables(el.options[k].value, vars)\n      })\n    }\n\n    if (isTagElement(el)) {\n      vars.set(el.value, el.type)\n      collectVariables(el.children, vars)\n    }\n  })\n}\n\ninterface IsStructurallySameResult {\n  error?: Error\n  success: boolean\n}\n\n/**\n * Check if 2 ASTs are structurally the same. This primarily means that\n * they have the same variables with the same type\n * @param a\n * @param b\n * @returns\n */\nexport function isStructurallySame(\n  a: MessageFormatElement[],\n  b: MessageFormatElement[]\n): IsStructurallySameResult {\n  const aVars = new Map<string, TYPE>()\n  const bVars = new Map<string, TYPE>()\n  collectVariables(a, aVars)\n  collectVariables(b, bVars)\n\n  if (aVars.size !== bVars.size) {\n    return {\n      success: false,\n      error: new Error(\n        `Different number of variables: [${Array.from(aVars.keys()).join(', ')}] vs [${Array.from(bVars.keys()).join(', ')}]`\n      ),\n    }\n  }\n\n  return Array.from(aVars.entries()).reduce<IsStructurallySameResult>(\n    (result, [key, type]) => {\n      if (!result.success) {\n        return result\n      }\n      const bType = bVars.get(key)\n      if (bType == null) {\n        return {\n          success: false,\n          error: new Error(`Missing variable ${key} in message`),\n        }\n      }\n      if (bType !== type) {\n        return {\n          success: false,\n          error: new Error(\n            `Variable ${key} has conflicting types: ${TYPE[type]} vs ${TYPE[bType]}`\n          ),\n        }\n      }\n      return result\n    },\n    {success: true}\n  )\n}\n"],"mappings":"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"}