HEX
Server: LiteSpeed
System: Linux eko108.isimtescil.net 4.18.0-477.21.1.lve.1.el8.x86_64 #1 SMP Tue Sep 5 23:08:35 UTC 2023 x86_64
User: uyarreklamcomtr (11202)
PHP: 7.4.33
Disabled: opcache_get_status
Upload Files
File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/utils.tar
Linter.ts000064400000010701151540735350006360 0ustar00/**
 * Based on work licensed under the BSD 3-Clause license.
 *
 * Copyright (c) 2017, glayzzle
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * * Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * * Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import { Engine } from 'php-parser'
import CodeMirror from 'codemirror'
import type { Block, Location, Node } from 'php-parser'

export interface Annotation {
	message: string
	severity: string
	from: CodeMirror.Position
	to: CodeMirror.Position
}

export interface Identifier extends Node {
	name: string
}

export interface Declaration extends Node {
	name: Identifier | string
}

export class Linter {
	private readonly code: string

	private readonly function_names: Set<string>

	private readonly class_names: Set<string>

	public readonly annotations: Annotation[]

	/**
	 * Constructor.
	 * @param code
	 */
	constructor(code: string) {
		this.code = code
		this.annotations = []

		this.function_names = new Set()
		this.class_names = new Set()
	}

	/**
	 * Lint the provided code.
	 */
	lint() {
		const parser = new Engine({
			parser: {
				suppressErrors: true,
				version: 800
			},
			ast: {
				withPositions: true
			}
		})

		try {
			const program = parser.parseEval(this.code)

			if (0 < program.errors.length) {
				for (const error of program.errors) {
					this.annotate(error.message, error.loc)
				}
			}

			this.visit(program)
		} catch (error) {
			console.error(error)
		}
	}

	/**
	 * Visit nodes recursively.
	 * @param node
	 */
	visit(node: Node) {
		if (node.kind) {
			this.validate(node)
		}

		if ('children' in node) {
			const block = <Block> node
			for (const child of block.children) {
				this.visit(child)
			}
		}
	}

	/**
	 * Check whether a given identifier has already been defined, creating an annotation if so.
	 * @param identifier
	 * @param registry
	 * @param label
	 */
	checkDuplicateIdentifier(identifier: Identifier, registry: Set<string>, label: string) {
		if (registry.has(identifier.name)) {
			this.annotate(`Cannot redeclare ${label} ${identifier.name}()`, identifier.loc)
		} else {
			registry.add(identifier.name)
		}
	}

	/**
	 * Perform additional validations on nodes.
	 * @param node
	 */
	validate(node: Node) {
		const decl = <Declaration> node
		const ident = <Identifier> decl.name

		if (!('name' in decl && 'name' in ident) || 'identifier' !== ident.kind) {
			return
		}

		if ('function' === node.kind) {
			this.checkDuplicateIdentifier(ident, this.function_names, 'function')
		} else if ('class' === node.kind) {
			this.checkDuplicateIdentifier(ident, this.class_names, 'class')
		}
	}

	/**
	 * Create a lint annotation.
	 * @param message
	 * @param location
	 * @param severity
	 */
	annotate(message: string, location: Location | null, severity = 'error') {
		const [start, end] = location
			? location.end.offset < location.start.offset ? [location.end, location.start] : [location.start, location.end]
			: [{ line: 0, column: 0 }, { line: 0, column: 0 }]

		this.annotations.push({
			message,
			severity,
			from: CodeMirror.Pos(start.line - 1, start.column),
			to: CodeMirror.Pos(end.line - 1, end.column)
		})
	}
}
errors.ts000064400000000120151540735350006431 0ustar00export const handleUnknownError = (error: unknown) => {
	console.error(error)
}
files.ts000064400000002126151540735350006227 0ustar00import { getSnippetType } from './snippets'
import type { Snippet } from '../types/Snippet'

const SECOND_IN_MS = 1000
const TIMEOUT_SECONDS = 40

const MIME_INFO = <const> {
	php: ['php', 'text/php'],
	html: ['php', 'text/php'],
	css: ['css', 'text/css'],
	js: ['js', 'text/javascript'],
	json: ['json', 'application/json']
}

export const downloadAsFile = (content: BlobPart, filename: string, type: string) => {
	const link = document.createElement('a')
	link.download = filename
	link.href = URL.createObjectURL(new Blob([content], { type }))

	setTimeout(() => URL.revokeObjectURL(link.href), TIMEOUT_SECONDS * SECOND_IN_MS)
	setTimeout(() => link.click(), 0)
}

export const downloadSnippetExportFile = (
	content: BlobPart,
	{ id, name, scope }: Snippet,
	type?: keyof typeof MIME_INFO
) => {
	const [ext, mimeType] = MIME_INFO[type ?? getSnippetType(scope)]

	const sanitizedName = name.toLowerCase().replace(/[^\w-]+/g, '-').trim()

	const title = '' === sanitizedName ? `snippet-${id}` : sanitizedName
	const filename = `${title}.code-snippets.${ext}`

	downloadAsFile(content, filename, mimeType)
}
general.ts000064400000000371151540735350006542 0ustar00export const isNetworkAdmin = (): boolean =>
	window.pagenow.endsWith('-network')

export const isMacOS = (): boolean =>
	null !== /mac/i.exec(window.navigator.userAgent)

export const isLicensed = (): boolean =>
	!!window.CODE_SNIPPETS?.isLicensed
restAPI.ts000064400000001431151540735350006432 0ustar00import axios from 'axios'
import { trimLeadingChar, trimTrailingChar } from './text'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

const REST_BASE = window.CODE_SNIPPETS?.restAPI.base ?? ''

const getRestUrl = (endpoint: string): string =>
	`${trimTrailingChar(REST_BASE, '/')}/${trimLeadingChar(endpoint, '/')}`

const GET_CACHE: Record<string, AxiosResponse<unknown> | undefined> = {}

export const getCached = <T, D>(endpoint: string, refresh = false, config?: AxiosRequestConfig<D>): Promise<AxiosResponse<T, D>> =>
	!refresh && GET_CACHE[endpoint]
		? Promise.resolve(<AxiosResponse<T, D>> GET_CACHE[endpoint])
		: axios
			.get<T, AxiosResponse<T, D>, D>(getRestUrl(endpoint), config)
			.then(response => {
				GET_CACHE[endpoint] = response
				return response
			})
shortcodes.ts000064400000000532151540735350007301 0ustar00export type ShortcodeAtts = Record<string, unknown>

export const buildShortcodeTag = (tag: string, atts: ShortcodeAtts): string =>
	`[${[
		tag,
		...Object.entries(atts)
			.filter(([, value]) => Boolean(value))
			.map(([att, value]) =>
				'boolean' === typeof value ? att : `${att}=${JSON.stringify(value)}`)
	].filter(Boolean).join(' ')}]`
snippets.ts000064400000001710151540735350006770 0ustar00import { isNetworkAdmin } from './general'
import type { Snippet, SnippetScope, SnippetType } from '../types/Snippet'

const PRO_TYPES: SnippetType[] = ['css', 'js']

export const createEmptySnippet = (): Snippet => ({
	id: 0,
	name: '',
	desc: '',
	code: '',
	tags: [],
	scope: 'global',
	modified: '',
	active: false,
	network: isNetworkAdmin(),
	shared_network: null,
	priority: 10
})

export const getSnippetType = (snippetOrScope: Snippet | SnippetScope): SnippetType => {
	const scope = 'string' === typeof snippetOrScope ? snippetOrScope : snippetOrScope.scope

	switch (true) {
		case scope.endsWith('-css'):
			return 'css'

		case scope.endsWith('-js'):
			return 'js'

		case scope.endsWith('content'):
			return 'html'

		default:
			return 'php'
	}
}

export const isProSnippet = (snippet: Snippet | SnippetScope): boolean =>
	PRO_TYPES.includes(getSnippetType(snippet))

export const isProType = (type: SnippetType): boolean =>
	PRO_TYPES.includes(type)
text.ts000064400000001157151540735350006114 0ustar00export const toCamelCase = (text: string): string =>
	text.replace(/-(?<letter>[a-z])/g, (_, letter: string) => letter.toUpperCase())

export const trimLeadingChar = (text: string, character: string): string =>
	character === text.charAt(0) ? text.slice(1) : text

export const trimTrailingChar = (text: string, character: string): string =>
	character === text.charAt(text.length - 1) ? text.slice(0, -1) : text

