File: /var/www/vhosts/uyarreklam.com.tr/httpdocs/utils.tar
Linter.ts 0000644 00000010701 15154073535 0006360 0 ustar 00 /**
* 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.ts 0000644 00000000120 15154073535 0006431 0 ustar 00 export const handleUnknownError = (error: unknown) => {
console.error(error)
}
files.ts 0000644 00000002126 15154073535 0006227 0 ustar 00 import { 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.ts 0000644 00000000371 15154073535 0006542 0 ustar 00 export 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.ts 0000644 00000001431 15154073535 0006432 0 ustar 00 import 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.ts 0000644 00000000532 15154073535 0007301 0 ustar 00 export 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.ts 0000644 00000001710 15154073535 0006770 0 ustar 00 import { 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.ts 0000644 00000001157 15154073535 0006114 0 ustar 00 export 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.js 0000644 00000004115 15154141617 0010403 0 ustar 00 /**
* 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.js 0000644 00000013673 15154141617 0007304 0 ustar 00 /**
* 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') + '"> <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') + '"> </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.js 0000644 00000010100 15154141617 0006350 0 ustar 00 /**
* 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.js 0000644 00000014502 15154141617 0006702 0 ustar 00 /**
* 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.ts 0000644 00000000721 15154715272 0010424 0 ustar 00 /**
* 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.ts 0000644 00000005042 15154715272 0010457 0 ustar 00 /**
* 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.ts 0000644 00000007347 15154715272 0007326 0 ustar 00 /**
* 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.ts 0000644 00000004552 15154715272 0006603 0 ustar 00 /**
* 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.ts 0000644 00000000621 15154715272 0006233 0 ustar 00 export * 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.ts 0000644 00000000624 15154715272 0010702 0 ustar 00 /**
* 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.ts 0000644 00000000556 15154715272 0011225 0 ustar 00 /**
* 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.ts 0000644 00000001502 15154715272 0006567 0 ustar 00 /**
* 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.ts 0000644 00000000255 15154715272 0010556 0 ustar 00 /**
* 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.ts 0000644 00000002163 15154715272 0006772 0 ustar 00 /**
* 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.ts 0000644 00000001016 15154715272 0007713 0 ustar 00 /**
* 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.js 0000644 00000002562 15154715272 0010552 0 ustar 00 /**
* 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.js 0000644 00000001575 15154715272 0007552 0 ustar 00 /**
* 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.js 0000644 00000006125 15154715272 0007542 0 ustar 00 /**
* 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.js 0000644 00000004103 15154715272 0007733 0 ustar 00 /**
* 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.ts 0000644 00000006023 15154715272 0010214 0 ustar 00 /**
* 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…</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,
'…',
false
);
expect( trimmedString ).toEqual( 'Lorem ipsum dolor…' );
} );
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…</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…</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.ts 0000644 00000005313 15154715272 0007236 0 ustar 00 /**
* 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 = '…',
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 = '…',
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.js 0000644 00000000135 15154715417 0006222 0 ustar 00 export * from './validation';
export { extensionCartUpdate } from './extension-cart-update';
price.ts 0000644 00000011117 15154715417 0006231 0 ustar 00 /**
* 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.js 0000644 00000006177 15154715417 0007210 0 ustar 00 /**
* 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.ts 0000644 00000001362 15155076646 0011360 0 ustar 00 /**
* 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.ts 0000644 00000002040 15155076646 0015226 0 ustar 00 /**
* 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.ts 0000644 00000000312 15155076646 0010370 0 ustar 00 export { 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.ts 0000644 00000002276 15155076646 0011525 0 ustar 00 /**
* 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.ts 0000644 00000001124 15155076646 0011704 0 ustar 00 /**
* 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.tsx 0000644 00000003205 15155076646 0011543 0 ustar 00 /**
* 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.ts 0000644 00000010275 15155076646 0012502 0 ustar 00 /**
* 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
)
);
} );