<commit_before>/* This file is a part of @mdn/browser-compat-data
 * See LICENSE file for more information. */

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import compareVersions from 'compare-versions';
import esMain from 'es-main';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import bcd from '../index.js';
const { browsers } = bcd;

const dirname = fileURLToPath(new URL('.', import.meta.url));

/**
 * @typedef {import('../types').Identifier} Identifier
 * @typedef {import('../types').SupportStatement} SupportStatement
 * @typedef {import('../types').ReleaseStatement} ReleaseStatement
 */

/**
 * @param {string} targetBrowser
 * @param {string} sourceBrowser
 * @param {string} sourceVersion
 * @returns {ReleaseStatement|boolean}
 */
const getMatchingBrowserVersion = (targetBrowser, source) => {
  const { browser: sourceBrowser, version: sourceVersion } = source;

  const range = sourceVersion.includes('≤');
  const sourceRelease =
    browsers[sourceBrowser].releases[sourceVersion.replace('≤', '')];

  const browserData = browsers[targetBrowser];
  const releaseKeys = Object.keys(browserData.releases);
  releaseKeys.sort(compareVersions);

  for (const r of releaseKeys) {
    const release = browserData.releases[r];
    if (
      ['opera', 'opera_android', 'samsunginternet_android'].includes(
        targetBrowser,
      ) &&
      release.engine == 'Blink' &&
      sourceRelease.engine == 'WebKit'
    ) {
      return range ? `≤${r}` : r;
    } else if (release.engine == sourceRelease.engine) {
      if (
        ['beta', 'nightly'].includes(release.status) &&
        release.status == sourceRelease.status
      ) {
        return r;
      } else if (
        release.engine_version &&
        sourceRelease.engine_version &&
        compareVersions.compare(
          release.engine_version,
          sourceRelease.engine_version,
          '>=',
        )
      ) {
        return r;
      }
    }
  }

  return false;
};

/**
 * @param {string} browser
 * @returns {string}
 */
const getSource = (browser) => {
  switch (browser) {
    case 'chrome_android':
    case 'edge':
    case 'opera':
      return 'chrome';
    case 'opera_android':
    case 'samsunginternet_android':
    case 'webview_android':
      return 'chrome_android';
    case 'firefox_android':
      return 'firefox';
    case 'safari_ios':
      return 'safari';
    default:
      throw Error(`${browser} cannot be used as a mirroring destination.`);
  }
};

/**
 * @param {SupportStatement} compatData
 * @param {string | null} versionToCheck
 * @return {string | null}
 */
const isVersionAdded = (compatData, versionToCheck) => {
  if (Array.isArray(compatData)) {
    return compatData.some((s) => s.version_added == versionToCheck);
  }

  if (typeof compatData.version_added === 'string') {
    return compatData.version_added == versionToCheck;
  }

  return false;
};

/**
 * @param {SupportStatement} compatData
 * @param {string | null} versionToCheck
 * @return {string | null}
 */
const isVersionRemoved = (compatData, versionToCheck) => {
  if (typeof compatData.version_removed === 'string')
    return compatData.version_removed == versionToCheck;

  if (compatData.constructor === Array) {
    return compatData.some((s) => s.version_removed === versionToCheck);
  }

  return false;
};

/**
 * @param {string|string[]|null} notes1
 * @param {string|string[]|null} notes2
 * @returns {string|string[]|null}
 */
const combineNotes = (notes1, notes2) => {
  let newNotes = [];

  if (notes1) {
    if (typeof notes1 === 'string') {
      newNotes.push(notes1);
    } else {
      newNotes.push(...notes1);
    }
  }

  if (notes2) {
    if (typeof notes2 === 'string') {
      newNotes.push(notes2);
    } else {
      newNotes.push(...notes2);
    }
  }

  newNotes = newNotes.filter((item, pos) => newNotes.indexOf(item) == pos);

  if (newNotes.length == 0) {
    return null;
  }
  if (newNotes.length == 1) {
    return newNotes[0];
  }

  return newNotes;
};