export const truncateWords = (text: string, wordCount: number): string => {
	const words = text.trim().split(/\s+/)

	return words.length > wordCount
		? `${words.slice(0, wordCount).join(' ')}…`
		: text
}
editable_selects.js000064400000004115151541416170010403 0ustar00/**
 * editable_selects.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

var TinyMCE_EditableSelects = {
  editSelectElm : null,

  init : function () {
    var nl = document.getElementsByTagName("select"), i, d = document, o;

    for (i = 0; i < nl.length; i++) {
      if (nl[i].className.indexOf('mceEditableSelect') != -1) {
        o = new Option(tinyMCEPopup.editor.translate('value'), '__mce_add_custom__');

        o.className = 'mceAddSelectValue';

        nl[i].options[nl[i].options.length] = o;
        nl[i].onchange = TinyMCE_EditableSelects.onChangeEditableSelect;
      }
    }
  },

  onChangeEditableSelect : function (e) {
    var d = document, ne, se = window.event ? window.event.srcElement : e.target;

    if (se.options[se.selectedIndex].value == '__mce_add_custom__') {
      ne = d.createElement("input");
      ne.id = se.id + "_custom";
      ne.name = se.name + "_custom";
      ne.type = "text";

      ne.style.width = se.offsetWidth + 'px';
      se.parentNode.insertBefore(ne, se);
      se.style.display = 'none';
      ne.focus();
      ne.onblur = TinyMCE_EditableSelects.onBlurEditableSelectInput;
      ne.onkeydown = TinyMCE_EditableSelects.onKeyDown;
      TinyMCE_EditableSelects.editSelectElm = se;
    }
  },

  onBlurEditableSelectInput : function () {
    var se = TinyMCE_EditableSelects.editSelectElm;

    if (se) {
      if (se.previousSibling.value != '') {
        addSelectValue(document.forms[0], se.id, se.previousSibling.value, se.previousSibling.value);
        selectByValue(document.forms[0], se.id, se.previousSibling.value);
      } else {
        selectByValue(document.forms[0], se.id, '');
      }

      se.style.display = 'inline';
      se.parentNode.removeChild(se.previousSibling);
      TinyMCE_EditableSelects.editSelectElm = null;
    }
  },

  onKeyDown : function (e) {
    e = e || window.event;

    if (e.keyCode == 13) {
      TinyMCE_EditableSelects.onBlurEditableSelectInput();
    }
  }
};
form_utils.js000064400000013673151541416170007304 0ustar00/**
 * form_utils.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

var themeBaseURL = tinyMCEPopup.editor.baseURI.toAbsolute('themes/' + tinyMCEPopup.getParam("theme"));

function getColorPickerHTML(id, target_form_element) {
  var h = "", dom = tinyMCEPopup.dom;

  if (label = dom.select('label[for=' + target_form_element + ']')[0]) {
    label.id = label.id || dom.uniqueId();
  }

  h += '<a role="button" aria-labelledby="' + id + '_label" id="' + id + '_link" href="javascript:;" onclick="tinyMCEPopup.pickColor(event,\'' + target_form_element + '\');" onmousedown="return false;" class="pickcolor">';
  h += '<span id="' + id + '" title="' + tinyMCEPopup.getLang('browse') + '">&nbsp;<span id="' + id + '_label" class="mceVoiceLabel mceIconOnly" style="display:none;">' + tinyMCEPopup.getLang('browse') + '</span></span></a>';

  return h;
}

function updateColor(img_id, form_element_id) {
  document.getElementById(img_id).style.backgroundColor = document.forms[0].elements[form_element_id].value;
}

function setBrowserDisabled(id, state) {
  var img = document.getElementById(id);
  var lnk = document.getElementById(id + "_link");

  if (lnk) {
    if (state) {
      lnk.setAttribute("realhref", lnk.getAttribute("href"));
      lnk.removeAttribute("href");
      tinyMCEPopup.dom.addClass(img, 'disabled');
    } else {
      if (lnk.getAttribute("realhref")) {
        lnk.setAttribute("href", lnk.getAttribute("realhref"));
      }

      tinyMCEPopup.dom.removeClass(img, 'disabled');
    }
  }
}

function getBrowserHTML(id, target_form_element, type, prefix) {
  var option = prefix + "_" + type + "_browser_callback", cb, html;

  cb = tinyMCEPopup.getParam(option, tinyMCEPopup.getParam("file_browser_callback"));

  if (!cb) {
    return "";
  }

  html = "";
  html += '<a id="' + id + '_link" href="javascript:openBrowser(\'' + id + '\',\'' + target_form_element + '\', \'' + type + '\',\'' + option + '\');" onmousedown="return false;" class="browse">';
  html += '<span id="' + id + '" title="' + tinyMCEPopup.getLang('browse') + '">&nbsp;</span></a>';

  return html;
}

function openBrowser(img_id, target_form_element, type, option) {
  var img = document.getElementById(img_id);

  if (img.className != "mceButtonDisabled") {
    tinyMCEPopup.openBrowser(target_form_element, type, option);
  }
}

function selectByValue(form_obj, field_name, value, add_custom, ignore_case) {
  if (!form_obj || !form_obj.elements[field_name]) {
    return;
  }

  if (!value) {
    value = "";
  }

  var sel = form_obj.elements[field_name];

  var found = false;
  for (var i = 0; i < sel.options.length; i++) {
    var option = sel.options[i];

    if (option.value == value || (ignore_case && option.value.toLowerCase() == value.toLowerCase())) {
      option.selected = true;
      found = true;
    } else {
      option.selected = false;
    }
  }

  if (!found && add_custom && value != '') {
    var option = new Option(value, value);
    option.selected = true;
    sel.options[sel.options.length] = option;
    sel.selectedIndex = sel.options.length - 1;
  }

  return found;
}

function getSelectValue(form_obj, field_name) {
  var elm = form_obj.elements[field_name];

  if (elm == null || elm.options == null || elm.selectedIndex === -1) {
    return "";
  }

  return elm.options[elm.selectedIndex].value;
}

function addSelectValue(form_obj, field_name, name, value) {
  var s = form_obj.elements[field_name];
  var o = new Option(name, value);
  s.options[s.options.length] = o;
}

function addClassesToList(list_id, specific_option) {
  // Setup class droplist
  var styleSelectElm = document.getElementById(list_id);
  var styles = tinyMCEPopup.getParam('theme_advanced_styles', false);
  styles = tinyMCEPopup.getParam(specific_option, styles);

  if (styles) {
    var stylesAr = styles.split(';');

    for (var i = 0; i < stylesAr.length; i++) {
      if (stylesAr != "") {
        var key, value;

        key = stylesAr[i].split('=')[0];
        value = stylesAr[i].split('=')[1];

        styleSelectElm.options[styleSelectElm.length] = new Option(key, value);
      }
    }
  } else {
    /*tinymce.each(tinyMCEPopup.editor.dom.getClasses(), function(o) {
    styleSelectElm.options[styleSelectElm.length] = new Option(o.title || o['class'], o['class']);
    });*/
  }
}

function isVisible(element_id) {
  var elm = document.getElementById(element_id);

  return elm && elm.style.display != "none";
}

function convertRGBToHex(col) {
  var re = new RegExp("rgb\\s*\\(\\s*([0-9]+).*,\\s*([0-9]+).*,\\s*([0-9]+).*\\)", "gi");

  var rgb = col.replace(re, "$1,$2,$3").split(',');
  if (rgb.length == 3) {
    r = parseInt(rgb[0]).toString(16);
    g = parseInt(rgb[1]).toString(16);
    b = parseInt(rgb[2]).toString(16);

    r = r.length == 1 ? '0' + r : r;
    g = g.length == 1 ? '0' + g : g;
    b = b.length == 1 ? '0' + b : b;

    return "#" + r + g + b;
  }

  return col;
}

function convertHexToRGB(col) {
  if (col.indexOf('#') != -1) {
    col = col.replace(new RegExp('[^0-9A-F]', 'gi'), '');

    r = parseInt(col.substring(0, 2), 16);
    g = parseInt(col.substring(2, 4), 16);
    b = parseInt(col.substring(4, 6), 16);

    return "rgb(" + r + "," + g + "," + b + ")";
  }

  return col;
}

function trimSize(size) {
  return size.replace(/([0-9\.]+)(px|%|in|cm|mm|em|ex|pt|pc)/i, '$1$2');
}

function getCSSSize(size) {
  size = trimSize(size);

  if (size == "") {
    return "";
  }

  // Add px
  if (/^[0-9]+$/.test(size)) {
    size += 'px';
  }
  // Confidence check, IE doesn't like broken values
  else if (!(/^[0-9\.]+(px|%|in|cm|mm|em|ex|pt|pc)$/i.test(size))) {
    return "";
  }

  return size;
}

function getStyle(elm, attrib, style) {
  var val = tinyMCEPopup.dom.getAttrib(elm, attrib);

  if (val != '') {
    return '' + val;
  }

  if (typeof (style) == 'undefined') {
    style = attrib;
  }

  return tinyMCEPopup.dom.getStyle(elm, style);
}
mctabs.js000064400000010100151541416170006350 0ustar00/**
 * mctabs.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/*jshint globals: tinyMCEPopup */

function MCTabs() {
  this.settings = [];
  this.onChange = tinyMCEPopup.editor.windowManager.createInstance('tinymce.util.Dispatcher');
}

MCTabs.prototype.init = function (settings) {
  this.settings = settings;
};

MCTabs.prototype.getParam = function (name, default_value) {
  var value = null;

  value = (typeof (this.settings[name]) == "undefined") ? default_value : this.settings[name];

  // Fix bool values
  if (value == "true" || value == "false") {
    return (value == "true");
  }

  return value;
};

MCTabs.prototype.showTab = function (tab) {
  tab.className = 'current';
  tab.setAttribute("aria-selected", true);
  tab.setAttribute("aria-expanded", true);
  tab.tabIndex = 0;
};

MCTabs.prototype.hideTab = function (tab) {
  var t = this;

  tab.className = '';
  tab.setAttribute("aria-selected", false);
  tab.setAttribute("aria-expanded", false);
  tab.tabIndex = -1;
};

MCTabs.prototype.showPanel = function (panel) {
  panel.className = 'current';
  panel.setAttribute("aria-hidden", false);
};

MCTabs.prototype.hidePanel = function (panel) {
  panel.className = 'panel';
  panel.setAttribute("aria-hidden", true);
};

MCTabs.prototype.getPanelForTab = function (tabElm) {
  return tinyMCEPopup.dom.getAttrib(tabElm, "aria-controls");
};

