Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)

FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-12 12:15:39 +00:00
commit 8d688b459d
10003 changed files with 1928395 additions and 0 deletions

26
frontend/node_modules/pdf-lib/src/utils/Cache.ts generated vendored Normal file
View file

@ -0,0 +1,26 @@
class Cache<T> {
static readonly populatedBy = <T>(populate: () => T) => new Cache(populate);
private readonly populate: () => T;
private value: T | undefined;
private constructor(populate: () => T) {
this.populate = populate;
this.value = undefined;
}
getValue(): T | undefined {
return this.value;
}
access(): T {
if (!this.value) this.value = this.populate();
return this.value;
}
invalidate(): void {
this.value = undefined;
}
}
export default Cache;

146
frontend/node_modules/pdf-lib/src/utils/arrays.ts generated vendored Normal file
View file

@ -0,0 +1,146 @@
import { decodeFromBase64DataUri } from 'src/utils/base64';
import { charFromCode } from 'src/utils/strings';
export const last = <T>(array: T[]): T => array[array.length - 1];
// export const dropLast = <T>(array: T[]): T[] =>
// array.slice(0, array.length - 1);
export const typedArrayFor = (value: string | Uint8Array): Uint8Array => {
if (value instanceof Uint8Array) return value;
const length = value.length;
const typedArray = new Uint8Array(length);
for (let idx = 0; idx < length; idx++) {
typedArray[idx] = value.charCodeAt(idx);
}
return typedArray;
};
export const mergeIntoTypedArray = (...arrays: (string | Uint8Array)[]) => {
const arrayCount = arrays.length;
const typedArrays: Uint8Array[] = [];
for (let idx = 0; idx < arrayCount; idx++) {
const element = arrays[idx];
typedArrays[idx] =
element instanceof Uint8Array ? element : typedArrayFor(element);
}
let totalSize = 0;
for (let idx = 0; idx < arrayCount; idx++) {
totalSize += arrays[idx].length;
}
const merged = new Uint8Array(totalSize);
let offset = 0;
for (let arrIdx = 0; arrIdx < arrayCount; arrIdx++) {
const arr = typedArrays[arrIdx];
for (let byteIdx = 0, arrLen = arr.length; byteIdx < arrLen; byteIdx++) {
merged[offset++] = arr[byteIdx];
}
}
return merged;
};
export const mergeUint8Arrays = (arrays: Uint8Array[]): Uint8Array => {
let totalSize = 0;
for (let idx = 0, len = arrays.length; idx < len; idx++) {
totalSize += arrays[idx].length;
}
const mergedBuffer = new Uint8Array(totalSize);
let offset = 0;
for (let idx = 0, len = arrays.length; idx < len; idx++) {
const array = arrays[idx];
mergedBuffer.set(array, offset);
offset += array.length;
}
return mergedBuffer;
};
export const arrayAsString = (array: Uint8Array | number[]): string => {
let str = '';
for (let idx = 0, len = array.length; idx < len; idx++) {
str += charFromCode(array[idx]);
}
return str;
};
export const byAscendingId = <T extends { id: any }>(a: T, b: T) => a.id - b.id;
export const sortedUniq = <T>(array: T[], indexer: (elem: T) => any): T[] => {
const uniq: T[] = [];
for (let idx = 0, len = array.length; idx < len; idx++) {
const curr = array[idx];
const prev = array[idx - 1];
if (idx === 0 || indexer(curr) !== indexer(prev)) {
uniq.push(curr);
}
}
return uniq;
};
// Arrays and TypedArrays in JS both have .reverse() methods, which would seem
// to negate the need for this function. However, not all runtimes support this
// method (e.g. React Native). This function compensates for that fact.
export const reverseArray = (array: Uint8Array) => {
const arrayLen = array.length;
for (let idx = 0, len = Math.floor(arrayLen / 2); idx < len; idx++) {
const leftIdx = idx;
const rightIdx = arrayLen - idx - 1;
const temp = array[idx];
array[leftIdx] = array[rightIdx];
array[rightIdx] = temp;
}
return array;
};
export const sum = (array: number[] | Uint8Array): number => {
let total = 0;
for (let idx = 0, len = array.length; idx < len; idx++) {
total += array[idx];
}
return total;
};
export const range = (start: number, end: number): number[] => {
const arr = new Array(end - start);
for (let idx = 0, len = arr.length; idx < len; idx++) {
arr[idx] = start + idx;
}
return arr;
};
export const pluckIndices = <T>(arr: T[], indices: number[]) => {
const plucked = new Array<T>(indices.length);
for (let idx = 0, len = indices.length; idx < len; idx++) {
plucked[idx] = arr[indices[idx]];
}
return plucked;
};
export const canBeConvertedToUint8Array = (
input: any,
): input is string | ArrayBuffer | Uint8Array =>
input instanceof Uint8Array ||
input instanceof ArrayBuffer ||
typeof input === 'string';
export const toUint8Array = (input: string | ArrayBuffer | Uint8Array) => {
if (typeof input === 'string') {
return decodeFromBase64DataUri(input);
} else if (input instanceof ArrayBuffer) {
return new Uint8Array(input);
} else if (input instanceof Uint8Array) {
return input;
} else {
throw new TypeError(
'`input` must be one of `string | ArrayBuffer | Uint8Array`',
);
}
};

8
frontend/node_modules/pdf-lib/src/utils/async.ts generated vendored Normal file
View file

@ -0,0 +1,8 @@
/**
* Returns a Promise that resolves after at least one tick of the
* Macro Task Queue occurs.
*/
export const waitForTick = (): Promise<void> =>
new Promise((resolve) => {
setTimeout(() => resolve(), 0);
});

99
frontend/node_modules/pdf-lib/src/utils/base64.ts generated vendored Normal file
View file