/**
 * @param {string|string[]|null} notes
 * @param {RegExp} regex
 * @param {string} replace
 * @returns {string|string[]|null}
 */
const updateNotes = (notes, regex, replace) => {
  if (notes === null || notes === undefined) {
    return null;
  }

  if (typeof notes === 'string') {
    return notes.replace(regex, replace);
  }

  let newNotes = [];
  for (let note of notes) {
    newNotes.push(note.replace(regex, replace));
  }
  return newNotes;
};

/**
 * @param {SupportStatement} data
 * @returns {SupportStatement}
 */
const copyStatement = (data) => {
  let newData = {};
  for (let i in data) {
    newData[i] = data[i];
  }

  return newData;
};

/**
 * @param {...SupportStatement} data
 * @returns {SupportStatement}
 */
const combineStatements = (...data) => {
  const ignoredKeys = ['version_added', 'notes'];

  const flattenedData = data.flat(2);
  const sections = {};
  let newData = [];

  for (const d of flattenedData) {
    const key = Object.keys(d)
      .filter((k) => !ignoredKeys.includes(k))
      .join('');
    if (!(key in sections)) sections[key] = [];
    sections[key].push(d);
  }

  for (const k of Object.keys(sections)) {
    const currentStatement = sections[k][0];

    if (sections[k].length == 1) {
      newData.push(currentStatement);
      continue;
    }

    for (const i in sections[k]) {
      if (i === 0) continue;
      const newStatement = sections[k][i];

      const currentVA = currentStatement.version_added;
      const newVA = newStatement.version_added;

      if (newVA === false) {
        // Ignore statements with version_added being false
        continue;
      } else if (typeof newVA === 'string') {
        if (
          typeof currentVA !== 'string' ||
          compareVersions.compare(
            currentVA.replace('≤', ''),
            newVA.replace('≤', ''),
            '>',
          )
        ) {
          currentStatement.version_added = newVA;
        }
      }

      const newNotes = combineNotes(currentStatement.notes, newStatement.notes);
      if (newNotes) currentStatement.notes = newNotes;
    }

    if ('notes' in currentStatement && !currentStatement.notes) {
      delete currentStatement.notes;
    }
    newData.push(currentStatement);
  }

  if (newData.length === 1) {
    return newData[0];
  }

  // Remove duplicate statements and statements that are only version_added = false
  newData = newData
    .filter((item, pos) => newData.indexOf(item) == pos)
    .filter((item) => item.version_added);

  switch (newData.length) {
    case 0:
      return { version_added: false };

    case 1:
      return newData[0];

    default:
      return newData;
  }
};

/**
 * @param {SupportStatement} sourceData
 * @param {string} targetBrowser
 * @param {string} sourceBrowser
 * @param {Array.<RegExp, string>} notesRepl
 * @returns {SupportStatement}
 */