MCTabs.prototype.displayTab = function (tab_id, panel_id, avoid_focus) {
  var panelElm, panelContainerElm, tabElm, tabContainerElm, selectionClass, nodes, i, t = this;

  tabElm = document.getElementById(tab_id);

  if (panel_id === undefined) {
    panel_id = t.getPanelForTab(tabElm);
  }

  panelElm = document.getElementById(panel_id);
  panelContainerElm = panelElm ? panelElm.parentNode : null;
  tabContainerElm = tabElm ? tabElm.parentNode : null;
  selectionClass = t.getParam('selection_class', 'current');

  if (tabElm && tabContainerElm) {
    nodes = tabContainerElm.childNodes;

    // Hide all other tabs
    for (i = 0; i < nodes.length; i++) {
      if (nodes[i].nodeName == "LI") {
        t.hideTab(nodes[i]);
      }
    }

    // Show selected tab
    t.showTab(tabElm);
  }

  if (panelElm && panelContainerElm) {
    nodes = panelContainerElm.childNodes;

    // Hide all other panels
    for (i = 0; i < nodes.length; i++) {
      if (nodes[i].nodeName == "DIV") {
        t.hidePanel(nodes[i]);
      }
    }

    if (!avoid_focus) {
      tabElm.focus();
    }

    // Show selected panel
    t.showPanel(panelElm);
  }
};

MCTabs.prototype.getAnchor = function () {
  var pos, url = document.location.href;

  if ((pos = url.lastIndexOf('#')) != -1) {
    return url.substring(pos + 1);
  }

  return "";
};


//Global instance
var mcTabs = new MCTabs();

tinyMCEPopup.onInit.add(function () {
  var tinymce = tinyMCEPopup.getWin().tinymce, dom = tinyMCEPopup.dom, each = tinymce.each;

  each(dom.select('div.tabs'), function (tabContainerElm) {
    //var keyNav;

    dom.setAttrib(tabContainerElm, "role", "tablist");

    var items = tinyMCEPopup.dom.select('li', tabContainerElm);
    var action = function (id) {
      mcTabs.displayTab(id, mcTabs.getPanelForTab(id));
      mcTabs.onChange.dispatch(id);
    };

    each(items, function (item) {
      dom.setAttrib(item, 'role', 'tab');
      dom.bind(item, 'click', function (evt) {
        action(item.id);
      });
    });

    dom.bind(dom.getRoot(), 'keydown', function (evt) {
      if (evt.keyCode === 9 && evt.ctrlKey && !evt.altKey) { // Tab
        //keyNav.moveFocus(evt.shiftKey ? -1 : 1);
        tinymce.dom.Event.cancel(evt);
      }
    });

    each(dom.select('a', tabContainerElm), function (a) {
      dom.setAttrib(a, 'tabindex', '-1');
    });

    /*keyNav = tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', {
      root: tabContainerElm,
      items: items,
      onAction: action,
      actOnFocus: true,
      enableLeftRight: true,
      enableUpDown: true
    }, tinyMCEPopup.dom);*/
  }
);
});validate.js000064400000014502151541416170006702 0ustar00/**
 * validate.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/**
  // String validation:

  if (!Validator.isEmail('myemail'))
    alert('Invalid email.');

  // Form validation:

  var f = document.forms['myform'];

  if (!Validator.isEmail(f.myemail))
    alert('Invalid email.');
*/

var Validator = {
  isEmail : function (s) {
    return this.test(s, '^[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+@[-!#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$');
  },

  isAbsUrl : function (s) {
    return this.test(s, '^(news|telnet|nttp|file|http|ftp|https)://[-A-Za-z0-9\\.]+\\/?.*$');
  },

  isSize : function (s) {
    return this.test(s, '^[0-9.]+(%|in|cm|mm|em|ex|pt|pc|px)?$');
  },

  isId : function (s) {
    return this.test(s, '^[A-Za-z_]([A-Za-z0-9_])*$');
  },

  isEmpty : function (s) {
    var nl, i;

    if (s.nodeName == 'SELECT' && s.selectedIndex < 1) {
      return true;
    }

    if (s.type == 'checkbox' && !s.checked) {
      return true;
    }

    if (s.type == 'radio') {
      for (i = 0, nl = s.form.elements; i < nl.length; i++) {
        if (nl[i].type == "radio" && nl[i].name == s.name && nl[i].checked) {
          return false;
        }
      }

      return true;
    }

    return new RegExp('^\\s*$').test(s.nodeType == 1 ? s.value : s);
  },

  isNumber : function (s, d) {
    return !isNaN(s.nodeType == 1 ? s.value : s) && (!d || !this.test(s, '^-?[0-9]*\\.[0-9]*$'));
  },

  test : function (s, p) {
    s = s.nodeType == 1 ? s.value : s;

    return s == '' || new RegExp(p).test(s);
  }
};

var AutoValidator = {
  settings : {
    id_cls : 'id',
    int_cls : 'int',
    url_cls : 'url',
    number_cls : 'number',
    email_cls : 'email',
    size_cls : 'size',
    required_cls : 'required',
    invalid_cls : 'invalid',
    min_cls : 'min',
    max_cls : 'max'
  },

  init : function (s) {
    var n;

    for (n in s) {
      this.settings[n] = s[n];
    }
  },

  validate : function (f) {
    var i, nl, s = this.settings, c = 0;

    nl = this.tags(f, 'label');
    for (i = 0; i < nl.length; i++) {
      this.removeClass(nl[i], s.invalid_cls);
      nl[i].setAttribute('aria-invalid', false);
    }

    c += this.validateElms(f, 'input');
    c += this.validateElms(f, 'select');
    c += this.validateElms(f, 'textarea');

    return c == 3;
  },

  invalidate : function (n) {
    this.mark(n.form, n);
  },

  getErrorMessages : function (f) {
    var nl, i, s = this.settings, field, msg, values, messages = [], ed = tinyMCEPopup.editor;
    nl = this.tags(f, "label");
    for (i = 0; i < nl.length; i++) {
      if (this.hasClass(nl[i], s.invalid_cls)) {
        field = document.getElementById(nl[i].getAttribute("for"));
        values = { field: nl[i].textContent };
        if (this.hasClass(field, s.min_cls, true)) {
          message = ed.getLang('invalid_data_min');
          values.min = this.getNum(field, s.min_cls);
        } else if (this.hasClass(field, s.number_cls)) {
          message = ed.getLang('invalid_data_number');
        } else if (this.hasClass(field, s.size_cls)) {
          message = ed.getLang('invalid_data_size');
        } else {
          message = ed.getLang('invalid_data');
        }

        message = message.replace(/{\#([^}]+)\}/g, function (a, b) {
          return values[b] || '{#' + b + '}';
        });
        messages.push(message);
      }
    }
    return messages;
  },

  reset : function (e) {
    var t = ['label', 'input', 'select', 'textarea'];
    var i, j, nl, s = this.settings;

    if (e == null) {
      return;
    }

    for (i = 0; i < t.length; i++) {
      nl = this.tags(e.form ? e.form : e, t[i]);
      for (j = 0; j < nl.length; j++) {
        this.removeClass(nl[j], s.invalid_cls);
        nl[j].setAttribute('aria-invalid', false);
      }
    }
  },

  validateElms : function (f, e) {
    var nl, i, n, s = this.settings, st = true, va = Validator, v;

    nl = this.tags(f, e);
    for (i = 0; i < nl.length; i++) {
      n = nl[i];

      this.removeClass(n, s.invalid_cls);

      if (this.hasClass(n, s.required_cls) && va.isEmpty(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.number_cls) && !va.isNumber(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.int_cls) && !va.isNumber(n, true)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.url_cls) && !va.isAbsUrl(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.email_cls) && !va.isEmail(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.size_cls) && !va.isSize(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.id_cls) && !va.isId(n)) {
        st = this.mark(f, n);
      }

      if (this.hasClass(n, s.min_cls, true)) {
        v = this.getNum(n, s.min_cls);

        if (isNaN(v) || parseInt(n.value) < parseInt(v)) {
          st = this.mark(f, n);
        }
      }

      if (this.hasClass(n, s.max_cls, true)) {
        v = this.getNum(n, s.max_cls);

        if (isNaN(v) || parseInt(n.value) > parseInt(v)) {
          st = this.mark(f, n);
        }
      }
    }

    return st;
  },

  hasClass : function (n, c, d) {
    return new RegExp('\\b' + c + (d ? '[0-9]+' : '') + '\\b', 'g').test(n.className);
  },

  getNum : function (n, c) {
    c = n.className.match(new RegExp('\\b' + c + '([0-9]+)\\b', 'g'))[0];
    c = c.replace(/[^0-9]/g, '');

    return c;
  },

  addClass : function (n, c, b) {
    var o = this.removeClass(n, c);
    n.className = b ? c + (o !== '' ? (' ' + o) : '') : (o !== '' ? (o + ' ') : '') + c;
  },

  removeClass : function (n, c) {
    c = n.className.replace(new RegExp("(^|\\s+)" + c + "(\\s+|$)"), ' ');
    return n.className = c !== ' ' ? c : '';
  },

  tags : function (f, s) {
    return f.getElementsByTagName(s);
  },

  mark : function (f, n) {
    var s = this.settings;

    this.addClass(n, s.invalid_cls);
    n.setAttribute('aria-invalid', 'true');
    this.markLabels(f, n, s.invalid_cls);

    return false;
  },

  markLabels : function (f, n, ic) {
    var nl, i;

    nl = this.tags(f, "label");
    for (i = 0; i < nl.length; i++) {
      if (nl[i].getAttribute("for") == n.id || nl[i].htmlFor == n.id) {
        this.addClass(nl[i], ic);
      }
    }

    return null;
  }
};
array-operations.ts000064400000000721151547152720010424 0ustar00/**
 * Returns the difference between two arrays (A - B)
 */
export function arrayDifferenceBy< T >( a: T[], b: T[], key: keyof T ) {
	const keys = new Set( b.map( ( item ) => item[ key ] ) );

	return a.filter( ( item ) => ! keys.has( item[ key ] ) );
}

/**
 * Returns the union of two arrays (A ∪ B)
 */
export function arrayUnionBy< T >( a: T[], b: T[], key: keyof T ) {
	const difference = arrayDifferenceBy( b, a, key );

	return [ ...a, ...difference ];
}
attributes-query.ts000064400000005042151547152720010457 0ustar00/**
 * External dependencies
 */