@ -0,0 +1,99 @@
/*
* The `chars`, `lookup`, `encode`, and `decode` members of this file are
* licensed under the following:
*
* base64-arraybuffer
* https://github.com/niklasvh/base64-arraybuffer
*
* Copyright (c) 2012 Niklas von Hertzen
* Licensed under the MIT license.
*
*/
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const lookup = new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
export const encodeToBase64 = (bytes: Uint8Array): string => {
let base64 = '';
const len = bytes.length;
for (let i = 0; i < len; i += 3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + '==';
}
return base64;
};
export const decodeFromBase64 = (base64: string): Uint8Array => {
let bufferLength = base64.length * 0.75;
const len = base64.length;
let i;
let p = 0;
let encoded1;
let encoded2;
let encoded3;
let encoded4;
if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const bytes = new Uint8Array(bufferLength);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return bytes;
};
// This regex is designed to be as flexible as possible. It will parse certain
// invalid data URIs.
const DATA_URI_PREFIX_REGEX = /^(data)?:?([\w\/\+]+)?;?(charset=[\w-]+|base64)?.*,/i;
/**
* If the `dataUri` input is a data URI, then the data URI prefix must not be
* longer than 100 characters, or this function will fail to decode it.
*
* @param dataUri a base64 data URI or plain base64 string
* @returns a Uint8Array containing the decoded input
*/
export const decodeFromBase64DataUri = (dataUri: string): Uint8Array => {
const trimmedUri = dataUri.trim();
const prefix = trimmedUri.substring(0, 100);
const res = prefix.match(DATA_URI_PREFIX_REGEX);
// Assume it's not a data URI - just a plain base64 string
if (!res) return decodeFromBase64(trimmedUri);
// Remove the data URI prefix and parse the remainder as a base64 string
const [fullMatch] = res;
const data = trimmedUri.substring(fullMatch.length);
return decodeFromBase64(data);
};

3
frontend/node_modules/pdf-lib/src/utils/errors.ts generated vendored Normal file
View file

@ -0,0 +1,3 @@
export const error = (msg: string) => {
throw new Error(msg);
};

11
frontend/node_modules/pdf-lib/src/utils/index.ts generated vendored Normal file
View file

@ -0,0 +1,11 @@
export * from 'src/utils/arrays';
export * from 'src/utils/async';
export * from 'src/utils/strings';
export * from 'src/utils/unicode';
export * from 'src/utils/numbers';
export * from 'src/utils/errors';
export * from 'src/utils/base64';
export * from 'src/utils/objects';
export * from 'src/utils/validators';
export * from 'src/utils/pdfDocEncoding';
export { default as Cache } from 'src/utils/Cache';

55
frontend/node_modules/pdf-lib/src/utils/numbers.ts generated vendored Normal file
View file

@ -0,0 +1,55 @@
// tslint:disable radix
/**
* Converts a number to its string representation in decimal. This function
* differs from simply converting a number to a string with `.toString()`
* because this function's output string will **not** contain exponential
* notation.
*
* Credit: https://stackoverflow.com/a/46545519
*/
export const numberToString = (num: number) => {
let numStr = String(num);
if (Math.abs(num) < 1.0) {
const e = parseInt(num.toString().split('e-')[1]);
if (e) {
const negative = num < 0;
if (negative) num *= -1;
num *= Math.pow(10, e - 1);
numStr = '0.' + new Array(e).join('0') + num.toString().substring(2);
if (negative) numStr = '-' + numStr;
}
} else {
let e = parseInt(num.toString().split('+')[1]);
if (e > 20) {
e -= 20;
num /= Math.pow(10, e);
numStr = num.toString() + new Array(e + 1).join('0');
}
}
return numStr;
};
export const sizeInBytes = (n: number) => Math.ceil(n.toString(2).length / 8);
/**
* Converts a number into its constituent bytes and returns them as
* a number[].
*
* Returns most significant byte as first element in array. It may be necessary
* to call .reverse() to get the bits in the desired order.
*
* Example:
* bytesFor(0x02A41E) => [ 0b10, 0b10100100, 0b11110 ]
*
* Credit for algorithm: https://stackoverflow.com/a/1936865
*/
export const bytesFor = (n: number) => {
const bytes = new Uint8Array(sizeInBytes(n));
for (let i = 1; i <= bytes.length; i++) {
bytes[i - 1] = n >> ((bytes.length - i) * 8);
}
return bytes;
};

13
frontend/node_modules/pdf-lib/src/utils/objects.ts generated vendored Normal file
View file

@ -0,0 +1,13 @@
import { FontNames } from '@pdf-lib/standard-fonts';
export const values = (obj: any) => Object.keys(obj).map((k) => obj[k]);
export const StandardFontValues = values(FontNames);
export const isStandardFont = (input: any): input is FontNames =>
StandardFontValues.includes(input);
export const rectanglesAreEqual = (
a: { x: number; y: number; width: number; height: number },
b: { x: number; y: number; width: number; height: number },
) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;

View file

