"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getComputedFluidTypographyValue = getComputedFluidTypographyValue;
exports.getTypographyValueAndUnit = getTypographyValueAndUnit;
exports.roundToPrecision = roundToPrecision;
/**
 * The fluid utilities must match the backend equivalent.
 * See: gutenberg_get_typography_font_size_value() in lib/block-supports/typography.php
 * ---------------------------------------------------------------
 */

// Defaults.
const DEFAULT_MAXIMUM_VIEWPORT_WIDTH = '1600px';
const DEFAULT_MINIMUM_VIEWPORT_WIDTH = '320px';
const DEFAULT_SCALE_FACTOR = 1;
const DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MIN = 0.25;
const DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MAX = 0.75;
const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px';

/**
 * Computes a fluid font-size value that uses clamp(). A minimum and maximum
 * font size OR a single font size can be specified.
 *
 * If a single font size is specified, it is scaled up and down using a logarithmic scale.
 *
 * @example
 * ```js
 * // Calculate fluid font-size value from a minimum and maximum value.
 * const fontSize = getComputedFluidTypographyValue( {
 *     minimumFontSize: '20px',
 *     maximumFontSize: '45px'
 * } );
 * // Calculate fluid font-size value from a single font size.
 * const fontSize = getComputedFluidTypographyValue( {
 *     fontSize: '30px',
 * } );
 * ```
 *
 * @param {Object}        args
 * @param {?string}       args.minimumViewportWidth Minimum viewport size from which type will have fluidity. Optional if fontSize is specified.
 * @param {?string}       args.maximumViewportWidth Maximum size up to which type will have fluidity. Optional if fontSize is specified.
 * @param {string|number} [args.fontSize]           Size to derive maximumFontSize and minimumFontSize from, if necessary. Optional if minimumFontSize and maximumFontSize are specified.
 * @param {?string}       args.maximumFontSize      Maximum font size for any clamp() calculation. Optional.
 * @param {?string}       args.minimumFontSize      Minimum font size for any clamp() calculation. Optional.
 * @param {?number}       args.scaleFactor          A scale factor to determine how fast a font scales within boundaries. Optional.
 * @param {?string}       args.minimumFontSizeLimit The smallest a calculated font size may be. Optional.
 *
 * @return {string|null} A font-size value using clamp().
 */
function getComputedFluidTypographyValue({
  minimumFontSize,
  maximumFontSize,
  fontSize,
  minimumViewportWidth = DEFAULT_MINIMUM_VIEWPORT_WIDTH,
  maximumViewportWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH,
  scaleFactor = DEFAULT_SCALE_FACTOR,
  minimumFontSizeLimit
}) {
  // Validate incoming settings and set defaults.
  minimumFontSizeLimit = !!getTypographyValueAndUnit(minimumFontSizeLimit) ? minimumFontSizeLimit : DEFAULT_MINIMUM_FONT_SIZE_LIMIT;

  /*
   * Calculates missing minimumFontSize and maximumFontSize from
   * defaultFontSize if provided.
   */
  if (fontSize) {
    // Parses default font size.
    const fontSizeParsed = getTypographyValueAndUnit(fontSize);

    // Protect against invalid units.
    if (!fontSizeParsed?.unit) {
      return null;
    }

    // Parses the minimum font size limit, so we can perform checks using it.
    const minimumFontSizeLimitParsed = getTypographyValueAndUnit(minimumFontSizeLimit, {
      coerceTo: fontSizeParsed.unit
    });

    // Don't enforce minimum font size if a font size has explicitly set a min and max value.
    if (!!minimumFontSizeLimitParsed?.value && !minimumFontSize && !maximumFontSize) {
      /*
       * If a minimum size was not passed to this function
       * and the user-defined font size is lower than $minimum_font_size_limit,
       * do not calculate a fluid value.
       */
      if (fontSizeParsed?.value <= minimumFontSizeLimitParsed?.value) {
        return null;
      }
    }

    // If no fluid max font size is available use the incoming value.
    if (!maximumFontSize) {
      maximumFontSize = `${fontSizeParsed.value}${fontSizeParsed.unit}`;
    }

    /*
     * If no minimumFontSize is provided, create one using
     * the given font size multiplied by the min font size scale factor.
     */
    if (!minimumFontSize) {
      const fontSizeValueInPx = fontSizeParsed.unit === 'px' ? fontSizeParsed.value : fontSizeParsed.value * 16;

      /*
       * The scale factor is a multiplier that affects how quickly the curve will move towards the minimum,
       * that is, how quickly the size factor reaches 0 given increasing font size values.
       * For a - b * log2(), lower values of b will make the curve move towards the minimum faster.
       * The scale factor is constrained between min and max values.
       */
      const minimumFontSizeFactor = Math.min(Math.max(1 - 0.075 * Math.log2(fontSizeValueInPx), DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MIN), DEFAULT_MINIMUM_FONT_SIZE_FACTOR_MAX);

      // Calculates the minimum font size.
      const calculatedMinimumFontSize = roundToPrecision(fontSizeParsed.value * minimumFontSizeFactor, 3);

      // Only use calculated min font size if it's > $minimum_font_size_limit value.
      if (!!minimumFontSizeLimitParsed?.value && calculatedMinimumFontSize < minimumFontSizeLimitParsed?.value) {
        minimumFontSize = `${minimumFontSizeLimitParsed.value}${minimumFontSizeLimitParsed.unit}`;
      } else {
        minimumFontSize = `${calculatedMinimumFontSize}${fontSizeParsed.unit}`;
      }
    }
  }

  // Grab the minimum font size and normalize it in order to use the value for calculations.
  const minimumFontSizeParsed = getTypographyValueAndUnit(minimumFontSize);

  // We get a 'preferred' unit to keep units consistent when calculating,
  // otherwise the result will not be accurate.
  const fontSizeUnit = minimumFontSizeParsed?.unit || 'rem';

  // Grabs the maximum font size and normalize it in order to use the value for calculations.
  const maximumFontSizeParsed = getTypographyValueAndUnit(maximumFontSize, {
    coerceTo: fontSizeUnit
  });

  // Checks for mandatory min and max sizes, and protects against unsupported units.
  if (!minimumFontSizeParsed || !maximumFontSizeParsed) {
    return null;
  }

  // Uses rem for accessible fluid target font scaling.
  const minimumFontSizeRem = getTypographyValueAndUnit(minimumFontSize, {
    coerceTo: 'rem'
  });

  // Viewport widths defined for fluid typography. Normalize units
  const maximumViewportWidthParsed = getTypographyValueAndUnit(maximumViewportWidth, {
    coerceTo: fontSizeUnit
  });
  const minimumViewportWidthParsed = getTypographyValueAndUnit(minimumViewportWidth, {
    coerceTo: fontSizeUnit
  });

  // Protect against unsupported units.
  if (!maximumViewportWidthParsed || !minimumViewportWidthParsed || !minimumFontSizeRem) {
    return null;
  }

  // Calculates the linear factor denominator. If it's 0, we cannot calculate a fluid value.
  const linearDenominator = maximumViewportWidthParsed.value - minimumViewportWidthParsed.value;
  if (!linearDenominator) {
    return null;
  }

  // Build CSS rule.
  // Borrowed from https://websemantics.uk/tools/responsive-font-calculator/.
  const minViewportWidthOffsetValue = roundToPrecision(minimumViewportWidthParsed.value / 100, 3);
  const viewportWidthOffset = roundToPrecision(minViewportWidthOffsetValue, 3) + fontSizeUnit;
  const linearFactor = 100 * ((maximumFontSizeParsed.value - minimumFontSizeParsed.value) / linearDenominator);
  const linearFactorScaled = roundToPrecision((linearFactor || 1) * scaleFactor, 3);
  const fluidTargetFontSize = `${minimumFontSizeRem.value}${minimumFontSizeRem.unit} + ((1vw - ${viewportWidthOffset}) * ${linearFactorScaled})`;
  return `clamp(${minimumFontSize}, ${fluidTargetFontSize}, ${maximumFontSize})`;
}