const bumpGeneric = (sourceData, targetBrowser, sourceBrowser, notesRepl) => {
  let newData = copyStatement(sourceData);

  if (typeof sourceData.version_added === 'string') {
    newData.version_added = getMatchingBrowserVersion(targetBrowser, {
      browser: sourceBrowser,
      version: sourceData.version_added,
    });
  }

  if (
    sourceData.version_removed &&
    typeof sourceData.version_removed === 'string'
  ) {
    newData.version_removed = getMatchingBrowserVersion(targetBrowser, {
      browser: sourceBrowser,
      version: sourceData.version_removed,
    });
  }

  if (notesRepl && typeof sourceData.notes === 'string') {
    newData.notes = updateNotes(sourceData.notes, notesRepl[0], notesRepl[1]);
  }

  return newData;
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpChromeAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'chrome_android', 'chrome');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpEdge = (sourceData) => {
  if (
    typeof sourceData.version_removed === 'string' &&
    compareVersions.compare(sourceData.version_removed, '79', '<=')
  ) {
    // If this feature was removed before Chrome 79, it's not present in Chromium Edge
    return { version_added: false };
  }

  return bumpGeneric(sourceData, 'edge', 'chrome', [/Chrome/g, 'Edge']);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpFirefoxAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'firefox_android', 'firefox');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpOpera = (sourceData) => {
  return bumpGeneric(sourceData, 'opera', 'chrome', [/Chrome/g, 'Opera']);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpOperaAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'opera_android', 'chrome_android', [
    /Chrome/g,
    'Opera',
  ]);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpSafariiOS = (sourceData) => {
  return bumpGeneric(sourceData, 'safari_ios', 'safari');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpSamsungInternet = (sourceData) => {
  return bumpGeneric(sourceData, 'samsunginternet_android', 'chrome_android', [
    /Chrome/g,
    'Samsung Internet',
  ]);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpWebView = (sourceData) => {
  let newData = copyStatement(sourceData);

  const createWebViewRange = (version) => {
    if (Number(version) <= 18) {
      return '1';
    } else if (Number(version) > 18 && Number(version) < 30) {
      return '≤37';
    } else if (Number(version) >= 30 && Number(version) < 33) {
      return '4.4';
    } else if (Number(version) >= 33 && Number(version) < 37) {
      return '4.4.3';
    } else {
      return version;
    }
  };

  if (typeof sourceData.version_added === 'string') {
    newData.version_added = createWebViewRange(sourceData.version_added);
  }

  if (
    sourceData.version_removed &&
    typeof sourceData.version_removed === 'string'
  ) {
    newData.version_removed = createWebViewRange(sourceData.version_removed);
  }

  if (typeof sourceData.notes === 'string') {
    newData.notes = updateNotes(sourceData.notes, /Chrome/g, 'WebView');
  }

  return newData;
};

/**
 * @param {SupportStatement} data
 * @param {string} destination
 * @param {string} targetVersion
 */<commit_msg>Mirroring script: improve note mirroring (#16379)<commit_after>/* This file is a part of @mdn/browser-compat-data
 * See LICENSE file for more information. */

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import compareVersions from 'compare-versions';
import esMain from 'es-main';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import bcd from '../index.js';
const { browsers } = bcd;

const dirname = fileURLToPath(new URL('.', import.meta.url));

/**
 * @typedef {import('../types').Identifier} Identifier
 * @typedef {import('../types').SupportStatement} SupportStatement
 * @typedef {import('../types').ReleaseStatement} ReleaseStatement
 */

/**
 * @param {string} targetBrowser
 * @param {string} sourceBrowser
 * @param {string} sourceVersion
 * @returns {ReleaseStatement|boolean}
 */
const getMatchingBrowserVersion = (targetBrowser, source) => {
  const { browser: sourceBrowser, version: sourceVersion } = source;

  const range = sourceVersion.includes('≤');
  const sourceRelease =
    browsers[sourceBrowser].releases[sourceVersion.replace('≤', '')];

  const browserData = browsers[targetBrowser];
  const releaseKeys = Object.keys(browserData.releases);
  releaseKeys.sort(compareVersions);

  for (const r of releaseKeys) {
    const release = browserData.releases[r];
    if (
      ['opera', 'opera_android', 'samsunginternet_android'].includes(
        targetBrowser,
      ) &&
      release.engine == 'Blink' &&
      sourceRelease.engine == 'WebKit'
    ) {
      return range ? `≤${r}` : r;
    } else if (release.engine == sourceRelease.engine) {
      if (
        ['beta', 'nightly'].includes(release.status) &&
        release.status == sourceRelease.status
      ) {
        return r;
      } else if (
        release.engine_version &&
        sourceRelease.engine_version &&
        compareVersions.compare(
          release.engine_version,
          sourceRelease.engine_version,
          '>=',
        )
      ) {
        return r;
      }
    }
  }

  return false;
};

/**
 * @param {string} browser
 * @returns {string}
 */
const getSource = (browser) => {
  switch (browser) {
    case 'chrome_android':
    case 'edge':
    case 'opera':
      return 'chrome';
    case 'opera_android':
    case 'samsunginternet_android':
    case 'webview_android':
      return 'chrome_android';
    case 'firefox_android':
      return 'firefox';
    case 'safari_ios':
      return 'safari';
    default:
      throw Error(`${browser} cannot be used as a mirroring destination.`);
  }
};

/**
 * @param {SupportStatement} compatData
 * @param {string | null} versionToCheck
 * @return {string | null}
 */
const isVersionAdded = (compatData, versionToCheck) => {
  if (Array.isArray(compatData)) {
    return compatData.some((s) => s.version_added == versionToCheck);
  }

  if (typeof compatData.version_added === 'string') {
    return compatData.version_added == versionToCheck;
  }

  return false;
};

/**
 * @param {SupportStatement} compatData
 * @param {string | null} versionToCheck
 * @return {string | null}
 */
const isVersionRemoved = (compatData, versionToCheck) => {
  if (typeof compatData.version_removed === 'string')
    return compatData.version_removed == versionToCheck;

  if (compatData.constructor === Array) {
    return compatData.some((s) => s.version_removed === versionToCheck);
  }

  return false;
};

/**
 * @param {string|string[]|null} notes1
 * @param {string|string[]|null} notes2
 * @returns {string|string[]|null}
 */
const combineNotes = (notes1, notes2) => {
  let newNotes = [];

  if (notes1) {
    if (typeof notes1 === 'string') {
      newNotes.push(notes1);
    } else {
      newNotes.push(...notes1);
    }
  }

  if (notes2) {
    if (typeof notes2 === 'string') {
      newNotes.push(notes2);
    } else {
      newNotes.push(...notes2);
    }
  }

  newNotes = newNotes.filter((item, pos) => newNotes.indexOf(item) == pos);

  if (newNotes.length == 0) {
    return null;
  }
  if (newNotes.length == 1) {
    return newNotes[0];
  }

  return newNotes;
};

/**
 * @param {string|string[]|null} notes
 * @param {RegExp} regex
 * @param {string} replace
 * @param {Function} versionMapper - Receives the source browser version and returns the target browser version.
 * @returns {string|string[]|null}
 */
const updateNotes = (notes, regex, replace, versionMapper) => {
  if (!notes) {
    return null;
  }

  if (Array.isArray(notes)) {
    return notes.map((note) =>
      updateNotes(note, regex, replace, versionMapper),
    );
  }

  return notes
    .replace(regex, replace)
    .replace(
      new RegExp(`(?:${replace}|version)\\s(\\d+)`),
      (match, p1) => replace + ' ' + versionMapper(p1),
    );
};

/**
 * @param {SupportStatement} data
 * @returns {SupportStatement}
 */
const copyStatement = (data) => {
  let newData = {};
  for (let i in data) {
    newData[i] = data[i];
  }

  return newData;
};

/**
 * @param {...SupportStatement} data
 * @returns {SupportStatement}
 */
const combineStatements = (...data) => {
  const ignoredKeys = ['version_added', 'notes'];

  const flattenedData = data.flat(2);
  const sections = {};
  let newData = [];

  for (const d of flattenedData) {
    const key = Object.keys(d)
      .filter((k) => !ignoredKeys.includes(k))
      .join('');
    if (!(key in sections)) sections[key] = [];
    sections[key].push(d);
  }

  for (const k of Object.keys(sections)) {
    const currentStatement = sections[k][0];

    if (sections[k].length == 1) {
      newData.push(currentStatement);
      continue;
    }

    for (const i in sections[k]) {
      if (i === 0) continue;
      const newStatement = sections[k][i];

      const currentVA = currentStatement.version_added;
      const newVA = newStatement.version_added;

      if (newVA === false) {
        // Ignore statements with version_added being false
        continue;
      } else if (typeof newVA === 'string') {
        if (
          typeof currentVA !== 'string' ||
          compareVersions.compare(
            currentVA.replace('≤', ''),
            newVA.replace('≤', ''),
            '>',
          )
        ) {
          currentStatement.version_added = newVA;
        }
      }

      const newNotes = combineNotes(currentStatement.notes, newStatement.notes);
      if (newNotes) currentStatement.notes = newNotes;
    }

    if ('notes' in currentStatement && !currentStatement.notes) {
      delete currentStatement.notes;
    }
    newData.push(currentStatement);
  }

  if (newData.length === 1) {
    return newData[0];
  }

  // Remove duplicate statements and statements that are only version_added = false
  newData = newData
    .filter((item, pos) => newData.indexOf(item) == pos)
    .filter((item) => item.version_added);

  switch (newData.length) {
    case 0:
      return { version_added: false };

    case 1:
      return newData[0];

    default:
      return newData;
  }
};

/**
 * @param {SupportStatement} sourceData
 * @param {string} targetBrowser
 * @param {string} sourceBrowser
 * @param {Array.<RegExp, string>} notesRepl
 * @returns {SupportStatement}
 */
const bumpGeneric = (sourceData, targetBrowser, sourceBrowser, notesRepl) => {
  let newData = copyStatement(sourceData);

  if (typeof sourceData.version_added === 'string') {
    newData.version_added = getMatchingBrowserVersion(targetBrowser, {
      browser: sourceBrowser,
      version: sourceData.version_added,
    });
  }

  if (
    sourceData.version_removed &&
    typeof sourceData.version_removed === 'string'
  ) {
    newData.version_removed = getMatchingBrowserVersion(targetBrowser, {
      browser: sourceBrowser,
      version: sourceData.version_removed,
    });
  }

  if (notesRepl && sourceData.notes) {
    newData.notes = updateNotes(
      sourceData.notes,
      notesRepl[0],
      notesRepl[1],
      (v) =>
        getMatchingBrowserVersion(targetBrowser, {
          browser: sourceBrowser,
          version: v,
        }),
    );
  }

  return newData;
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpChromeAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'chrome_android', 'chrome');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpEdge = (sourceData) => {
  if (
    typeof sourceData.version_removed === 'string' &&
    compareVersions.compare(sourceData.version_removed, '79', '<=')
  ) {
    // If this feature was removed before Chrome 79, it's not present in Chromium Edge
    return { version_added: false };
  }

  return bumpGeneric(sourceData, 'edge', 'chrome', [/Chrome/g, 'Edge']);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpFirefoxAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'firefox_android', 'firefox');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpOpera = (sourceData) => {
  return bumpGeneric(sourceData, 'opera', 'chrome', [/Chrome/g, 'Opera']);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpOperaAndroid = (sourceData) => {
  return bumpGeneric(sourceData, 'opera_android', 'chrome_android', [
    /Chrome/g,
    'Opera',
  ]);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpSafariiOS = (sourceData) => {
  return bumpGeneric(sourceData, 'safari_ios', 'safari');
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpSamsungInternet = (sourceData) => {
  return bumpGeneric(sourceData, 'samsunginternet_android', 'chrome_android', [
    /Chrome/g,
    'Samsung Internet',
  ]);
};

/**
 * @param {SupportStatement} sourceData
 * @returns {SupportStatement}
 */
const bumpWebView = (sourceData) => {
  let newData = copyStatement(sourceData);

  const createWebViewRange = (version) => {
    if (Number(version) <= 18) {
      return '1';
    } else if (Number(version) > 18 && Number(version) < 30) {
      return '≤37';
    } else if (Number(version) >= 30 && Number(version) < 33) {
      return '4.4';
    } else if (Number(version) >= 33 && Number(version) < 37) {
      return '4.4.3';
    } else {
      return version;
    }
  };

  if (typeof sourceData.version_added === 'string') {
    newData.version_added = createWebViewRange(sourceData.version_added);
  }

  if (
    sourceData.version_removed &&
    typeof sourceData.version_removed === 'string'
  ) {
    newData.version_removed = createWebViewRange(sourceData.version_removed);
  }

  if (sourceData.notes) {
    newData.notes = updateNotes(
      sourceData.notes,
      /Chrome/g,
      'WebView',
      createWebViewRange,
    );
  }

  return newData;
};

/**
 * @param {SupportStatement} data
 * @param {string} destination
 * @param {string} targetVersion
 */
{'fraction_non_alphanumeric': 0.1492878823254728, 'fraction_numerical': 0.005183282745738968, 'mean_word_length': 3.265285799641506, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 66, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1409970', 'n_tokens_mistral': 6786, 'n_tokens_neox': 6598, 'n_words': 1517}