@ -0,0 +1,69 @@
import { toCharCode } from 'src/utils/strings';
// Mapping from PDFDocEncoding to Unicode code point
const pdfDocEncodingToUnicode = new Uint16Array(256);
// Initialize the code points which are the same
for (let idx = 0; idx < 256; idx++) {
pdfDocEncodingToUnicode[idx] = idx;
}
// Set differences (see "Table D.2 PDFDocEncoding Character Set" of the PDF spec)
pdfDocEncodingToUnicode[0x16] = toCharCode('\u0017'); // SYNCRONOUS IDLE
pdfDocEncodingToUnicode[0x18] = toCharCode('\u02D8'); // BREVE
pdfDocEncodingToUnicode[0x19] = toCharCode('\u02C7'); // CARON
pdfDocEncodingToUnicode[0x1a] = toCharCode('\u02C6'); // MODIFIER LETTER CIRCUMFLEX ACCENT
pdfDocEncodingToUnicode[0x1b] = toCharCode('\u02D9'); // DOT ABOVE
pdfDocEncodingToUnicode[0x1c] = toCharCode('\u02DD'); // DOUBLE ACUTE ACCENT
pdfDocEncodingToUnicode[0x1d] = toCharCode('\u02DB'); // OGONEK
pdfDocEncodingToUnicode[0x1e] = toCharCode('\u02DA'); // RING ABOVE
pdfDocEncodingToUnicode[0x1f] = toCharCode('\u02DC'); // SMALL TILDE
pdfDocEncodingToUnicode[0x7f] = toCharCode('\uFFFD'); // REPLACEMENT CHARACTER (box with questionmark)
pdfDocEncodingToUnicode[0x80] = toCharCode('\u2022'); // BULLET
pdfDocEncodingToUnicode[0x81] = toCharCode('\u2020'); // DAGGER
pdfDocEncodingToUnicode[0x82] = toCharCode('\u2021'); // DOUBLE DAGGER
pdfDocEncodingToUnicode[0x83] = toCharCode('\u2026'); // HORIZONTAL ELLIPSIS
pdfDocEncodingToUnicode[0x84] = toCharCode('\u2014'); // EM DASH
pdfDocEncodingToUnicode[0x85] = toCharCode('\u2013'); // EN DASH
pdfDocEncodingToUnicode[0x86] = toCharCode('\u0192'); // LATIN SMALL LETTER SCRIPT F
pdfDocEncodingToUnicode[0x87] = toCharCode('\u2044'); // FRACTION SLASH (solidus)
pdfDocEncodingToUnicode[0x88] = toCharCode('\u2039'); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK
pdfDocEncodingToUnicode[0x89] = toCharCode('\u203A'); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
pdfDocEncodingToUnicode[0x8a] = toCharCode('\u2212'); // MINUS SIGN
pdfDocEncodingToUnicode[0x8b] = toCharCode('\u2030'); // PER MILLE SIGN
pdfDocEncodingToUnicode[0x8c] = toCharCode('\u201E'); // DOUBLE LOW-9 QUOTATION MARK (quotedblbase)
pdfDocEncodingToUnicode[0x8d] = toCharCode('\u201C'); // LEFT DOUBLE QUOTATION MARK (quotedblleft)
pdfDocEncodingToUnicode[0x8e] = toCharCode('\u201D'); // RIGHT DOUBLE QUOTATION MARK (quotedblright)
pdfDocEncodingToUnicode[0x8f] = toCharCode('\u2018'); // LEFT SINGLE QUOTATION MARK (quoteleft)
pdfDocEncodingToUnicode[0x90] = toCharCode('\u2019'); // RIGHT SINGLE QUOTATION MARK (quoteright)
pdfDocEncodingToUnicode[0x91] = toCharCode('\u201A'); // SINGLE LOW-9 QUOTATION MARK (quotesinglbase)
pdfDocEncodingToUnicode[0x92] = toCharCode('\u2122'); // TRADE MARK SIGN
pdfDocEncodingToUnicode[0x93] = toCharCode('\uFB01'); // LATIN SMALL LIGATURE FI
pdfDocEncodingToUnicode[0x94] = toCharCode('\uFB02'); // LATIN SMALL LIGATURE FL
pdfDocEncodingToUnicode[0x95] = toCharCode('\u0141'); // LATIN CAPITAL LETTER L WITH STROKE
pdfDocEncodingToUnicode[0x96] = toCharCode('\u0152'); // LATIN CAPITAL LIGATURE OE
pdfDocEncodingToUnicode[0x97] = toCharCode('\u0160'); // LATIN CAPITAL LETTER S WITH CARON
pdfDocEncodingToUnicode[0x98] = toCharCode('\u0178'); // LATIN CAPITAL LETTER Y WITH DIAERESIS
pdfDocEncodingToUnicode[0x99] = toCharCode('\u017D'); // LATIN CAPITAL LETTER Z WITH CARON
pdfDocEncodingToUnicode[0x9a] = toCharCode('\u0131'); // LATIN SMALL LETTER DOTLESS I
pdfDocEncodingToUnicode[0x9b] = toCharCode('\u0142'); // LATIN SMALL LETTER L WITH STROKE
pdfDocEncodingToUnicode[0x9c] = toCharCode('\u0153'); // LATIN SMALL LIGATURE OE
pdfDocEncodingToUnicode[0x9d] = toCharCode('\u0161'); // LATIN SMALL LETTER S WITH CARON
pdfDocEncodingToUnicode[0x9e] = toCharCode('\u017E'); // LATIN SMALL LETTER Z WITH CARON
pdfDocEncodingToUnicode[0x9f] = toCharCode('\uFFFD'); // REPLACEMENT CHARACTER (box with questionmark)
pdfDocEncodingToUnicode[0xa0] = toCharCode('\u20AC'); // EURO SIGN
pdfDocEncodingToUnicode[0xad] = toCharCode('\uFFFD'); // REPLACEMENT CHARACTER (box with questionmark)
/**
* Decode a byte array into a string using PDFDocEncoding.
*
* @param bytes a byte array (decimal representation) containing a string
* encoded with PDFDocEncoding.
*/
export const pdfDocEncodingDecode = (bytes: Uint8Array): string => {
const codePoints = new Array(bytes.length);
for (let idx = 0, len = bytes.length; idx < len; idx++) {
codePoints[idx] = pdfDocEncodingToUnicode[bytes[idx]];
}
return String.fromCodePoint(...codePoints);
};

70
frontend/node_modules/pdf-lib/src/utils/png.ts generated vendored Normal file
View file