import {
	AttributeObject,
	AttributeQuery,
	AttributeTerm,
} from '@woocommerce/types';
import { sort } from 'fast-sort';

/**
 * Given a query object, removes an attribute filter by a single slug.
 *
 * @param {Array}    query     Current query object.
 * @param {Function} setQuery  Callback to update the current query object.
 * @param {Object}   attribute An attribute object.
 * @param {string}   slug      Term slug to remove.
 */
export const removeAttributeFilterBySlug = (
	query: AttributeQuery[] = [],
	setQuery: ( query: AttributeQuery[] ) => void,
	attribute: AttributeObject,
	slug = ''
) => {
	// Get current filter for provided attribute.
	const foundQuery = query.filter(
		( item ) => item.attribute === attribute.taxonomy
	);

	const currentQuery = foundQuery.length ? foundQuery[ 0 ] : null;

	if (
		! currentQuery ||
		! currentQuery.slug ||
		! Array.isArray( currentQuery.slug ) ||
		! currentQuery.slug.includes( slug )
	) {
		return;
	}

	const newSlugs = currentQuery.slug.filter( ( item ) => item !== slug );

	// Remove current attribute filter from query.
	const returnQuery = query.filter(
		( item ) => item.attribute !== attribute.taxonomy
	);

	// Add a new query for selected terms, if provided.
	if ( newSlugs.length > 0 ) {
		currentQuery.slug = newSlugs.sort();
		returnQuery.push( currentQuery );
	}

	setQuery( sort( returnQuery ).asc( 'attribute' ) );
};

/**
 * Given a query object, sets the query up to filter by a given attribute and attribute terms.
 *
 * @param {Array}    query          Current query object.
 * @param {Function} setQuery       Callback to update the current query object.
 * @param {Object}   attribute      An attribute object.
 * @param {Array}    attributeTerms Array of term objects.
 * @param {string}   operator       Operator for the filter. Valid values: in, and.
 *
 * @return {Object} An attribute object.
 */
export const updateAttributeFilter = (
	query: AttributeQuery[] = [],
	setQuery: ( query: AttributeQuery[] ) => void,
	attribute?: AttributeObject,
	attributeTerms: AttributeTerm[] = [],
	operator: 'in' | 'and' = 'in'
) => {
	if ( ! attribute || ! attribute.taxonomy ) {
		return [];
	}

	const returnQuery = query.filter(
		( item ) => item.attribute !== attribute.taxonomy
	);

	if ( attributeTerms.length === 0 ) {
		setQuery( returnQuery );
	} else {
		returnQuery.push( {
			attribute: attribute.taxonomy,
			operator,
			slug: attributeTerms.map( ( { slug } ) => slug ).sort(),
		} );
		setQuery( sort( returnQuery ).asc( 'attribute' ) );
	}

	return returnQuery;
};
attributes.ts000064400000007347151547152720007326 0ustar00/**
 * External dependencies
 */
import { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
import { getSetting } from '@woocommerce/settings';
import {
	AttributeObject,
	AttributeSetting,
	AttributeTerm,
	AttributeWithTerms,
	isAttributeTerm,
} from '@woocommerce/types';
import { dispatch, select } from '@wordpress/data';

const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );

/**
 * Format an attribute from the settings into an object with standardized keys.
 *
 * @param {Object} attribute The attribute object.
 */
const attributeSettingToObject = ( attribute: AttributeSetting ) => {
	if ( ! attribute || ! attribute.attribute_name ) {
		return null;
	}
	return {
		id: parseInt( attribute.attribute_id, 10 ),
		name: attribute.attribute_name,
		taxonomy: 'pa_' + attribute.attribute_name,
		label: attribute.attribute_label,
	};
};

/**
 * Format all attribute settings into objects.
 */
const attributeObjects = ATTRIBUTES.reduce(
	( acc: Partial< AttributeObject >[], current ) => {
		const attributeObject = attributeSettingToObject( current );

		if ( attributeObject && attributeObject.id ) {
			acc.push( attributeObject );
		}

		return acc;
	},
	[]
);

/**
 * Converts an Attribute object into a shape compatible with the `SearchListControl`
 */
export const convertAttributeObjectToSearchItem = (
	attribute: AttributeObject | AttributeTerm | AttributeWithTerms
): SearchListItem => {
	const { count, id, name, parent } = attribute;

	return {
		count,
		id,
		name,
		parent,
		breadcrumbs: [],
		children: [],
		value: isAttributeTerm( attribute ) ? attribute.attr_slug : '',
	};
};

/**
 * Get attribute data by taxonomy.
 *
 * @param {number} attributeId The attribute ID.
 * @return {Object|undefined} The attribute object if it exists.
 */
export const getAttributeFromID = ( attributeId: number ) => {
	if ( ! attributeId ) {
		return;
	}
	return attributeObjects.find( ( attribute ) => {
		return attribute.id === attributeId;
	} );
};

/**
 * Get attribute data by taxonomy.
 *
 * @param {string} taxonomy The attribute taxonomy name e.g. pa_color.
 * @return {Object|undefined} The attribute object if it exists.
 */
export const getAttributeFromTaxonomy = ( taxonomy: string ) => {
	if ( ! taxonomy ) {
		return;
	}
	return attributeObjects.find( ( attribute ) => {
		return attribute.taxonomy === taxonomy;
	} );
};

/**
 * Get the taxonomy of an attribute by Attribute ID.
 *
 * @param {number} attributeId The attribute ID.
 * @return {string} The taxonomy name.
 */
export const getTaxonomyFromAttributeId = ( attributeId: number ) => {
	if ( ! attributeId ) {
		return null;
	}
	const attribute = getAttributeFromID( attributeId );
	return attribute ? attribute.taxonomy : null;
};

/**
 * Updates an attribute in a sibling block. Useful if two settings control the same attribute, but you don't want to
 * have this attribute exist on a parent block.
 */
export const updateAttributeInSiblingBlock = (
	clientId: string,
	attribute: string,
	newValue: unknown,
	siblingBlockName: string
) => {
	const store = select( 'core/block-editor' );
	const actions = dispatch( 'core/block-editor' );
	const parentBlocks = store.getBlockParents( clientId );

	let shippingMethodsBlockClientId = '';

	// Loop through parent block's children until we find woocommerce/checkout-shipping-methods-block.
	// Also set this attribute in the woocommerce/checkout-shipping-methods-block.
	parentBlocks.forEach( ( parent ) => {
		const childBlock = store
			.getBlock( parent )
			.innerBlocks.find( ( child ) => child.name === siblingBlockName );
		if ( ! childBlock ) {
			return;
		}
		shippingMethodsBlockClientId = childBlock.clientId;
	} );
	actions.updateBlockAttributes( shippingMethodsBlockClientId, {
		[ attribute ]: newValue,
	} );
};
filters.ts000064400000004552151547152720006603 0ustar00/**
 * External dependencies
 */
import { getQueryArg, getQueryArgs, addQueryArgs } from '@wordpress/url';
import { getSettingWithCoercion } from '@woocommerce/settings';
import { isBoolean } from '@woocommerce/types';

const filteringForPhpTemplate = getSettingWithCoercion(
	'isRenderingPhpTemplate',
	false,
	isBoolean
);

/**
 * Returns specified parameter from URL
 *
 * @param {string} name Parameter you want the value of.
 */

export const PREFIX_QUERY_ARG_QUERY_TYPE = 'query_type_';
export const PREFIX_QUERY_ARG_FILTER_TYPE = 'filter_';

export function getUrlParameter( name: string ) {
	if ( ! window ) {
		return null;
	}
	return getQueryArg( window.location.href, name );
}

/**
 * Change the URL and reload the page if filtering for PHP templates.
 *
 * @param {string} newUrl New URL to be set.
 */
export function changeUrl( newUrl: string ) {
	if ( filteringForPhpTemplate ) {
		/**
		 * We want to remove page number from URL whenever filters are changed.
		 * This will move the user to the first page of results.
		 *
		 * There are following page number formats:
		 * 1. query-{number}-page={number} (ex. query-1-page=2)
		 * 	- ref: https://github.com/WordPress/gutenberg/blob/5693a62214b6c76d3dc5f3f69d8aad187748af79/packages/block-library/src/query-pagination-numbers/index.php#L18
		 * 2. query-page={number} (ex. query-page=2)
		 * 	- ref: same as above
		 * 3. page/{number} (ex. page/2) (Default WordPress pagination format)
		 */
		newUrl = newUrl.replace(
			/(?:query-(?:\d+-)?page=(\d+))|(?:page\/(\d+))/g,
			''
		);

		/**
		 * If the URL ends with '?', we remove the trailing '?' from the URL.
		 * The trailing '?' in a URL is unnecessary and can cause the page to
		 * reload, which can negatively affect performance. By removing the '?',
		 * we prevent this unnecessary reload. This is safe to do even if there
		 * are query parameters, as they will not be affected by the removal
		 * of a trailing '?'.
		 */
		if ( newUrl.endsWith( '?' ) ) {
			newUrl = newUrl.slice( 0, -1 );
		}

		window.location.href = newUrl;
	} else {
		window.history.replaceState( {}, '', newUrl );
	}
}

/**
 * Run the query params through buildQueryString to normalise the params.
 *
 * @param {string} url URL to encode the search param from.
 */
export const normalizeQueryParams = ( url: string ) => {
	const queryArgs = getQueryArgs( url );
	return addQueryArgs( url, queryArgs );
};
index.ts000064400000000621151547152720006233 0ustar00export * from './array-operations';
export * from './attributes-query';
export * from './attributes';
export * from './filters';
export * from './notices';
export * from './object-operations';
export * from './products';
export * from './shared-attributes';
export * from './sanitize-html';
export * from './is-site-editor-page';
export * from './is-widget-editor-page';
export * from './trim-words';
is-site-editor-page.ts000064400000000624151547152720010702 0ustar00/**
 * Internal dependencies
 */