/**
 * Internal method that checks a string for a unit and value and returns an array consisting of `'value'` and `'unit'`, e.g., [ '42', 'rem' ].
 * A raw font size of `value + unit` is expected. If the value is an integer, it will convert to `value + 'px'`.
 *
 * @param {string|number}    rawValue Raw size value from theme.json.
 * @param {Object|undefined} options  Calculation options.
 *
 * @return {{ unit: string, value: number }|null} An object consisting of `'value'` and `'unit'` properties.
 */
function getTypographyValueAndUnit(rawValue, options = {}) {
  if (typeof rawValue !== 'string' && typeof rawValue !== 'number') {
    return null;
  }

  // Converts numeric values to pixel values by default.
  if (isFinite(rawValue)) {
    rawValue = `${rawValue}px`;
  }
  const {
    coerceTo,
    rootSizeValue,
    acceptableUnits
  } = {
    coerceTo: '',
    // Default browser font size. Later we could inject some JS to compute this `getComputedStyle( document.querySelector( "html" ) ).fontSize`.
    rootSizeValue: 16,
    acceptableUnits: ['rem', 'px', 'em'],
    ...options
  };
  const acceptableUnitsGroup = acceptableUnits?.join('|');
  const regexUnits = new RegExp(`^(\\d*\\.?\\d+)(${acceptableUnitsGroup}){1,1}$`);
  const matches = rawValue.match(regexUnits);

  // We need a number value and a unit.
  if (!matches || matches.length < 3) {
    return null;
  }
  let [, value, unit] = matches;
  let returnValue = parseFloat(value);
  if ('px' === coerceTo && ('em' === unit || 'rem' === unit)) {
    returnValue = returnValue * rootSizeValue;
    unit = coerceTo;
  }
  if ('px' === unit && ('em' === coerceTo || 'rem' === coerceTo)) {
    returnValue = returnValue / rootSizeValue;
    unit = coerceTo;
  }

  /*
   * No calculation is required if swapping between em and rem yet,
   * since we assume a root size value. Later we might like to differentiate between
   * :root font size (rem) and parent element font size (em) relativity.
   */
  if (('em' === coerceTo || 'rem' === coerceTo) && ('em' === unit || 'rem' === unit)) {
    unit = coerceTo;
  }
  return {
    value: roundToPrecision(returnValue, 3),
    unit
  };
}

/**
 * Returns a value rounded to defined precision.
 * Returns `undefined` if the value is not a valid finite number.
 *
 * @param {number} value  Raw value.
 * @param {number} digits The number of digits to appear after the decimal point
 *
 * @return {number|undefined} Value rounded to standard precision.
 */
function roundToPrecision(value, digits = 3) {
  const base = Math.pow(10, digits);
  return Number.isFinite(value) ? parseFloat(Math.round(value * base) / base) : undefined;
}
//# sourceMappingURL=fluid-utils.js.map