@ -0,0 +1,70 @@
import UPNG from '@pdf-lib/upng';
const getImageType = (ctype: number) => {
if (ctype === 0) return PngType.Greyscale;
if (ctype === 2) return PngType.Truecolour;
if (ctype === 3) return PngType.IndexedColour;
if (ctype === 4) return PngType.GreyscaleWithAlpha;
if (ctype === 6) return PngType.TruecolourWithAlpha;
throw new Error(`Unknown color type: ${ctype}`);
};
const splitAlphaChannel = (rgbaChannel: Uint8Array) => {
const pixelCount = Math.floor(rgbaChannel.length / 4);
const rgbChannel = new Uint8Array(pixelCount * 3);
const alphaChannel = new Uint8Array(pixelCount * 1);
let rgbaOffset = 0;
let rgbOffset = 0;
let alphaOffset = 0;
while (rgbaOffset < rgbaChannel.length) {
rgbChannel[rgbOffset++] = rgbaChannel[rgbaOffset++];
rgbChannel[rgbOffset++] = rgbaChannel[rgbaOffset++];
rgbChannel[rgbOffset++] = rgbaChannel[rgbaOffset++];
alphaChannel[alphaOffset++] = rgbaChannel[rgbaOffset++];
}
return { rgbChannel, alphaChannel };
};
export enum PngType {
Greyscale = 'Greyscale',
Truecolour = 'Truecolour',
IndexedColour = 'IndexedColour',
GreyscaleWithAlpha = 'GreyscaleWithAlpha',
TruecolourWithAlpha = 'TruecolourWithAlpha',
}
export class PNG {
static load = (pngData: Uint8Array) => new PNG(pngData);
readonly rgbChannel: Uint8Array;
readonly alphaChannel?: Uint8Array;
readonly type: PngType;
readonly width: number;
readonly height: number;
readonly bitsPerComponent: number;
private constructor(pngData: Uint8Array) {
const upng = UPNG.decode(pngData);
const frames = UPNG.toRGBA8(upng);
if (frames.length > 1) throw new Error(`Animated PNGs are not supported`);
const frame = new Uint8Array(frames[0]);
const { rgbChannel, alphaChannel } = splitAlphaChannel(frame);
this.rgbChannel = rgbChannel;
const hasAlphaValues = alphaChannel.some((a) => a < 255);
if (hasAlphaValues) this.alphaChannel = alphaChannel;
this.type = getImageType(upng.ctype);
this.width = upng.width;
this.height = upng.height;
this.bitsPerComponent = 8;
}
}

21
frontend/node_modules/pdf-lib/src/utils/rng.ts generated vendored Normal file
View file

@ -0,0 +1,21 @@
/**
* Generates a pseudo random number. Although it is not cryptographically secure
* and uniformly distributed, it is not a concern for the intended use-case,
* which is to generate distinct numbers.
*
* Credit: https://stackoverflow.com/a/19303725/10254049
*/
export class SimpleRNG {
static withSeed = (seed: number) => new SimpleRNG(seed);
private seed: number;
private constructor(seed: number) {
this.seed = seed;
}
nextInt(): number {
const x = Math.sin(this.seed++) * 10000;
return x - Math.floor(x);
}
}

182
frontend/node_modules/pdf-lib/src/utils/strings.ts generated vendored Normal file
View file