import { isObject } from '../types/type-guards';

export const isSiteEditorPage = ( store: unknown ): boolean => {
	if ( isObject( store ) ) {
		const editedPostType = (
			store as {
				getEditedPostType: () => string;
			}
		 ).getEditedPostType();

		return (
			editedPostType === 'wp_template' ||
			editedPostType === 'wp_template_part'
		);
	}

	return false;
};
is-widget-editor-page.ts000064400000000556151547152720011225 0ustar00/**
 * Internal dependencies
 */
import { isObject } from '../types/type-guards';

export const isWidgetEditorPage = ( store: unknown ): boolean => {
	if ( isObject( store ) ) {
		const widgetAreas = (
			store as {
				getWidgetAreas: () => string;
			}
		 ).getWidgetAreas();

		return Array.isArray( widgetAreas ) && widgetAreas.length > 0;
	}

	return false;
};
notices.ts000064400000001502151547152720006567 0ustar00/**
 * External dependencies
 */
import { dispatch, select } from '@wordpress/data';
import type { Notice } from '@wordpress/notices';

export const hasNoticesOfType = (
	type: 'default' | 'snackbar',
	context?: string | undefined
): boolean => {
	const notices: Notice[] = select( 'core/notices' ).getNotices( context );
	return notices.some( ( notice: Notice ) => notice.type === type );
};

// Note, if context is blank, the default context is used.
export const removeNoticesByStatus = (
	status: string,
	context?: string | undefined
): void => {
	const notices = select( 'core/notices' ).getNotices( context );
	const { removeNotice } = dispatch( 'core/notices' );
	const noticesOfType = notices.filter(
		( notice ) => notice.status === status
	);
	noticesOfType.forEach( ( notice ) => removeNotice( notice.id, context ) );
};
object-operations.ts000064400000000255151547152720010556 0ustar00/**
 * Returns an object without a key.
 */
export function objectOmit< T, K extends keyof T >( obj: T, key: K ) {
	const { [ key ]: omit, ...rest } = obj;

	return rest;
}
products.ts000064400000002163151547152720006772 0ustar00/**
 * External dependencies
 */
import type { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
import type { ProductResponseItem } from '@woocommerce/types';

/**
 * Converts a Product object into a shape compatible with the `SearchListControl`
 */
export const convertProductResponseItemToSearchItem = (
	product: ProductResponseItem
): SearchListItem< ProductResponseItem > => {
	const { id, name, parent } = product;

	return {
		id,
		name,
		parent,
		breadcrumbs: [],
		children: [],
		details: product,
		value: product.slug,
	};
};

/**
 * Get the src of the first image attached to a product (the featured image).
 */
export function getImageSrcFromProduct( product: ProductResponseItem ) {
	if ( ! product || ! product.images || ! product.images.length ) {
		return '';
	}

	return product.images[ 0 ].src || '';
}

/**
 * Get the ID of the first image attached to a product (the featured image).
 */
export function getImageIdFromProduct( product: ProductResponseItem ) {
	if ( ! product || ! product.images || ! product.images.length ) {
		return 0;
	}

	return product.images[ 0 ].id || 0;
}
sanitize-html.ts000064400000001016151547152720007713 0ustar00/**
 * External dependencies
 */
import DOMPurify from 'dompurify';

const ALLOWED_TAGS = [ 'a', 'b', 'em', 'i', 'strong', 'p', 'br' ];
const ALLOWED_ATTR = [ 'target', 'href', 'rel', 'name', 'download' ];

export const sanitizeHTML = (
	html: string,
	config?: { tags?: typeof ALLOWED_TAGS; attr?: typeof ALLOWED_ATTR }
) => {
	const tagsValue = config?.tags || ALLOWED_TAGS;
	const attrValue = config?.attr || ALLOWED_ATTR;

	return DOMPurify.sanitize( html, {
		ALLOWED_TAGS: tagsValue,
		ALLOWED_ATTR: attrValue,
	} );
};
shared-attributes.js000064400000002562151547152720010552 0ustar00/**
 * External dependencies
 */
import { getSetting } from '@woocommerce/settings';

export const sharedAttributeBlockTypes = [
	'woocommerce/product-best-sellers',
	'woocommerce/product-category',
	'woocommerce/product-new',
	'woocommerce/product-on-sale',
	'woocommerce/product-top-rated',
];

export default {
	/**
	 * Number of columns.
	 */
	columns: {
		type: 'number',
		default: getSetting( 'defaultColumns', 3 ),
	},

	/**
	 * Number of rows.
	 */
	rows: {
		type: 'number',
		default: getSetting( 'defaultRows', 3 ),
	},

	/**
	 * How to align cart buttons.
	 */
	alignButtons: {
		type: 'boolean',
		default: false,
	},

	/**
	 * Product category, used to display only products in the given categories.
	 */
	categories: {
		type: 'array',
		default: [],
	},

	/**
	 * Product category operator, used to restrict to products in all or any selected categories.
	 */
	catOperator: {
		type: 'string',
		default: 'any',
	},

	/**
	 * Content visibility setting
	 */
	contentVisibility: {
		type: 'object',
		default: {
			image: true,
			title: true,
			price: true,
			rating: true,
			button: true,
		},
	},

	/**
	 * Are we previewing?
	 */
	isPreview: {
		type: 'boolean',
		default: false,
	},

	/**
	 * Whether to display in stock, out of stock or backorder products.
	 */
	stockStatus: {
		type: 'array',
		default: Object.keys( getSetting( 'stockStatusOptions', [] ) ),
	},
};
test/filters.js000064400000001575151547152720007552 0ustar00/**
 * Internal dependencies
 */
import { normalizeQueryParams } from '../filters';

describe( 'normalizeQueryParams', () => {
	test( 'does not change url if there is no query params', () => {
		const input = 'https://example.com';
		const expected = 'https://example.com';

		expect( normalizeQueryParams( input ) ).toBe( expected );
	} );

	test( 'does not change search term if there is no special character', () => {
		const input = 'https://example.com?foo=bar&s=asdf1234&baz=qux';
		const expected = 'https://example.com?foo=bar&s=asdf1234&baz=qux';

		expect( normalizeQueryParams( input ) ).toBe( expected );
	} );

	test( 'decodes single quote characters', () => {
		const input = 'https://example.com?foo=bar%27&s=asd%27f1234&baz=qux%27';
		const expected = "https://example.com?foo=bar'&s=asd'f1234&baz=qux'";

		expect( normalizeQueryParams( input ) ).toBe( expected );
	} );
} );
test/notices.js000064400000006125151547152720007542 0ustar00/**
 * External dependencies
 */
import { select, dispatch } from '@wordpress/data';

/**
 * Internal dependencies
 */
import { hasNoticesOfType, removeNoticesByStatus } from '../notices';

jest.mock( '@wordpress/data' );

describe( 'Notice utils', () => {
	beforeEach( () => {
		jest.resetAllMocks();
	} );
	describe( 'hasNoticesOfType', () => {
		it( 'Correctly returns if there are notices of a given type in the core data store', () => {
			select.mockReturnValue( {
				getNotices: jest.fn().mockReturnValue( [
					{
						id: 'coupon-form',
						status: 'error',
						content:
							'Coupon cannot be removed because it is not already applied to the cart.',
						spokenMessage:
							'Coupon cannot be removed because it is not already applied to the cart.',
						isDismissible: true,
						actions: [],
						type: 'default',
						icon: null,
						explicitDismiss: false,
					},
				] ),
			} );
			const hasSnackbarNotices = hasNoticesOfType(
				'snackbar',
				'wc/cart'
			);
			const hasDefaultNotices = hasNoticesOfType( 'default', 'wc/cart' );
			expect( hasDefaultNotices ).toBe( true );
			expect( hasSnackbarNotices ).toBe( false );
		} );

		it( 'Handles notices being empty', () => {
			select.mockReturnValue( {
				getNotices: jest.fn().mockReturnValue( [] ),
			} );
			const hasDefaultNotices = hasNoticesOfType( 'default', 'wc/cart' );
			expect( hasDefaultNotices ).toBe( false );
		} );
	} );
	describe( 'removeNoticesByStatus', () => {
		it( 'Correctly removes notices of a given status', () => {
			select.mockReturnValue( {
				getNotices: jest.fn().mockReturnValue( [
					{
						id: 'coupon-form',
						status: 'error',
						content:
							'Coupon cannot be removed because it is not already applied to the cart.',
						spokenMessage:
							'Coupon cannot be removed because it is not already applied to the cart.',
						isDismissible: true,
						actions: [],
						type: 'default',
						icon: null,
						explicitDismiss: false,
					},
					{
						id: 'address-form',
						status: 'error',
						content: 'Address invalid',
						spokenMessage: 'Address invalid',
						isDismissible: true,
						actions: [],
						type: 'default',
						icon: null,
						explicitDismiss: false,
					},
					{
						id: 'some-warning',
						status: 'warning',
						content: 'Warning notice.',
						spokenMessage: 'Warning notice.',
						isDismissible: true,
						actions: [],
						type: 'default',
						icon: null,
						explicitDismiss: false,
					},
				] ),
			} );
			dispatch.mockReturnValue( {
				removeNotice: jest.fn(),
			} );
			removeNoticesByStatus( 'error' );
			expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
				1,
				'coupon-form',
				undefined
			);
			expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
				2,
				'address-form',
				undefined
			);
		} );

		it( 'Handles notices being empty', () => {
			select.mockReturnValue( {
				getNotices: jest.fn().mockReturnValue( [] ),
			} );

			dispatch.mockReturnValue( {
				removeNotice: jest.fn(),
			} );
			removeNoticesByStatus( 'empty' );
			expect( dispatch().removeNotice ).not.toBeCalled();
		} );
	} );
} );
test/products.js000064400000004103151547152720007733 0ustar00/**
 * Internal dependencies
 */