@ -0,0 +1,182 @@
export const toCharCode = (character: string) => character.charCodeAt(0);
export const toCodePoint = (character: string) => character.codePointAt(0);
export const toHexStringOfMinLength = (num: number, minLength: number) =>
padStart(num.toString(16), minLength, '0').toUpperCase();
export const toHexString = (num: number) => toHexStringOfMinLength(num, 2);
export const charFromCode = (code: number) => String.fromCharCode(code);
export const charFromHexCode = (hex: string) => charFromCode(parseInt(hex, 16));
export const padStart = (value: string, length: number, padChar: string) => {
let padding = '';
for (let idx = 0, len = length - value.length; idx < len; idx++) {
padding += padChar;
}
return padding + value;
};
export const copyStringIntoBuffer = (
str: string,
buffer: Uint8Array,
offset: number,
): number => {
const length = str.length;
for (let idx = 0; idx < length; idx++) {
buffer[offset++] = str.charCodeAt(idx);
}
return length;
};
export const addRandomSuffix = (prefix: string, suffixLength = 4) =>
`${prefix}-${Math.floor(Math.random() * 10 ** suffixLength)}`;
export const escapeRegExp = (str: string) =>
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
export const cleanText = (text: string) =>
text.replace(/\t|\u0085|\u2028|\u2029/g, ' ').replace(/[\b\v]/g, '');
export const escapedNewlineChars = ['\\n', '\\f', '\\r', '\\u000B'];
export const newlineChars = ['\n', '\f', '\r', '\u000B'];
export const isNewlineChar = (text: string) => /^[\n\f\r\u000B]$/.test(text);
export const lineSplit = (text: string) => text.split(/[\n\f\r\u000B]/);
export const mergeLines = (text: string) =>
text.replace(/[\n\f\r\u000B]/g, ' ');
// JavaScript's String.charAt() method doesn work on strings containing UTF-16
// characters (with high and low surrogate pairs), such as 💩 (poo emoji). This
// `charAtIndex()` function does.
//
// Credit: https://github.com/mathiasbynens/String.prototype.at/blob/master/at.js#L14-L48
export const charAtIndex = (text: string, index: number): [string, number] => {
// Get the first code unit and code unit value
const cuFirst = text.charCodeAt(index);
let cuSecond: number;
const nextIndex = index + 1;
let length = 1;
if (
// Check if it's the start of a surrogate pair.
cuFirst >= 0xd800 &&
cuFirst <= 0xdbff && // high surrogate
text.length > nextIndex // there is a next code unit
) {
cuSecond = text.charCodeAt(nextIndex);
if (cuSecond >= 0xdc00 && cuSecond <= 0xdfff) length = 2; // low surrogate
}
return [text.slice(index, index + length), length];
};
export const charSplit = (text: string) => {
const chars: string[] = [];
for (let idx = 0, len = text.length; idx < len; ) {
const [c, cLen] = charAtIndex(text, idx);
chars.push(c);
idx += cLen;
}
return chars;
};
const buildWordBreakRegex = (wordBreaks: string[]) => {
const newlineCharUnion = escapedNewlineChars.join('|');
const escapedRules: string[] = ['$'];
for (let idx = 0, len = wordBreaks.length; idx < len; idx++) {
const wordBreak = wordBreaks[idx];
if (isNewlineChar(wordBreak)) {
throw new TypeError(`\`wordBreak\` must not include ${newlineCharUnion}`);
}
escapedRules.push(wordBreak === '' ? '.' : escapeRegExp(wordBreak));
}
const breakRules = escapedRules.join('|');
return new RegExp(`(${newlineCharUnion})|((.*?)(${breakRules}))`, 'gm');
};
export const breakTextIntoLines = (
text: string,
wordBreaks: string[],
maxWidth: number,
computeWidthOfText: (t: string) => number,
): string[] => {
const regex = buildWordBreakRegex(wordBreaks);
const words = cleanText(text).match(regex)!;
let currLine = '';
let currWidth = 0;
const lines: string[] = [];
const pushCurrLine = () => {
if (currLine !== '') lines.push(currLine);
currLine = '';
currWidth = 0;
};
for (let idx = 0, len = words.length; idx < len; idx++) {
const word = words[idx];
if (isNewlineChar(word)) {
pushCurrLine();
} else {
const width = computeWidthOfText(word);
if (currWidth + width > maxWidth) pushCurrLine();
currLine += word;
currWidth += width;
}
}
pushCurrLine();
return lines;
};
// See section "7.9.4 Dates" of the PDF specification
const dateRegex = /^D:(\d\d\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([+\-Z])?(\d\d)?'?(\d\d)?'?$/;
export const parseDate = (dateStr: string): Date | undefined => {
const match = dateStr.match(dateRegex);
if (!match) return undefined;
const [
,
year,
month = '01',
day = '01',
hours = '00',
mins = '00',
secs = '00',
offsetSign = 'Z',
offsetHours = '00',
offsetMins = '00',
] = match;
// http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
const tzOffset =
offsetSign === 'Z' ? 'Z' : `${offsetSign}${offsetHours}:${offsetMins}`;
const date = new Date(
`${year}-${month}-${day}T${hours}:${mins}:${secs}${tzOffset}`,
);
return date;
};
export const findLastMatch = (value: string, regex: RegExp) => {
let position = 0;
let lastMatch: RegExpMatchArray | undefined;
while (position < value.length) {
const match = value.substring(position).match(regex);
if (!match) return { match: lastMatch, pos: position };
lastMatch = match;
position += (match.index ?? 0) + match[0].length;
}
return { match: lastMatch, pos: position };
};

386
frontend/node_modules/pdf-lib/src/utils/unicode.ts generated vendored Normal file
View file

@ -0,0 +1,386 @@
import { toHexString } from 'src/utils/strings';
/**
* Encodes a string to UTF-8.
*
* @param input The string to be encoded.
* @param byteOrderMark Whether or not a byte order marker (BOM) should be added
* to the start of the encoding. (default `true`)
* @returns A Uint8Array containing the UTF-8 encoding of the input string.
*
* -----------------------------------------------------------------------------
*
* JavaScript strings are composed of Unicode code points. Code points are
* integers in the range 0 to 1,114,111 (0x10FFFF). When serializing a string,
* it must be encoded as a sequence of words. A word is typically 8, 16, or 32
* bytes in size. As such, Unicode defines three encoding forms: UTF-8, UTF-16,
* and UTF-32. These encoding forms are described in the Unicode standard [1].
* This function implements the UTF-8 encoding form.
*
* -----------------------------------------------------------------------------
*
* In UTF-8, each code point is mapped to a sequence of 1, 2, 3, or 4 bytes.
* Note that the logic which defines this mapping is slightly convoluted, and
* not as straightforward as the mapping logic for UTF-16 or UTF-32. The UTF-8
* mapping logic is as follows [2]:
*
* If a code point is in the range U+0000..U+007F, then view it as a 7-bit
* integer: 0bxxxxxxx. Map the code point to 1 byte with the first high order
* bit set to 0:
*
* b1=0b0xxxxxxx
*
* If a code point is in the range U+0080..U+07FF, then view it as an 11-bit
* integer: 0byyyyyxxxxxx. Map the code point to 2 bytes with the first 5 bits
* of the code point stored in the first byte, and the last 6 bits stored in
* the second byte:
*
* b1=0b110yyyyy b2=0b10xxxxxx
*
* If a code point is in the range U+0800..U+FFFF, then view it as a 16-bit
* integer, 0bzzzzyyyyyyxxxxxx. Map the code point to 3 bytes with the first
* 4 bits stored in the first byte, the next 6 bits stored in the second byte,
* and the last 6 bits in the third byte:
*
* b1=0b1110zzzz b2=0b10yyyyyy b3=0b10xxxxxx
*
* If a code point is in the range U+10000...U+10FFFF, then view it as a
* 21-bit integer, 0bvvvzzzzzzyyyyyyxxxxxx. Map the code point to 4 bytes with
* the first 3 bits stored in the first byte, the next 6 bits stored in the
* second byte, the next 6 bits stored in the third byte, and the last 6 bits
* stored in the fourth byte:
*
* b1=0b11110xxx b2=0b10zzzzzz b3=0b10yyyyyy b4=0b10xxxxxx
*
* -----------------------------------------------------------------------------
*
* It is important to note, when iterating through the code points of a string
* in JavaScript, that if a character is encoded as a surrogate pair it will
* increase the string's length by 2 instead of 1 [4]. For example:
*
* ```
* > 'a'.length
* 1
* > '💩'.length
* 2
* > '語'.length
* 1
* > 'a💩語'.length
* 4
* ```
*
* The results of the above example are explained by the fact that the
* characters 'a' and '語' are not represented by surrogate pairs, but '💩' is.
*
* Because of this idiosyncrasy in JavaScript's string implementation and APIs,
* we must "jump" an extra index after encoding a character as a surrogate
* pair. In practice, this means we must increment the index of our for loop by
* 2 if we encode a surrogate pair, and 1 in all other cases.
*
* -----------------------------------------------------------------------------
*
* References:
* - [1] https://www.unicode.org/versions/Unicode12.0.0/UnicodeStandard-12.0.pdf
* 3.9 Unicode Encoding Forms - UTF-8
* - [2] http://www.herongyang.com/Unicode/UTF-8-UTF-8-Encoding.html
* - [3] http://www.herongyang.com/Unicode/UTF-8-UTF-8-Encoding-Algorithm.html
* - [4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#Description
*
*/
export const utf8Encode = (input: string, byteOrderMark = true): Uint8Array => {
const encoded = [];
if (byteOrderMark) encoded.push(0xef, 0xbb, 0xbf);
for (let idx = 0, len = input.length; idx < len; ) {
const codePoint = input.codePointAt(idx)!;
// One byte encoding
if (codePoint < 0x80) {
const byte1 = codePoint & 0x7f;
encoded.push(byte1);
idx += 1;
}
// Two byte encoding
else if (codePoint < 0x0800) {
const byte1 = ((codePoint >> 6) & 0x1f) | 0xc0;
const byte2 = (codePoint & 0x3f) | 0x80;
encoded.push(byte1, byte2);
idx += 1;
}
// Three byte encoding
else if (codePoint < 0x010000) {
const byte1 = ((codePoint >> 12) & 0x0f) | 0xe0;
const byte2 = ((codePoint >> 6) & 0x3f) | 0x80;
const byte3 = (codePoint & 0x3f) | 0x80;
encoded.push(byte1, byte2, byte3);
idx += 1;
}
// Four byte encoding (surrogate pair)
else if (codePoint < 0x110000) {
const byte1 = ((codePoint >> 18) & 0x07) | 0xf0;
const byte2 = ((codePoint >> 12) & 0x3f) | 0x80;
const byte3 = ((codePoint >> 6) & 0x3f) | 0x80;
const byte4 = ((codePoint >> 0) & 0x3f) | 0x80;
encoded.push(byte1, byte2, byte3, byte4);
idx += 2;
}
// Should never reach this case
else throw new Error(`Invalid code point: 0x${toHexString(codePoint)}`);
}
return new Uint8Array(encoded);
};
/**
* Encodes a string to UTF-16.
*
* @param input The string to be encoded.
* @param byteOrderMark Whether or not a byte order marker (BOM) should be added
* to the start of the encoding. (default `true`)
* @returns A Uint16Array containing the UTF-16 encoding of the input string.
*
* -----------------------------------------------------------------------------
*
* JavaScript strings are composed of Unicode code points. Code points are
* integers in the range 0 to 1,114,111 (0x10FFFF). When serializing a string,
* it must be encoded as a sequence of words. A word is typically 8, 16, or 32
* bytes in size. As such, Unicode defines three encoding forms: UTF-8, UTF-16,
* and UTF-32. These encoding forms are described in the Unicode standard [1].
* This function implements the UTF-16 encoding form.
*
* -----------------------------------------------------------------------------
*
* In UTF-16, each code point is mapped to one or two 16-bit integers. The
* UTF-16 mapping logic is as follows [2]:
*
* If a code point is in the range U+0000..U+FFFF, then map the code point to
* a 16-bit integer with the most significant byte first.
*
* If a code point is in the range U+10000..U+10000, then map the code point
* to two 16-bit integers. The first integer should contain the high surrogate
* and the second integer should contain the low surrogate. Both surrogates
* should be written with the most significant byte first.
*
* -----------------------------------------------------------------------------
*
* It is important to note, when iterating through the code points of a string
* in JavaScript, that if a character is encoded as a surrogate pair it will
* increase the string's length by 2 instead of 1 [4]. For example:
*
* ```
* > 'a'.length
* 1
* > '💩'.length
* 2
* > '語'.length
* 1
* > 'a💩語'.length
* 4
* ```
*
* The results of the above example are explained by the fact that the
* characters 'a' and '語' are not represented by surrogate pairs, but '💩' is.
*
* Because of this idiosyncrasy in JavaScript's string implementation and APIs,
* we must "jump" an extra index after encoding a character as a surrogate
* pair. In practice, this means we must increment the index of our for loop by
* 2 if we encode a surrogate pair, and 1 in all other cases.
*
* -----------------------------------------------------------------------------
*
* References:
* - [1] https://www.unicode.org/versions/Unicode12.0.0/UnicodeStandard-12.0.pdf
* 3.9 Unicode Encoding Forms - UTF-8
* - [2] http://www.herongyang.com/Unicode/UTF-16-UTF-16-Encoding.html
* - [3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length#Description
*
*/
export const utf16Encode = (
input: string,
byteOrderMark = true,
): Uint16Array => {
const encoded = [];
if (byteOrderMark) encoded.push(0xfeff);
for (let idx = 0, len = input.length; idx < len; ) {
const codePoint = input.codePointAt(idx)!;
// Two byte encoding
if (codePoint < 0x010000) {
encoded.push(codePoint);
idx += 1;
}
// Four byte encoding (surrogate pair)
else if (codePoint < 0x110000) {
encoded.push(highSurrogate(codePoint), lowSurrogate(codePoint));
idx += 2;
}
// Should never reach this case
else throw new Error(`Invalid code point: 0x${toHexString(codePoint)}`);
}
return new Uint16Array(encoded);
};
/**
* Returns `true` if the `codePoint` is within the
* Basic Multilingual Plane (BMP). Code points inside the BMP are not encoded
* with surrogate pairs.
* @param codePoint The code point to be evaluated.
*
* Reference: https://en.wikipedia.org/wiki/UTF-16#Description
*/
export const isWithinBMP = (codePoint: number) =>
codePoint >= 0 && codePoint <= 0xffff;
/**
* Returns `true` if the given `codePoint` is valid and must be represented
* with a surrogate pair when encoded.
* @param codePoint The code point to be evaluated.
*
* Reference: https://en.wikipedia.org/wiki/UTF-16#Description
*/
export const hasSurrogates = (codePoint: number) =>
codePoint >= 0x010000 && codePoint <= 0x10ffff;
// From Unicode 3.0 spec, section 3.7:
// http://unicode.org/versions/Unicode3.0.0/ch03.pdf
export const highSurrogate = (codePoint: number) =>
Math.floor((codePoint - 0x10000) / 0x400) + 0xd800;
// From Unicode 3.0 spec, section 3.7:
// http://unicode.org/versions/Unicode3.0.0/ch03.pdf
export const lowSurrogate = (codePoint: number) =>
((codePoint - 0x10000) % 0x400) + 0xdc00;
enum ByteOrder {
BigEndian = 'BigEndian',
LittleEndian = 'LittleEndian',
}
const REPLACEMENT = '<27>'.codePointAt(0)!;
/**
* Decodes a Uint8Array of data to a string using UTF-16.
*
* Note that this function attempts to recover from erronous input by
* inserting the replacement character (<EFBFBD>) to mark invalid code points
* and surrogate pairs.
*
* @param input A Uint8Array containing UTF-16 encoded data
* @param byteOrderMark Whether or not a byte order marker (BOM) should be read
* at the start of the encoding. (default `true`)
* @returns The decoded string.
*/
export const utf16Decode = (
input: Uint8Array,
byteOrderMark = true,
): string => {
// Need at least 2 bytes of data in UTF-16 encodings
if (input.length <= 1) return String.fromCodePoint(REPLACEMENT);
const byteOrder = byteOrderMark ? readBOM(input) : ByteOrder.BigEndian;
// Skip byte order mark if needed
let idx = byteOrderMark ? 2 : 0;
const codePoints: number[] = [];
while (input.length - idx >= 2) {
const first = decodeValues(input[idx++], input[idx++], byteOrder);
if (isHighSurrogate(first)) {
if (input.length - idx < 2) {
// Need at least 2 bytes left for the low surrogate that is required
codePoints.push(REPLACEMENT);
} else {
const second = decodeValues(input[idx++], input[idx++], byteOrder);
if (isLowSurrogate(second)) {
codePoints.push(first, second);
} else {
// Low surrogates should always follow high surrogates
codePoints.push(REPLACEMENT);
}
}
} else if (isLowSurrogate(first)) {
// High surrogates should always come first since `decodeValues()`
// accounts for the byte ordering
idx += 2;
codePoints.push(REPLACEMENT);
} else {
codePoints.push(first);
}
}
// There shouldn't be extra byte(s) left over
if (idx < input.length) codePoints.push(REPLACEMENT);
return String.fromCodePoint(...codePoints);
};
/**
* Returns `true` if the given `codePoint` is a high surrogate.
* @param codePoint The code point to be evaluated.
*
* Reference: https://en.wikipedia.org/wiki/UTF-16#Description
*/
const isHighSurrogate = (codePoint: number) =>
codePoint >= 0xd800 && codePoint <= 0xdbff;
/**
* Returns `true` if the given `codePoint` is a low surrogate.
* @param codePoint The code point to be evaluated.
*
* Reference: https://en.wikipedia.org/wiki/UTF-16#Description
*/
const isLowSurrogate = (codePoint: number) =>
codePoint >= 0xdc00 && codePoint <= 0xdfff;
/**
* Decodes the given utf-16 values first and second using the specified
* byte order.
* @param first The first byte of the encoding.
* @param second The second byte of the encoding.
* @param byteOrder The byte order of the encoding.
* Reference: https://en.wikipedia.org/wiki/UTF-16#Examples
*/
const decodeValues = (first: number, second: number, byteOrder: ByteOrder) => {
// Append the binary representation of the preceding byte by shifting the
// first one 8 to the left and than applying a bitwise or-operator to append
// the second one.
if (byteOrder === ByteOrder.LittleEndian) return (second << 8) | first;
if (byteOrder === ByteOrder.BigEndian) return (first << 8) | second;
throw new Error(`Invalid byteOrder: ${byteOrder}`);
};
/**
* Returns whether the given array contains a byte order mark for the
* UTF-16BE or UTF-16LE encoding. If it has neither, BigEndian is assumed.
*
* Reference: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-16
*
* @param bytes The byte array to be evaluated.
*/
// prettier-ignore
const readBOM = (bytes: Uint8Array): ByteOrder => (
hasUtf16BigEndianBOM(bytes) ? ByteOrder.BigEndian
: hasUtf16LittleEndianBOM(bytes) ? ByteOrder.LittleEndian
: ByteOrder.BigEndian
);
const hasUtf16BigEndianBOM = (bytes: Uint8Array) =>
bytes[0] === 0xfe && bytes[1] === 0xff;
const hasUtf16LittleEndianBOM = (bytes: Uint8Array) =>
bytes[0] === 0xff && bytes[1] === 0xfe;
export const hasUtf16BOM = (bytes: Uint8Array) =>
hasUtf16BigEndianBOM(bytes) || hasUtf16LittleEndianBOM(bytes);

228
frontend/node_modules/pdf-lib/src/utils/validators.ts generated vendored Normal file
View file

@ -0,0 +1,228 @@
/* tslint:disable:ban-types */
import { values as objectValues } from 'src/utils/objects';
export const backtick = (val: any) => `\`${val}\``;
export const singleQuote = (val: any) => `'${val}'`;
type Primitive = string | number | boolean | undefined | null;
// prettier-ignore
const formatValue = (value: any) => {
const type = typeof value;
if (type ==='string') return singleQuote(value);
else if (type ==='undefined') return backtick(value);
else return value;
};
export const createValueErrorMsg = (
value: any,
valueName: string,
values: Primitive[],
) => {
const allowedValues = new Array(values.length);
for (let idx = 0, len = values.length; idx < len; idx++) {
const v = values[idx];
allowedValues[idx] = formatValue(v);
}
const joinedValues = allowedValues.join(' or ');
// prettier-ignore
return `${backtick(valueName)} must be one of ${joinedValues}, but was actually ${formatValue(value)}`;
};
export const assertIsOneOf = (
value: any,
valueName: string,
allowedValues: Primitive[] | { [key: string]: Primitive },
) => {
if (!Array.isArray(allowedValues)) {
allowedValues = objectValues(allowedValues);
}
for (let idx = 0, len = allowedValues.length; idx < len; idx++) {
if (value === allowedValues[idx]) return;
}
throw new TypeError(createValueErrorMsg(value, valueName, allowedValues));
};
export const assertIsOneOfOrUndefined = (
value: any,
valueName: string,
allowedValues: Primitive[] | { [key: string]: Primitive },
) => {
if (!Array.isArray(allowedValues)) {
allowedValues = objectValues(allowedValues);
}
assertIsOneOf(value, valueName, allowedValues.concat(undefined));
};
export const assertIsSubset = (
values: any[],
valueName: string,
allowedValues: Primitive[] | { [key: string]: Primitive },
) => {
if (!Array.isArray(allowedValues)) {
allowedValues = objectValues(allowedValues);
}
for (let idx = 0, len = values.length; idx < len; idx++) {
assertIsOneOf(values[idx], valueName, allowedValues);
}
};
export const getType = (val: any) => {
if (val === null) return 'null';
if (val === undefined) return 'undefined';
if (typeof val === 'string') return 'string';
if (isNaN(val)) return 'NaN';
if (typeof val === 'number') return 'number';
if (typeof val === 'boolean') return 'boolean';
if (typeof val === 'symbol') return 'symbol';
if (typeof val === 'bigint') return 'bigint';
if (val.constructor && val.constructor.name) return val.constructor.name;
if (val.name) return val.name;
if (val.constructor) return String(val.constructor);
return String(val);
};
export type TypeDescriptor =
| 'null'
| 'undefined'
| 'string'
| 'number'
| 'boolean'
| 'symbol'
| 'bigint'
| DateConstructor
| ArrayConstructor
| Uint8ArrayConstructor
| ArrayBufferConstructor
| FunctionConstructor
| [Function, string];
export const isType = (value: any, type: TypeDescriptor) => {
if (type === 'null') return value === null;
if (type === 'undefined') return value === undefined;
if (type === 'string') return typeof value === 'string';
if (type === 'number') return typeof value === 'number' && !isNaN(value);
if (type === 'boolean') return typeof value === 'boolean';
if (type === 'symbol') return typeof value === 'symbol';
if (type === 'bigint') return typeof value === 'bigint';
if (type === Date) return value instanceof Date;
if (type === Array) return value instanceof Array;
if (type === Uint8Array) return value instanceof Uint8Array;
if (type === ArrayBuffer) return value instanceof ArrayBuffer;
if (type === Function) return value instanceof Function;
return value instanceof (type as [Function, string])[0];
};
export const createTypeErrorMsg = (
value: any,
valueName: string,
types: TypeDescriptor[],
) => {
const allowedTypes = new Array(types.length);
for (let idx = 0, len = types.length; idx < len; idx++) {
const type = types[idx];
if (type === 'null') allowedTypes[idx] = backtick('null');
if (type === 'undefined') allowedTypes[idx] = backtick('undefined');
if (type === 'string') allowedTypes[idx] = backtick('string');
else if (type === 'number') allowedTypes[idx] = backtick('number');
else if (type === 'boolean') allowedTypes[idx] = backtick('boolean');
else if (type === 'symbol') allowedTypes[idx] = backtick('symbol');
else if (type === 'bigint') allowedTypes[idx] = backtick('bigint');
else if (type === Array) allowedTypes[idx] = backtick('Array');
else if (type === Uint8Array) allowedTypes[idx] = backtick('Uint8Array');
else if (type === ArrayBuffer) allowedTypes[idx] = backtick('ArrayBuffer');
else allowedTypes[idx] = backtick((type as [Function, string])[1]);
}
const joinedTypes = allowedTypes.join(' or ');
// prettier-ignore
return `${backtick(valueName)} must be of type ${joinedTypes}, but was actually of type ${backtick(getType(value))}`;
};
export const assertIs = (
value: any,
valueName: string,
types: TypeDescriptor[],
) => {
for (let idx = 0, len = types.length; idx < len; idx++) {
if (isType(value, types[idx])) return;
}
throw new TypeError(createTypeErrorMsg(value, valueName, types));
};
export const assertOrUndefined = (
value: any,
valueName: string,
types: TypeDescriptor[],
) => {
assertIs(value, valueName, types.concat('undefined'));
};
export const assertEachIs = (
values: any[],
valueName: string,
types: TypeDescriptor[],
) => {
for (let idx = 0, len = values.length; idx < len; idx++) {
assertIs(values[idx], valueName, types);
}
};
export const assertRange = (
value: any,
valueName: string,
min: number,
max: number,
) => {
assertIs(value, valueName, ['number']);
assertIs(min, 'min', ['number']);
assertIs(max, 'max', ['number']);
max = Math.max(min, max);
if (value < min || value > max) {
// prettier-ignore
throw new Error(`${backtick(valueName)} must be at least ${min} and at most ${max}, but was actually ${value}`);
}
};
export const assertRangeOrUndefined = (
value: any,
valueName: string,
min: number,
max: number,
) => {
assertIs(value, valueName, ['number', 'undefined']);
if (typeof value === 'number') assertRange(value, valueName, min, max);
};
export const assertMultiple = (
value: any,
valueName: string,
multiplier: number,
) => {
assertIs(value, valueName, ['number']);
if (value % multiplier !== 0) {
// prettier-ignore
throw new Error(`${backtick(valueName)} must be a multiple of ${multiplier}, but was actually ${value}`);
}
};
export const assertInteger = (value: any, valueName: string) => {
if (!Number.isInteger(value)) {
throw new Error(
`${backtick(valueName)} must be an integer, but was actually ${value}`,
);
}
};
export const assertPositive = (value: number, valueName: string) => {
if (![1, 0].includes(Math.sign(value))) {
// prettier-ignore
throw new Error(`${backtick(valueName)} must be a positive number or 0, but was actually ${value}`);
}
};