import { getImageSrcFromProduct, getImageIdFromProduct } from '../products';

describe( 'getImageSrcFromProduct', () => {
	test( 'returns first image src', () => {
		const imageSrc = getImageSrcFromProduct( {
			images: [ { src: 'foo.jpg' } ],
		} );

		expect( imageSrc ).toBe( 'foo.jpg' );
	} );

	test( 'returns empty string if no product was provided', () => {
		const imageSrc = getImageSrcFromProduct();

		expect( imageSrc ).toBe( '' );
	} );

	test( 'returns empty string if product is empty', () => {
		const imageSrc = getImageSrcFromProduct( {} );

		expect( imageSrc ).toBe( '' );
	} );

	test( 'returns empty string if product has no images', () => {
		const imageSrc = getImageSrcFromProduct( { images: null } );

		expect( imageSrc ).toBe( '' );
	} );

	test( 'returns empty string if product has 0 images', () => {
		const imageSrc = getImageSrcFromProduct( { images: [] } );

		expect( imageSrc ).toBe( '' );
	} );

	test( 'returns empty string if product image has no src attribute', () => {
		const imageSrc = getImageSrcFromProduct( { images: [ {} ] } );

		expect( imageSrc ).toBe( '' );
	} );
} );

describe( 'getImageIdFromProduct', () => {
	test( 'returns first image id', () => {
		const imageUrl = getImageIdFromProduct( {
			images: [ { id: 123 } ],
		} );

		expect( imageUrl ).toBe( 123 );
	} );

	test( 'returns 0 if no product was provided', () => {
		const imageUrl = getImageIdFromProduct();

		expect( imageUrl ).toBe( 0 );
	} );

	test( 'returns 0 if product is empty', () => {
		const imageUrl = getImageIdFromProduct( {} );

		expect( imageUrl ).toBe( 0 );
	} );

	test( 'returns 0 if product has no images', () => {
		const imageUrl = getImageIdFromProduct( { images: null } );

		expect( imageUrl ).toBe( 0 );
	} );

	test( 'returns 0 if product has 0 images', () => {
		const imageUrl = getImageIdFromProduct( { images: [] } );

		expect( imageUrl ).toBe( 0 );
	} );

	test( 'returns 0 if product image has no src attribute', () => {
		const imageUrl = getImageIdFromProduct( { images: [ {} ] } );

		expect( imageUrl ).toBe( 0 );
	} );
} );
test/trim-words.ts000064400000006023151547152720010214 0ustar00/**
 * External dependencies
 */
import {
	appendMoreText,
	removeTags,
	trimCharacters,
	trimWords,
} from '@woocommerce/utils';

describe( 'trim-words', () => {
	describe( 'removeTags', () => {
		it( 'Removes HTML tags from a string', () => {
			const string = '<div><a href="/index.php">trim-words.ts</a></div>';
			const trimmedString = removeTags( string );
			expect( trimmedString ).toEqual( 'trim-words.ts' );
		} );
	} );
	describe( 'appendMoreText', () => {
		it( 'Removes trailing punctuation and appends some characters to a string', () => {
			const string = 'trim-words.ts,';
			const appendedString = appendMoreText( string, '...' );
			expect( appendedString ).toEqual( 'trim-words.ts...' );
		} );
	} );
	describe( 'trimWords', () => {
		const testContent =
			'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.';
		it( 'Limits words in string and returns trimmed version', () => {
			const trimmedString = trimWords( testContent, 3 );
			expect( trimmedString ).toBe(
				'<p>Lorem ipsum dolor&hellip;</p>\n'
			);
		} );
		it( 'Limits words in string and returns trimmed version with custom moreText', () => {
			const trimmedString = trimWords( testContent, 4, '... read more.' );
			expect( trimmedString ).toEqual(
				'<p>Lorem ipsum dolor sit... read more.</p>\n'
			);
		} );
		it( 'Limits words in string and returns trimmed version without autop', () => {
			const trimmedString = trimWords(
				testContent,
				3,
				'&hellip;',
				false
			);
			expect( trimmedString ).toEqual( 'Lorem ipsum dolor&hellip;' );
		} );
		it( 'does not append anything if the text is shorter than the trim limit', () => {
			const trimmedString = trimWords( testContent, 100 );
			expect( trimmedString ).toEqual( '<p>' + testContent + '</p>\n' );
		} );
	} );
	describe( 'trimCharacters', () => {
		const testContent = 'Lorem ipsum dolor sit amet.';

		it( 'Limits characters in string and returns trimmed version including spaces', () => {
			const result = trimCharacters( testContent, 10 );
			expect( result ).toEqual( '<p>Lorem ipsu&hellip;</p>\n' );
		} );
		it( 'Limits characters in string and returns trimmed version excluding spaces', () => {
			const result = trimCharacters( testContent, 10, false );
			expect( result ).toEqual( '<p>Lorem ipsum&hellip;</p>\n' );
		} );
		it( 'Limits characters in string and returns trimmed version with custom moreText', () => {
			const result = trimCharacters(
				testContent,
				10,
				false,
				'... read more.'
			);
			expect( result ).toEqual( '<p>Lorem ipsum... read more.</p>\n' );
		} );
		it( 'Limits characters in string and returns trimmed version without autop', () => {
			const result = trimCharacters(
				testContent,
				10,
				false,
				'... read more.',
				false
			);
			expect( result ).toEqual( 'Lorem ipsum... read more.' );
		} );

		it( 'does not append anything if the text is shorter than the trim limit', () => {
			const trimmedString = trimCharacters( testContent, 1000 );
			expect( trimmedString ).toEqual( '<p>' + testContent + '</p>\n' );
		} );
	} );
} );
trim-words.ts000064400000005313151547152720007236 0ustar00/**
 * External dependencies
 */
import { autop } from '@wordpress/autop';

/**
 * Remove HTML tags from a string.
 *
 * @param {string} htmlString String to remove tags from.
 * @return {string} Plain text string.
 */
export const removeTags = ( htmlString: string ) => {
	const tagsRegExp = /<\/?[a-z][^>]*?>/gi;
	return htmlString.replace( tagsRegExp, '' );
};

/**
 * Remove trailing punctuation and append some characters to a string.
 *
 * @param {string} text     Text to append to.
 * @param {string} moreText Text to append.
 * @return {string} String with appended characters.
 */
export const appendMoreText = ( text: string, moreText: string ) => {
	return text.replace( /[\s|\.\,]+$/i, '' ) + moreText;
};

/**
 * Limit words in string and returned trimmed version.
 *
 * @param {string} text      Text to trim.
 * @param {number} maxLength Number of countType to limit to.
 * @param {string} moreText  Appended to the trimmed string.
 * @param {string} useAutop  Whether to format with autop before returning.
 * @return {string} Trimmed string.
 */
export const trimWords = (
	text: string,
	maxLength: number,
	moreText = '&hellip;',
	useAutop = true
) => {
	const textToTrim = removeTags( text );
	const trimmedText = textToTrim
		.split( ' ' )
		.splice( 0, maxLength )
		.join( ' ' );

	if ( trimmedText === textToTrim ) {
		return useAutop ? autop( textToTrim ) : textToTrim;
	}

	if ( ! useAutop ) {
		return appendMoreText( trimmedText, moreText );
	}

	return autop( appendMoreText( trimmedText, moreText ) );
};

/**
 * Limit characters in string and returned trimmed version.
 *
 * @param {string}  text          Text to trim.
 * @param {number}  maxLength     Number of countType to limit to.
 * @param {boolean} includeSpaces Should spaces be included in the count.
 * @param {string}  moreText      Appended to the trimmed string.
 * @param {string}  useAutop      Whether to format with autop before returning.
 * @return {string} Trimmed string.
 */
export const trimCharacters = (
	text: string,
	maxLength: number,
	includeSpaces = true,
	moreText = '&hellip;',
	useAutop = true
) => {
	const textToTrim = removeTags( text );
	const trimmedText = textToTrim.slice( 0, maxLength );

	if ( trimmedText === textToTrim ) {
		return useAutop ? autop( textToTrim ) : textToTrim;
	}

	if ( includeSpaces ) {
		return autop( appendMoreText( trimmedText, moreText ) );
	}

	const matchSpaces = trimmedText.match( /([\s]+)/g );
	const spaceCount = matchSpaces ? matchSpaces.length : 0;
	const trimmedTextExcludingSpaces = textToTrim.slice(
		0,
		maxLength + spaceCount
	);

	if ( ! useAutop ) {
		return appendMoreText( trimmedTextExcludingSpaces, moreText );
	}

	return autop( appendMoreText( trimmedTextExcludingSpaces, moreText ) );
};
index.js000064400000000135151547154170006222 0ustar00export * from './validation';
export { extensionCartUpdate } from './extension-cart-update';
price.ts000064400000011117151547154170006231 0ustar00/**
 * External dependencies
 */
import { CURRENCY } from '@woocommerce/settings';
import type {
	Currency,
	CurrencyResponse,
	CartShippingPackageShippingRate,
	SymbolPosition,
} from '@woocommerce/types';

/**
 * Get currency prefix.
 */
const getPrefix = (
	// Currency symbol.
	symbol: string,
	// Position of currency symbol from settings.
	symbolPosition: SymbolPosition
): string => {
	const prefixes = {
		left: symbol,
		left_space: ' ' + symbol,
		right: '',
		right_space: '',
	};
	return prefixes[ symbolPosition ] || '';
};

/**
 * Get currency suffix.
 */
const getSuffix = (
	// Currency symbol.
	symbol: string,
	// Position of currency symbol from settings.
	symbolPosition: SymbolPosition
): string => {
	const suffixes = {
		left: '',
		left_space: '',
		right: symbol,
		right_space: ' ' + symbol,
	};
	return suffixes[ symbolPosition ] || '';
};

/**
 * Currency information in normalized format from server settings.
 */
const siteCurrencySettings: Currency = {
	code: CURRENCY.code,
	symbol: CURRENCY.symbol,
	thousandSeparator: CURRENCY.thousandSeparator,
	decimalSeparator: CURRENCY.decimalSeparator,
	minorUnit: CURRENCY.precision,
	prefix: getPrefix(
		CURRENCY.symbol,
		CURRENCY.symbolPosition as SymbolPosition
	),
	suffix: getSuffix(
		CURRENCY.symbol,
		CURRENCY.symbolPosition as SymbolPosition
	),
};

/**
 * Gets currency information in normalized format from an API response or the server.
 *
 * If no currency was provided, or currency_code is empty, the default store currency will be used.
 */
export const getCurrencyFromPriceResponse = (
	// Currency data object, for example an API response containing currency formatting data.
	currencyData?:
		| CurrencyResponse
		| Record< string, never >
		| CartShippingPackageShippingRate
): Currency => {
	if ( ! currencyData?.currency_code ) {
		return siteCurrencySettings;
	}

	const {
		currency_code: code,
		currency_symbol: symbol,
		currency_thousand_separator: thousandSeparator,
		currency_decimal_separator: decimalSeparator,
		currency_minor_unit: minorUnit,
		currency_prefix: prefix,
		currency_suffix: suffix,
	} = currencyData;

	return {
		code: code || 'USD',
		symbol: symbol || '$',
		thousandSeparator:
			typeof thousandSeparator === 'string' ? thousandSeparator : ',',
		decimalSeparator:
			typeof decimalSeparator === 'string' ? decimalSeparator : '.',
		minorUnit: Number.isFinite( minorUnit ) ? minorUnit : 2,
		prefix: typeof prefix === 'string' ? prefix : '$',
		suffix: typeof suffix === 'string' ? suffix : '',
	};
};

/**
 * Gets currency information in normalized format, allowing overrides.
 */
export const getCurrency = (
	currencyData: Partial< Currency > = {}
): Currency => {
	return {
		...siteCurrencySettings,
		...currencyData,
	};
};

const applyThousandSeparator = (
	numberString: string,
	thousandSeparator: string
): string => {
	return numberString.replace( /\B(?=(\d{3})+(?!\d))/g, thousandSeparator );
};

const splitDecimal = (
	numberString: string
): {
	beforeDecimal: string;
	afterDecimal: string;
} => {
	const parts = numberString.split( '.' );
	const beforeDecimal = parts[ 0 ];
	const afterDecimal = parts[ 1 ] || '';
	return {
		beforeDecimal,
		afterDecimal,
	};
};

const applyDecimal = (
	afterDecimal: string,
	decimalSeparator: string,
	minorUnit: number
): string => {
	if ( afterDecimal ) {
		return `${ decimalSeparator }${ afterDecimal.padEnd(
			minorUnit,
			'0'
		) }`;
	}

	if ( minorUnit > 0 ) {
		return `${ decimalSeparator }${ '0'.repeat( minorUnit ) }`;
	}

	return '';
};

/**
 * Format a price, provided using the smallest unit of the currency, as a
 * decimal complete with currency symbols using current store settings.
 */
export const formatPrice = (
	// Price in minor unit, e.g. cents.
	price: number | string,
	currencyData?: Currency
): string => {
	if ( price === '' || price === undefined ) {
		return '';
	}

	const priceInt: number =
		typeof price === 'number' ? price : parseInt( price, 10 );

	if ( ! Number.isFinite( priceInt ) ) {
		return '';
	}

	const currency: Currency = getCurrency( currencyData );

	const { minorUnit, prefix, suffix, decimalSeparator, thousandSeparator } =
		currency;

	const formattedPrice: number = priceInt / 10 ** minorUnit;

	const { beforeDecimal, afterDecimal } = splitDecimal(
		formattedPrice.toString()
	);

	const formattedValue = `${ prefix }${ applyThousandSeparator(
		beforeDecimal,
		thousandSeparator
	) }${ applyDecimal(
		afterDecimal,
		decimalSeparator,
		minorUnit
	) }${ suffix }`;

	// This uses a textarea to magically decode HTML currency symbols.
	const txt = document.createElement( 'textarea' );
	txt.innerHTML = formattedValue;
	return txt.value;
};
test/price.js000064400000006177151547154170007210 0ustar00/**
 * Internal dependencies
 */
import { formatPrice, getCurrency } from '../price';

describe( 'The function formatPrice()', () => {
	test.each`
		value               | prefix    | suffix   | thousandSeparator | decimalSeparator | minorUnit | expected
		${ 1020 }           | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€10.20' }
		${ 1000 }           | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€10.00' }
		${ 1000 }           | ${ '' }   | ${ '€' } | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '10.00€' }
		${ 1000 }           | ${ '' }   | ${ '$' } | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '10.00$' }
		${ '1000' }         | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€10.00' }
		${ 0 }              | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€0.00' }
		${ '' }             | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '' }
		${ null }           | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '' }
		${ undefined }      | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '' }
		${ 100000 }         | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€1,000.00' }
		${ 1000000 }        | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€10,000.00' }
		${ 1000000000 }     | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '€10,000,000.00' }
		${ 10000000000 }    | ${ '€' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 3 }    | ${ '€10,000,000.000' }
		${ 10000000000000 } | ${ '€ ' } | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 6 }    | ${ '€ 10,000,000.000000' }
		${ 10000000 }       | ${ '€ ' } | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 0 }    | ${ '€ 10,000,000' }
		${ 1000000099 }     | ${ '$' }  | ${ '' }  | ${ ',' }          | ${ '.' }         | ${ 2 }    | ${ '$10,000,000.99' }
		${ 1000000099 }     | ${ '$' }  | ${ '' }  | ${ '.' }          | ${ ',' }         | ${ 2 }    | ${ '$10.000.000,99' }
	`(
		'correctly formats price given "$value", "$prefix" prefix, "$suffix" suffix, "$thousandSeparator" thousandSeparator, "$decimalSeparator" decimalSeparator, and "$minorUnit" minorUnit as "$expected"',
		( {
			value,
			prefix,
			suffix,
			expected,
			thousandSeparator,
			decimalSeparator,
			minorUnit,
		} ) => {
			const formattedPrice = formatPrice(
				value,
				getCurrency( {
					prefix,
					suffix,
					thousandSeparator,
					decimalSeparator,
					minorUnit,
				} )
			);

			expect( formattedPrice ).toEqual( expected );
		}
	);

	test.each`
		value          | expected
		${ 1000 }      | ${ '$10.00' }
		${ 0 }         | ${ '$0.00' }
		${ '' }        | ${ '' }
		${ null }      | ${ '' }
		${ undefined } | ${ '' }
	`(
		'correctly formats price given "$value" only as "$expected"',
		( { value, expected } ) => {
			const formattedPrice = formatPrice( value );

			expect( formattedPrice ).toEqual( expected );
		}
	);
} );
extension-cart-update.ts000064400000001362151550766460011360 0ustar00/**
 * External dependencies
 */
import { dispatch } from '@wordpress/data';
import { CartResponse, ExtensionCartUpdateArgs } from '@woocommerce/types';

/**
 * Internal dependencies
 */
import { STORE_KEY } from '../../../assets/js/data/cart/constants';

/**
 * When executed, this will call the cart/extensions endpoint.
 * The args contains a namespace, so if that extension has registered an update
 * callback, it will be executed server-side and the new cart will be returned.
 * The new cart is then received into the client-side store.
 */
export const extensionCartUpdate = (
	args: ExtensionCartUpdateArgs
): Promise< CartResponse > => {
	const { applyExtensionCartUpdate } = dispatch( STORE_KEY );
	return applyExtensionCartUpdate( args );
};
validation/get-validity-message-for-input.ts000064400000002040151550766460015226 0ustar00/**
 * External dependencies
 */
import { __, sprintf } from '@wordpress/i18n';

/**
 * Converts an input's validityState to a string to display on the frontend.
 *
 * This returns custom messages for invalid/required fields. Other error types use defaults from the browser (these
 * could be implemented in the future but are not currently used by the block checkout).
 */
const getValidityMessageForInput = (
	label: string,
	inputElement: HTMLInputElement
): string => {
	const { valid, customError, valueMissing, badInput, typeMismatch } =
		inputElement.validity;

	// No errors, or custom error - return early.
	if ( valid || customError ) {
		return inputElement.validationMessage;
	}

	const invalidFieldMessage = sprintf(
		/* translators: %s field label */
		__( 'Please enter a valid %s', 'woo-gutenberg-products-block' ),
		label.toLowerCase()
	);

	if ( valueMissing || badInput || typeMismatch ) {
		return invalidFieldMessage;
	}

	return inputElement.validationMessage || invalidFieldMessage;
};

export default getValidityMessageForInput;
validation/index.ts000064400000000312151550766460010370 0ustar00export { default as mustContain } from './must-contain';
export { default as getValidityMessageForInput } from './get-validity-message-for-input';
export { default as isPostcode } from './is-postcode';
validation/is-postcode.ts000064400000002276151550766460011525 0ustar00/**
 * External dependencies
 */
import { POSTCODE_REGEXES } from 'postcode-validator/lib/cjs/postcode-regexes.js';

const CUSTOM_REGEXES = new Map< string, RegExp >( [
	[ 'BA', /^([7-8]{1})([0-9]{4})$/ ],
	[
		'GB',
		/^([A-Z]){1}([0-9]{1,2}|[A-Z][0-9][A-Z]|[A-Z][0-9]{2}|[A-Z][0-9]|[0-9][A-Z]){1}([ ])?([0-9][A-Z]{2}){1}|BFPO(?:\s)?([0-9]{1,4})$|BFPO(c\/o[0-9]{1,3})$/i,
	],
	[ 'IN', /^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/ ],
	[ 'JP', /^([0-9]{3})([-]?)([0-9]{4})$/ ],
	[ 'LI', /^(94[8-9][0-9])$/ ],
	[ 'NL', /^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i ],
	[ 'SI', /^([1-9][0-9]{3})$/ ],
	[ 'KH', /^[0-9]{6}$/ ], // Cambodia (6-digit postal code)
] );

const DEFAULT_REGEXES = new Map< string, RegExp >( [
	...POSTCODE_REGEXES,
	...CUSTOM_REGEXES,
] );

export interface IsPostcodeProps {
	postcode: string;
	country: string;
}

const isPostcode = ( { postcode, country }: IsPostcodeProps ): boolean => {
	// If the country is not in the list of regexes, trying to test it would result in an error, so we skip and assume
	// that it is valid.
	const postcodeTest = DEFAULT_REGEXES.get( country )?.test( postcode );
	return typeof postcodeTest !== 'undefined' ? postcodeTest : true;
};

export default isPostcode;
validation/must-contain.ts000064400000001124151550766460011704 0ustar00/**
 * External dependencies
 */
import { __, sprintf } from '@wordpress/i18n';

/**
 * Ensures that a given value contains a string, or throws an error.
 */
const mustContain = ( value: string, requiredValue: string ): true | never => {
	if ( ! value.includes( requiredValue ) ) {
		throw Error(
			sprintf(
				/* translators: %1$s value passed to filter, %2$s : value that must be included. */
				__(
					'Returned value must include %1$s, you passed "%2$s"',
					'woo-gutenberg-products-block'
				),
				requiredValue,
				value
			)
		);
	}
	return true;
};

export default mustContain;
validation/test/index.tsx000064400000003205151550766460011543 0ustar00/**
 * External dependencies
 */
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
 * Internal dependencies
 */
import { getValidityMessageForInput } from '../index';

describe( 'getValidityMessageForInput', () => {
	it( 'Returns nothing if the input is valid', async () => {
		render( <input type="text" data-testid="custom-input" /> );

		const textInputElement = ( await screen.getByTestId(
			'custom-input'
		) ) as HTMLInputElement;

		const validityMessage = getValidityMessageForInput(
			'Test',
			textInputElement
		);
		expect( validityMessage ).toBe( '' );
	} );
	it( 'Returns error message if a required input is empty', async () => {
		render( <input type="text" required data-testid="custom-input" /> );

		const textInputElement = ( await screen.getByTestId(
			'custom-input'
		) ) as HTMLInputElement;

		const validityMessage = getValidityMessageForInput(
			'Test',
			textInputElement
		);

		expect( validityMessage ).toBe( 'Please enter a valid test' );
	} );
	it( 'Returns a custom error if set, rather than a new message', async () => {
		render(
			<input
				type="text"
				required
				onChange={ ( event ) => {
					event.target.setCustomValidity( 'Custom error' );
				} }
				data-testid="custom-input"
			/>
		);

		const textInputElement = ( await screen.getByTestId(
			'custom-input'
		) ) as HTMLInputElement;

		await act( async () => {
			await userEvent.type( textInputElement, 'Invalid Value' );
		} );

		const validityMessage = getValidityMessageForInput(
			'Test',
			textInputElement
		);
		expect( validityMessage ).toBe( 'Custom error' );
	} );
} );
validation/test/is-postcode.ts000064400000010275151550766460012502 0ustar00/**
 * Internal dependencies
 */
import isPostcode from '../is-postcode';
import type { IsPostcodeProps } from '../is-postcode';

describe( 'isPostcode', () => {
	const cases = [
		// Austrian postcodes
		[ true, '1000', 'AT' ],
		[ true, '9999', 'AT' ],
		[ false, '0000', 'AT' ],
		[ false, '10000', 'AT' ],

		// Bosnian postcodes
		[ true, '71000', 'BA' ],
		[ true, '78256', 'BA' ],
		[ true, '89240', 'BA' ],
		[ false, '61000', 'BA' ],
		[ false, '7850', 'BA' ],

		// Belgian postcodes
		[ true, '1111', 'BE' ],
		[ false, '111', 'BE' ],
		[ false, '11111', 'BE' ],

		// Brazilian postcodes
		[ true, '99999-999', 'BR' ],
		[ true, '99999999', 'BR' ],
		[ false, '99999 999', 'BR' ],
		[ false, '99999-ABC', 'BR' ],

		// Canadian postcodes
		[ true, 'A9A 9A9', 'CA' ],
		[ true, 'A9A9A9', 'CA' ],
		[ true, 'a9a9a9', 'CA' ],
		[ false, 'D0A 9A9', 'CA' ],
		[ false, '99999', 'CA' ],
		[ false, 'ABC999', 'CA' ],
		[ false, '0A0A0A', 'CA' ],

		// Swiss postcodes
		[ true, '9999', 'CH' ],
		[ false, '99999', 'CH' ],
		[ false, 'ABCDE', 'CH' ],

		// Czech postcodes
		[ true, '160 00', 'CZ' ],
		[ true, '16000', 'CZ' ],
		[ false, '1600', 'CZ' ],

		// German postcodes
		[ true, '01234', 'DE' ],
		[ true, '12345', 'DE' ],
		[ false, '12 345', 'DE' ],
		[ false, '1234', 'DE' ],

		// Spanish postcodes
		[ true, '03000', 'ES' ],
		[ true, '08000', 'ES' ],
		[ false, '08 000', 'ES' ],
		[ false, '1234', 'ES' ],

		// French postcodes
		[ true, '01000', 'FR' ],
		[ true, '99999', 'FR' ],
		[ true, '01 000', 'FR' ],
		[ false, '1234', 'FR' ],

		// British postcodes
		[ true, 'AA9A 9AA', 'GB' ],
		[ true, 'A9A 9AA', 'GB' ],
		[ true, 'A9 9AA', 'GB' ],
		[ true, 'A99 9AA', 'GB' ],
		[ true, 'AA99 9AA', 'GB' ],
		[ true, 'BFPO 801', 'GB' ],
		[ false, '99999', 'GB' ],
		[ false, '9999 999', 'GB' ],
		[ false, '999 999', 'GB' ],
		[ false, '99 999', 'GB' ],
		[ false, '9A A9A', 'GB' ],

		// Hungarian postcodes
		[ true, '1234', 'HU' ],
		[ false, '123', 'HU' ],
		[ false, '12345', 'HU' ],

		// Irish postcodes
		[ true, 'A65F4E2', 'IE' ],
		[ true, 'A65 F4E2', 'IE' ],
		[ true, 'A65-F4E2', 'IE' ],
		[ false, 'B23F854', 'IE' ],

		// Indian postcodes
		[ true, '110001', 'IN' ],
		[ true, '110 001', 'IN' ],
		[ false, '11 0001', 'IN' ],
		[ false, '1100 01', 'IN' ],

		// Italian postcodes
		[ true, '99999', 'IT' ],
		[ false, '9999', 'IT' ],
		[ false, 'ABC 999', 'IT' ],
		[ false, 'ABC-999', 'IT' ],
		[ false, 'ABC_123', 'IT' ],

		// Japanese postcodes
		[ true, '1340088', 'JP' ],
		[ true, '134-0088', 'JP' ],
		[ false, '1340-088', 'JP' ],
		[ false, '12345', 'JP' ],
		[ false, '0123', 'JP' ],

		// Lichtenstein postcodes
		[ true, '9485', 'LI' ],
		[ true, '9486', 'LI' ],
		[ true, '9499', 'LI' ],
		[ false, '9585', 'LI' ],
		[ false, '9385', 'LI' ],
		[ false, '9475', 'LI' ],

		// Dutch postcodes
		[ true, '3852GC', 'NL' ],
		[ true, '3852 GC', 'NL' ],
		[ true, '3852 gc', 'NL' ],
		[ false, '3852SA', 'NL' ],
		[ false, '3852 SA', 'NL' ],
		[ false, '3852 sa', 'NL' ],

		// Polish postcodes
		[ true, '00-001', 'PL' ],
		[ true, '99-440', 'PL' ],
		[ false, '000-01', 'PL' ],
		[ false, '994-40', 'PL' ],
		[ false, '00001', 'PL' ],
		[ false, '99440', 'PL' ],

		// Puerto Rican postcodes
		[ true, '00901', 'PR' ],
		[ true, '00617', 'PR' ],
		[ true, '00602-1211', 'PR' ],
		[ false, '1234', 'PR' ],
		[ false, '0060-21211', 'PR' ],

		// Portuguese postcodes
		[ true, '1234-567', 'PT' ],
		[ true, '2345-678', 'PT' ],
		[ false, '123-4567', 'PT' ],
		[ false, '234-5678', 'PT' ],

		// Slovenian postcodes
		[ true, '1234', 'SI' ],
		[ true, '1000', 'SI' ],
		[ true, '9876', 'SI' ],
		[ false, '12345', 'SI' ],
		[ false, '0123', 'SI' ],

		// Slovak postcodes
		[ true, '010 01', 'SK' ],
		[ true, '01001', 'SK' ],
		[ false, '01 001', 'SK' ],
		[ false, '1234', 'SK' ],
		[ false, '123456', 'SK' ],

		// United States postcodes
		[ true, '90210', 'US' ],
		[ true, '99577-0727', 'US' ],
		[ false, 'ABCDE', 'US' ],
		[ false, 'ABCDE-9999', 'US' ],

		// Cambodian postcodes
		[ false, '12345', 'KH' ],
		[ false, '1234', 'KH' ],
		[ true, '123456', 'KH' ],
	];

	test.each( cases )( '%s: %s for %s', ( result, postcode, country ) =>
		expect( isPostcode( { postcode, country } as IsPostcodeProps ) ).toBe(
			result
		)
	);
} );