@promptbook/utils
Version:
Promptbook: Turn your company's scattered knowledge into AI ready books
1,597 lines (1,544 loc) • 124 kB
JavaScript
import spaceTrim$1, { spaceTrim as spaceTrim$2 } from 'spacetrim';
import { basename } from 'path';
import { randomBytes } from 'crypto';
// ⚠️ WARNING: This code has been generated so that any manual changes will be overwritten
/**
* The version of the Book language
*
* @generated
* @see https://github.com/webgptorg/book
*/
const BOOK_LANGUAGE_VERSION = '1.0.0';
/**
* The version of the Promptbook engine
*
* @generated
* @see https://github.com/webgptorg/promptbook
*/
const PROMPTBOOK_ENGINE_VERSION = '0.103.0-27';
/**
* TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
* Note: [💞] Ignore a discrepancy between file name and entity name
*/
/**
* @private util of `@promptbook/color`
* @de
*/
class TakeChain {
constructor(value) {
this.value = value;
}
then(callback) {
const newValue = callback(this.value);
return take(newValue);
}
}
/**
* A function that takes an initial value and returns a proxy object with chainable methods.
*
* @param {*} initialValue - The initial value.
* @returns {Proxy<WithTake<TValue>>} - A proxy object with a `take` method.
*
* @private util of `@promptbook/color`
* @deprecated [🤡] Use some better functional library instead of `TakeChain`
*/
function take(initialValue) {
if (initialValue instanceof TakeChain) {
return initialValue;
}
return new Proxy(new TakeChain(initialValue), {
get(target, property, receiver) {
if (Reflect.has(target, property)) {
return Reflect.get(target, property, receiver);
}
else if (Reflect.has(initialValue, property)) {
return Reflect.get(initialValue, property, receiver);
}
else {
return undefined;
}
},
});
}
/**
* 🎨 List of all 140 color names which are supported by CSS
*
* @public exported from `@promptbook/color`
*/
const CSS_COLORS = {
transparent: 'rgba(0,0,0,0)',
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
};
/**
* Note: [💞] Ignore a discrepancy between file name and entity name
*/
/**
* Validates that a channel value is a valid number within the range of 0 to 255.
* Throws an error if the value is not valid.
*
* @param channelName - The name of the channel being validated.
* @param value - The value of the channel to validate.
* @throws Will throw an error if the value is not a valid channel number.
*
* @private util of `@promptbook/color`
*/
function checkChannelValue(channelName, value) {
if (typeof value !== 'number') {
throw new Error(`${channelName} channel value is not number but ${typeof value}`);
}
if (isNaN(value)) {
throw new Error(`${channelName} channel value is NaN`);
}
if (Math.round(value) !== value) {
throw new Error(`${channelName} channel is not whole number, it is ${value}`);
}
if (value < 0) {
throw new Error(`${channelName} channel is lower than 0, it is ${value}`);
}
if (value > 255) {
throw new Error(`${channelName} channel is greater than 255, it is ${value}`);
}
}
/**
* TODO: [🧠][🚓] Is/which combination it better to use asserts/check, validate or is utility function?
*/
/**
* Color object represents an RGB color with alpha channel
*
* Note: There is no fromObject/toObject because the most logical way to serialize color is as a hex string (#009edd)
*
* @public exported from `@promptbook/color`
*/
class Color {
/**
* Creates a new Color instance from miscellaneous formats
* - It can receive Color instance and just return the same instance
* - It can receive color in string format for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`
*
* Note: This is not including fromImage because detecting color from an image is heavy task which requires async stuff and we cannot safely determine with overloading if return value will be a promise
*
* @param color
* @returns Color object
*/
static from(color) {
if (color instanceof Color) {
return take(color);
}
else if (Color.isColor(color)) {
return take(color);
}
else if (typeof color === 'string') {
return Color.fromString(color);
}
else {
console.error({ color });
throw new Error(`Can not create color from given object`);
}
}
/**
* Creates a new Color instance from miscellaneous string formats
*
* @param color as a string for example `#009edd`, `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`, `hsl(197.1,100%,43.3%)`, `red`, `darkgrey`,...
* @returns Color object
*/
static fromString(color) {
if (CSS_COLORS[color]) {
return Color.fromString(CSS_COLORS[color]);
// -----
}
else if (Color.isHexColorString(color)) {
return Color.fromHex(color);
// -----
}
else if (/^hsl\(\s*(\d+)\s*,\s*(\d+(?:\.\d+)?%)\s*,\s*(\d+(?:\.\d+)?%)\)$/.test(color)) {
return Color.fromHsl(color);
// -----
}
else if (/^rgb\((\s*[0-9-.%]+\s*,?){3}\)$/.test(color)) {
// TODO: [0] Should be fromRgbString and fromRgbaString one or two functions
return Color.fromRgbString(color);
// -----
}
else if (/^rgba\((\s*[0-9-.%]+\s*,?){4}\)$/.test(color)) {
return Color.fromRgbaString(color);
// -----
}
else {
throw new Error(`Can not create a new Color instance from string "${color}".`);
}
}
/**
* Gets common color
*
* @param key as a css string like `midnightblue`
* @returns Color object
*/
static get(key) {
if (!CSS_COLORS[key]) {
throw new Error(`"${key}" is not a common css color.`);
}
return Color.fromString(CSS_COLORS[key]);
}
/**
* Creates a new Color instance from average color of given image
*
* @param image as a source for example `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjYJh39z8ABJgCe/ZvAS4AAAAASUVORK5CYII=`
* @returns Color object
*/
static async fromImage(image) {
return Color.fromHex(`#009edd`);
}
/**
* Creates a new Color instance from color in hex format
*
* @param color in hex for example `#009edd`, `009edd`, `#555`,...
* @returns Color object
*/
static fromHex(hex) {
const hexOriginal = hex;
if (hex.startsWith('#')) {
hex = hex.substring(1);
}
if (hex.length === 3) {
return Color.fromHex3(hex);
}
if (hex.length === 6) {
return Color.fromHex6(hex);
}
if (hex.length === 8) {
return Color.fromHex8(hex);
}
throw new Error(`Can not parse color from hex string "${hexOriginal}"`);
}
/**
* Creates a new Color instance from color in hex format with 3 color digits (without alpha channel)
*
* @param color in hex for example `09d`
* @returns Color object
*/
static fromHex3(hex) {
const r = parseInt(hex.substr(0, 1), 16) * 16;
const g = parseInt(hex.substr(1, 1), 16) * 16;
const b = parseInt(hex.substr(2, 1), 16) * 16;
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in hex format with 6 color digits (without alpha channel)
*
* @param color in hex for example `009edd`
* @returns Color object
*/
static fromHex6(hex) {
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in hex format with 8 color digits (with alpha channel)
*
* @param color in hex for example `009edd`
* @returns Color object
*/
static fromHex8(hex) {
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const a = parseInt(hex.substr(6, 2), 16);
return take(new Color(r, g, b, a));
}
/**
* Creates a new Color instance from color in hsl format
*
* @param color as a hsl for example `hsl(197.1,100%,43.3%)`
* @returns Color object
*/
static fromHsl(hsl) {
const match = hsl.match(/^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/);
if (!match) {
throw new Error(`Invalid hsl string format: "${hsl}"`);
}
const h = parseFloat(match[1]);
const s = parseFloat(match[2]) / 100;
const l = parseFloat(match[3]) / 100;
// HSL to RGB conversion
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = l - c / 2;
let r1 = 0, g1 = 0, b1 = 0;
if (h >= 0 && h < 60) {
r1 = c;
g1 = x;
b1 = 0;
}
else if (h >= 60 && h < 120) {
r1 = x;
g1 = c;
b1 = 0;
}
else if (h >= 120 && h < 180) {
r1 = 0;
g1 = c;
b1 = x;
}
else if (h >= 180 && h < 240) {
r1 = 0;
g1 = x;
b1 = c;
}
else if (h >= 240 && h < 300) {
r1 = x;
g1 = 0;
b1 = c;
}
else if (h >= 300 && h < 360) {
r1 = c;
g1 = 0;
b1 = x;
}
const r = Math.round((r1 + m) * 255);
const g = Math.round((g1 + m) * 255);
const b = Math.round((b1 + m) * 255);
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in rgb format
*
* @param color as a rgb for example `rgb(0,158,221)`, `rgb(0%,62%,86.7%)`
* @returns Color object
*/
static fromRgbString(rgb) {
const match = rgb.match(/^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/);
if (!match) {
throw new Error(`Invalid rgb string format: "${rgb}"`);
}
const parseChannel = (value) => {
if (value.endsWith('%')) {
// Percentage value
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
// Numeric value
return Math.round(parseFloat(value));
}
};
const r = parseChannel(match[1]);
const g = parseChannel(match[2]);
const b = parseChannel(match[3]);
return take(new Color(r, g, b));
}
/**
* Creates a new Color instance from color in rbga format
*
* @param color as a rgba for example `rgba(0,158,221,0.5)`, `rgb(0%,62%,86.7%,50%)`
* @returns Color object
*/
static fromRgbaString(rgba) {
const match = rgba.match(/^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/);
if (!match) {
throw new Error(`Invalid rgba string format: "${rgba}"`);
}
const parseChannel = (value) => {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
return Math.round(parseFloat(value));
}
};
const parseAlpha = (value) => {
if (value.endsWith('%')) {
const percent = parseFloat(value);
return Math.round((percent / 100) * 255);
}
else {
const alphaFloat = parseFloat(value);
// If alpha is between 0 and 1, treat as float
if (alphaFloat <= 1) {
return Math.round(alphaFloat * 255);
}
// Otherwise, treat as 0-255
return Math.round(alphaFloat);
}
};
const r = parseChannel(match[1]);
const g = parseChannel(match[2]);
const b = parseChannel(match[3]);
const a = parseAlpha(match[4]);
return take(new Color(r, g, b, a));
}
/**
* Creates a new Color for color channels values
*
* @param red number from 0 to 255
* @param green number from 0 to 255
* @param blue number from 0 to 255
* @param alpha number from 0 (transparent) to 255 (opaque = default)
* @returns Color object
*/
static fromValues(red, green, blue, alpha = 255) {
return take(new Color(red, green, blue, alpha));
}
/**
* Checks if the given value is a valid Color object.
*
* @param {unknown} value - The value to check.
* @return {value is WithTake<Color>} Returns true if the value is a valid Color object, false otherwise.
*/
static isColor(value) {
if (typeof value !== 'object') {
return false;
}
if (value === null) {
return false;
}
if (typeof value.red !== 'number' ||
typeof value.green !== 'number' ||
typeof value.blue !== 'number' ||
typeof value.alpha !== 'number') {
return false;
}
if (typeof value.then !== 'function') {
return false;
}
return true;
}
/**
* Checks if the given value is a valid hex color string
*
* @param value - value to check
* @returns true if the value is a valid hex color string (e.g., `#009edd`, `#fff`, etc.)
*/
static isHexColorString(value) {
return typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value);
}
/**
* Creates new Color object
*
* Note: Consider using one of static methods like `from` or `fromString`
*
* @param red number from 0 to 255
* @param green number from 0 to 255
* @param blue number from 0 to 255
* @param alpha number from 0 (transparent) to 255 (opaque)
*/
constructor(red, green, blue, alpha = 255) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
checkChannelValue('Red', red);
checkChannelValue('Green', green);
checkChannelValue('Blue', blue);
checkChannelValue('Alpha', alpha);
}
/**
* Shortcut for `red` property
* Number from 0 to 255
* @alias red
*/
get r() {
return this.red;
}
/**
* Shortcut for `green` property
* Number from 0 to 255
* @alias green
*/
get g() {
return this.green;
}
/**
* Shortcut for `blue` property
* Number from 0 to 255
* @alias blue
*/
get b() {
return this.blue;
}
/**
* Shortcut for `alpha` property
* Number from 0 (transparent) to 255 (opaque)
* @alias alpha
*/
get a() {
return this.alpha;
}
/**
* Shortcut for `alpha` property
* Number from 0 (transparent) to 255 (opaque)
* @alias alpha
*/
get opacity() {
return this.alpha;
}
/**
* Shortcut for 1-`alpha` property
*/
get transparency() {
return 255 - this.alpha;
}
clone() {
return take(new Color(this.red, this.green, this.blue, this.alpha));
}
toString() {
return this.toHex();
}
toHex() {
if (this.alpha === 255) {
return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
.toString(16)
.padStart(2, '0')}`;
}
else {
return `#${this.red.toString(16).padStart(2, '0')}${this.green.toString(16).padStart(2, '0')}${this.blue
.toString(16)
.padStart(2, '0')}${this.alpha.toString(16).padStart(2, '0')}`;
}
}
toRgb() {
if (this.alpha === 255) {
return `rgb(${this.red}, ${this.green}, ${this.blue})`;
}
else {
return `rgba(${this.red}, ${this.green}, ${this.blue}, ${Math.round((this.alpha / 255) * 100)}%)`;
}
}
toHsl() {
throw new Error(`Getting HSL is not implemented`);
}
}
/**
* TODO: [🥻] Split Color class and color type
* TODO: For each method a corresponding static method should be created
* Like clone can be done by color.clone() OR Color.clone(color)
* TODO: Probably as an independent LIB OR add to LIB xyzt (ask @roseckyj)
* TODO: !! Transfer back to Collboard (whole directory)
* TODO: Maybe [🏌️♂️] change ACRY toString => (toHex) toRgb when there will be toRgb and toRgba united
* TODO: Convert getters to methods - getters only for values
* TODO: Write tests
* TODO: Getters for alpha, opacity, transparency, r, b, g, h, s, l, a,...
* TODO: [0] Should be fromRgbString and fromRgbaString one or two functions + one or two regex
* TODO: Use rgb, rgba, hsl for testing and parsing with the same regex
* TODO: Regex for rgb, rgba, hsl does not support all options like deg, rad, turn,...
* TODO: Convolution matrix
* TODO: Maybe connect with textures
*/
/**
* Converts HSL values to RGB values
*
* @param hue [0-1]
* @param saturation [0-1]
* @param lightness [0-1]
* @returns [red, green, blue] [0-255]
*
* @private util of `@promptbook/color`
*/
function hslToRgb(hue, saturation, lightness) {
let red;
let green;
let blue;
if (saturation === 0) {
// achromatic
red = lightness;
green = lightness;
blue = lightness;
}
else {
// TODO: Extract to separate function
const hue2rgb = (p, q, t) => {
if (t < 0)
t += 1;
if (t > 1)
t -= 1;
if (t < 1 / 6)
return p + (q - p) * 6 * t;
if (t < 1 / 2)
return q;
if (t < 2 / 3)
return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
const p = 2 * lightness - q;
red = hue2rgb(p, q, hue + 1 / 3);
green = hue2rgb(p, q, hue);
blue = hue2rgb(p, q, hue - 1 / 3);
}
return [Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255)];
}
/**
* TODO: Properly name all used internal variables
*/
/**
* Converts RGB values to HSL values
*
* @param red [0-255]
* @param green [0-255]
* @param blue [0-255]
* @returns [hue, saturation, lightness] [0-1]
*
* @private util of `@promptbook/color`
*/
function rgbToHsl(red, green, blue) {
red /= 255;
green /= 255;
blue /= 255;
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
let hue;
let saturation;
const lightness = (max + min) / 2;
if (max === min) {
// achromatic
hue = 0;
saturation = 0;
}
else {
const d = max - min;
saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case red:
hue = (green - blue) / d + (green < blue ? 6 : 0);
break;
case green:
hue = (blue - red) / d + 2;
break;
case blue:
hue = (red - green) / d + 4;
break;
default:
hue = 0;
}
hue /= 6;
}
return [hue, saturation, lightness];
}
/**
* TODO: Properly name all used internal variables
*/
/**
* Makes color transformer which lighten the given color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function lighten(amount) {
return ({ red, green, blue, alpha }) => {
const [h, s, lInitial] = rgbToHsl(red, green, blue);
let l = lInitial + amount;
l = Math.max(0, Math.min(l, 1)); // Replace lodash clamp with Math.max and Math.min
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
/**
* TODO: Maybe implement by mix+hsl
*/
/**
* Calculates distance between two colors
*
* @param color1 first color
* @param color2 second color
*
* Note: This function is inefficient. Use colorDistanceSquared instead if possible.
*
* @public exported from `@promptbook/color`
*/
/**
* Calculates distance between two colors without square root
*
* @param color1 first color
* @param color2 second color
*
* @public exported from `@promptbook/color`
*/
function colorDistanceSquared(color1, color2) {
const rmean = (color1.red + color2.red) / 2;
const r = color1.red - color2.red;
const g = color1.green - color2.green;
const b = color1.blue - color2.blue;
const weightR = 2 + rmean / 256;
const weightG = 4.0;
const weightB = 2 + (255 - rmean) / 256;
const distance = weightR * r * r + weightG * g * g + weightB * b * b;
return distance;
}
/**
* Makes color transformer which finds the nearest color from the given list
*
* @param colors array of colors to choose from
*
* @public exported from `@promptbook/color`
*/
function nearest(...colors) {
return (color) => {
const distances = colors.map((c) => colorDistanceSquared(c, color));
const minDistance = Math.min(...distances);
const minIndex = distances.indexOf(minDistance);
const nearestColor = colors[minIndex];
return nearestColor;
};
}
/**
* Color transformer which returns the negative color
*
* @public exported from `@promptbook/color`
*/
function negative(color) {
const r = 255 - color.red;
const g = 255 - color.green;
const b = 255 - color.blue;
return Color.fromValues(r, g, b, color.alpha);
}
/**
* Makes color transformer which finds the furthest color from the given list
*
* @param colors array of colors to choose from
*
* @public exported from `@promptbook/color`
*/
function furthest(...colors) {
return (color) => {
const furthestColor = negative(nearest(...colors.map(negative))(color));
return furthestColor;
};
}
/**
* Makes color transformer which finds the best text color (black or white) for the given background color
*
* @public exported from `@promptbook/color`
*/
furthest(Color.get('white'), Color.from('black'));
/**
* Makes color transformer which returns a grayscale version of the color
*
* @param amount from 0 to 1
*
* @public exported from `@promptbook/color`
*/
function grayscale(amount) {
return ({ red, green, blue, alpha }) => {
const average = (red + green + blue) / 3;
red = Math.round(average * amount + red * (1 - amount));
green = Math.round(average * amount + green * (1 - amount));
blue = Math.round(average * amount + blue * (1 - amount));
return Color.fromValues(red, green, blue, alpha);
};
}
/**
* Makes color transformer which saturate the given color
*
* @param amount from -1 to 1
*
* @public exported from `@promptbook/color`
*/
function saturate(amount) {
return ({ red, green, blue, alpha }) => {
const [h, sInitial, l] = rgbToHsl(red, green, blue);
let s = sInitial + amount;
s = Math.max(0, Math.min(s, 1));
const [r, g, b] = hslToRgb(h, s, l);
return Color.fromValues(r, g, b, alpha);
};
}
/**
* TODO: Maybe implement by mix+hsl
*/
/**
* Name for the Promptbook
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const NAME = `Promptbook`;
/**
* Email of the responsible person
*
* @public exported from `@promptbook/core`
*/
const ADMIN_EMAIL = 'pavol@ptbk.io';
/**
* Name of the responsible person for the Promptbook on GitHub
*
* @public exported from `@promptbook/core`
*/
const ADMIN_GITHUB_NAME = 'hejny';
// <- TODO: [🐊] Pick the best claim
/**
* Color of the Promptbook
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
const PROMPTBOOK_COLOR = Color.fromHex('#79EAFD');
// <- TODO: [🧠][🈵] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Colors for syntax highlighting in the `<BookEditor/>`
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
({
TITLE: Color.fromHex('#244EA8'),
LINE: Color.fromHex('#eeeeee'),
COMMITMENT: Color.fromHex('#DA0F78'),
PARAMETER: Color.fromHex('#8e44ad'),
});
// <- TODO: [🧠][🈵] Using `Color` here increases the package size approx 3kb, maybe remove it
/**
* Chat color of the Promptbook (in chat)
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
PROMPTBOOK_COLOR.then(lighten(0.1)).then(saturate(0.9)).then(grayscale(0.9));
// <- TODO: [🧠][🈵] Using `Color` and `lighten`, `saturate`,... here increases the package size approx 3kb, maybe remove it
/**
* Color of the user (in chat)
*
* TODO: [🗽] Unite branding and make single place for it
*
* @public exported from `@promptbook/core`
*/
Color.fromHex('#1D4ED8');
// <- TODO: [🧠] Better system for generator warnings - not always "code" and "by `@promptbook/cli`"
/**
* The maximum number of iterations for a loops
*
* @private within the repository - too low-level in comparison with other `MAX_...`
*/
const LOOP_LIMIT = 1000;
/**
* Strings to represent various values in the context of parameter values
*
* @public exported from `@promptbook/utils`
*/
const VALUE_STRINGS = {
empty: '(nothing; empty string)',
null: '(no value; null)',
undefined: '(unknown value; undefined)',
nan: '(not a number; NaN)',
infinity: '(infinity; ∞)',
negativeInfinity: '(negative infinity; -∞)',
unserializable: '(unserializable value)',
circular: '(circular JSON)',
};
/**
* Small number limit
*
* @public exported from `@promptbook/utils`
*/
const SMALL_NUMBER = 0.001;
// <- TODO: [🧜♂️]
/**
* Default settings for parsing and generating CSV files in Promptbook.
*
* @public exported from `@promptbook/core`
*/
Object.freeze({
delimiter: ',',
quoteChar: '"',
newline: '\n',
skipEmptyLines: true,
});
/**
* API request timeout in milliseconds
* Can be overridden via API_REQUEST_TIMEOUT environment variable
*
* @public exported from `@promptbook/core`
*/
parseInt(process.env.API_REQUEST_TIMEOUT || '90000');
/**
* Note: [💞] Ignore a discrepancy between file name and entity name
* TODO: [🧠][🧜♂️] Maybe join remoteServerUrl and path into single value
*/
/**
* Orders JSON object by keys
*
* @returns The same type of object as the input re-ordered
* @public exported from `@promptbook/utils`
*/
function orderJson(options) {
const { value, order } = options;
const orderedValue = {
...(order === undefined ? {} : Object.fromEntries(order.map((key) => [key, undefined]))),
...value,
};
return orderedValue;
}
/**
* Freezes the given object and all its nested objects recursively
*
* Note: `$` is used to indicate that this function is not a pure function - it mutates given object
* Note: This function mutates the object and returns the original (but mutated-deep-freezed) object
*
* @returns The same object as the input, but deeply frozen
* @public exported from `@promptbook/utils`
*/
function $deepFreeze(objectValue) {
if (Array.isArray(objectValue)) {
return Object.freeze(objectValue.map((item) => $deepFreeze(item)));
}
const propertyNames = Object.getOwnPropertyNames(objectValue);
for (const propertyName of propertyNames) {
const value = objectValue[propertyName];
if (value && typeof value === 'object') {
$deepFreeze(value);
}
}
Object.freeze(objectValue);
return objectValue;
}
/**
* TODO: [🧠] Is there a way how to meaningfully test this utility
*/
/**
* Make error report URL for the given error
*
* @private private within the repository
*/
function getErrorReportUrl(error) {
const report = {
title: `🐜 Error report from ${NAME}`,
body: spaceTrim$1((block) => `
\`${error.name || 'Error'}\` has occurred in the [${NAME}], please look into it @${ADMIN_GITHUB_NAME}.
\`\`\`
${block(error.message || '(no error message)')}
\`\`\`
## More info:
- **Promptbook engine version:** ${PROMPTBOOK_ENGINE_VERSION}
- **Book language version:** ${BOOK_LANGUAGE_VERSION}
- **Time:** ${new Date().toISOString()}
<details>
<summary>Stack trace:</summary>
## Stack trace:
\`\`\`stacktrace
${block(error.stack || '(empty)')}
\`\`\`
</details>
`),
};
const reportUrl = new URL(`https://github.com/webgptorg/promptbook/issues/new`);
reportUrl.searchParams.set('labels', 'bug');
reportUrl.searchParams.set('assignees', ADMIN_GITHUB_NAME);
reportUrl.searchParams.set('title', report.title);
reportUrl.searchParams.set('body', report.body);
return reportUrl;
}
/**
* This error type indicates that the error should not happen and its last check before crashing with some other error
*
* @public exported from `@promptbook/core`
*/
class UnexpectedError extends Error {
constructor(message) {
super(spaceTrim$2((block) => `
${block(message)}
Note: This error should not happen.
It's probably a bug in the pipeline collection
Please report issue:
${block(getErrorReportUrl(new Error(message)).href)}
Or contact us on ${ADMIN_EMAIL}
`));
this.name = 'UnexpectedError';
Object.setPrototypeOf(this, UnexpectedError.prototype);
}
}
/**
* This error type indicates that somewhere in the code non-Error object was thrown and it was wrapped into the `WrappedError`
*
* @public exported from `@promptbook/core`
*/
class WrappedError extends Error {
constructor(whatWasThrown) {
const tag = `[🤮]`;
console.error(tag, whatWasThrown);
super(spaceTrim$2(`
Non-Error object was thrown
Note: Look for ${tag} in the console for more details
Please report issue on ${ADMIN_EMAIL}
`));
this.name = 'WrappedError';
Object.setPrototypeOf(this, WrappedError.prototype);
}
}
/**
* Helper used in catch blocks to assert that the error is an instance of `Error`
*
* @param whatWasThrown Any object that was thrown
* @returns Nothing if the error is an instance of `Error`
* @throws `WrappedError` or `UnexpectedError` if the error is not standard
*
* @private within the repository
*/
function assertsError(whatWasThrown) {
// Case 1: Handle error which was rethrown as `WrappedError`
if (whatWasThrown instanceof WrappedError) {
const wrappedError = whatWasThrown;
throw wrappedError;
}
// Case 2: Handle unexpected errors
if (whatWasThrown instanceof UnexpectedError) {
const unexpectedError = whatWasThrown;
throw unexpectedError;
}
// Case 3: Handle standard errors - keep them up to consumer
if (whatWasThrown instanceof Error) {
return;
}
// Case 4: Handle non-standard errors - wrap them into `WrappedError` and throw
throw new WrappedError(whatWasThrown);
}
/**
* Checks if the value is [🚉] serializable as JSON
* If not, throws an UnexpectedError with a rich error message and tracking
*
* - Almost all primitives are serializable BUT:
* - `undefined` is not serializable
* - `NaN` is not serializable
* - Objects and arrays are serializable if all their properties are serializable
* - Functions are not serializable
* - Circular references are not serializable
* - `Date` objects are not serializable
* - `Map` and `Set` objects are not serializable
* - `RegExp` objects are not serializable
* - `Error` objects are not serializable
* - `Symbol` objects are not serializable
* - And much more...
*
* @throws UnexpectedError if the value is not serializable as JSON
* @public exported from `@promptbook/utils`
*/
function checkSerializableAsJson(options) {
const { value, name, message } = options;
if (value === undefined) {
throw new UnexpectedError(`${name} is undefined`);
}
else if (value === null) {
return;
}
else if (typeof value === 'boolean') {
return;
}
else if (typeof value === 'number' && !isNaN(value)) {
return;
}
else if (typeof value === 'string') {
return;
}
else if (typeof value === 'symbol') {
throw new UnexpectedError(`${name} is symbol`);
}
else if (typeof value === 'function') {
throw new UnexpectedError(`${name} is function`);
}
else if (typeof value === 'object' && Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
checkSerializableAsJson({ name: `${name}[${i}]`, value: value[i], message });
}
}
else if (typeof value === 'object') {
if (value instanceof Date) {
throw new UnexpectedError(spaceTrim$1((block) => `
\`${name}\` is Date
Use \`string_date_iso8601\` instead
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
else if (value instanceof Map) {
throw new UnexpectedError(`${name} is Map`);
}
else if (value instanceof Set) {
throw new UnexpectedError(`${name} is Set`);
}
else if (value instanceof RegExp) {
throw new UnexpectedError(`${name} is RegExp`);
}
else if (value instanceof Error) {
throw new UnexpectedError(spaceTrim$1((block) => `
\`${name}\` is unserialized Error
Use function \`serializeError\`
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
else {
for (const [subName, subValue] of Object.entries(value)) {
if (subValue === undefined) {
// Note: undefined in object is serializable - it is just omitted
continue;
}
checkSerializableAsJson({ name: `${name}.${subName}`, value: subValue, message });
}
try {
JSON.stringify(value); // <- TODO: [0]
}
catch (error) {
assertsError(error);
throw new UnexpectedError(spaceTrim$1((block) => `
\`${name}\` is not serializable
${block(error.stack || error.message)}
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
/*
TODO: [0] Is there some more elegant way to check circular references?
const seen = new Set();
const stack = [{ value }];
while (stack.length > 0) {
const { value } = stack.pop()!;
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
throw new UnexpectedError(`${name} has circular reference`);
}
seen.add(value);
if (Array.isArray(value)) {
stack.push(...value.map((value) => ({ value })));
} else {
stack.push(...Object.values(value).map((value) => ({ value })));
}
}
}
*/
return;
}
}
else {
throw new UnexpectedError(spaceTrim$1((block) => `
\`${name}\` is unknown type
Additional message for \`${name}\`:
${block(message || '(nothing)')}
`));
}
}
/**
* TODO: Can be return type more type-safe? like `asserts options.value is JsonValue`
* TODO: [🧠][main] !!3 In-memory cache of same values to prevent multiple checks
* Note: [🐠] This is how `checkSerializableAsJson` + `isSerializableAsJson` together can just retun true/false or rich error message
*/
/**
* Creates a deep clone of the given object
*
* Note: [🔂] This function is idempotent.
* Note: This method only works for objects that are fully serializable to JSON and do not contain functions, Dates, or special types.
*
* @param objectValue The object to clone.
* @returns A deep, writable clone of the input object.
* @public exported from `@promptbook/utils`
*/
function deepClone(objectValue) {
return JSON.parse(JSON.stringify(objectValue));
/*
TODO: [🧠] Is there a better implementation?
> const propertyNames = Object.getOwnPropertyNames(objectValue);
> for (const propertyName of propertyNames) {
> const value = (objectValue as really_any)[propertyName];
> if (value && typeof value === 'object') {
> deepClone(value);
> }
> }
> return Object.assign({}, objectValue);
*/
}
/**
* TODO: [🧠] Is there a way how to meaningfully test this utility
*/
/**
* Utility to export a JSON object from a function
*
* 1) Checks if the value is serializable as JSON
* 2) Makes a deep clone of the object
* 2) Orders the object properties
* 2) Deeply freezes the cloned object
*
* Note: This function does not mutates the given object
*
* @returns The same type of object as the input but read-only and re-ordered
* @public exported from `@promptbook/utils`
*/
function exportJson(options) {
const { name, value, order, message } = options;
checkSerializableAsJson({ name, value, message });
const orderedValue =
// TODO: Fix error "Type instantiation is excessively deep and possibly infinite."
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
order === undefined
? deepClone(value)
: orderJson({
value: value,
// <- Note: checkSerializableAsJson asserts that the value is serializable as JSON
order: order,
});
$deepFreeze(orderedValue);
return orderedValue;
}
/**
* TODO: [🧠] Is there a way how to meaningfully test this utility
*/
/**
* Nonce which is used for replacing things in strings
*
* @private within the repository
*/
const REPLACING_NONCE = 'ptbkauk42kV2dzao34faw7FudQUHYPtW';
/**
* Placeholder value indicating a parameter is missing its value.
*
* @private within the repository
*/
const RESERVED_PARAMETER_MISSING_VALUE = 'MISSING-' + REPLACING_NONCE;
/**
* Placeholder value indicating a parameter is restricted and cannot be used directly.
*
* @private within the repository
*/
const RESERVED_PARAMETER_RESTRICTED = 'RESTRICTED-' + REPLACING_NONCE;
/**
* The names of the parameters that are reserved for special purposes
*
* @public exported from `@promptbook/core`
*/
const RESERVED_PARAMETER_NAMES = exportJson({
name: 'RESERVED_PARAMETER_NAMES',
message: `The names of the parameters that are reserved for special purposes`,
value: [
'content',
'context',
'knowledge',
'examples',
'modelName',
'currentDate',
// <- TODO: list here all command names
// <- TODO: Add more like 'date', 'modelName',...
// <- TODO: Add [emoji] + instructions ACRY when adding new reserved parameter
],
});
/**
* Note: [💞] Ignore a discrepancy between file name and entity name
*/
/**
* Normalizes a given text to camelCase format.
*
* @param text The text to be normalized.
* @param _isFirstLetterCapital Whether the first letter should be capitalized.
* @returns The camelCase formatted string.
* @example 'helloWorld'
* @example 'iLovePromptbook'
* @public exported from `@promptbook/utils`
*/
function normalizeTo_camelCase(text, _isFirstLetterCapital = false) {
let charType;
let lastCharType = null;
let normalizedName = '';
for (const char of text) {
let normalizedChar;
if (/^[a-z]$/.test(char)) {
charType = 'LOWERCASE';
normalizedChar = char;
}
else if (/^[A-Z]$/.test(char)) {
charType = 'UPPERCASE';
normalizedChar = char.toLowerCase();
}
else if (/^[0-9]$/.test(char)) {
charType = 'NUMBER';
normalizedChar = char;
}
else {
charType = 'OTHER';
normalizedChar = '';
}
if (!lastCharType) {
if (_isFirstLetterCapital) {
normalizedChar = normalizedChar.toUpperCase(); //TODO: DRY
}
}
else if (charType !== lastCharType &&
!(charType === 'LOWERCASE' && lastCharType === 'UPPERCASE') &&
!(lastCharType === 'NUMBER') &&
!(charType === 'NUMBER')) {
normalizedChar = normalizedChar.toUpperCase(); //TODO: [🌺] DRY
}
normalizedName += normalizedChar;
lastCharType = charType;
}
return normalizedName;
}
/**
* TODO: [🌺] Use some intermediate util splitWords
*/
/**
* Tests if given string is valid file path.
*
* Note: This does not check if the file exists only if the path is valid
* @public exported from `@promptbook/utils`
*/
function isValidFilePath(filename) {
if (typeof filename !== 'string') {
return false;
}
if (filename.split('\n').length > 1) {
return false;
}
// Normalize slashes early so heuristics can detect path-like inputs
const filenameSlashes = filename.replace(/\\/g, '/');
// Reject strings that look like sentences (informational text)
// Heuristic: contains multiple spaces and ends with a period, or contains typical sentence punctuation
// But skip this heuristic if the string looks like a path (contains '/' or starts with a drive letter)
if (filename.trim().length > 60 && // long enough to be a sentence
/[.!?]/.test(filename) && // contains sentence punctuation
filename.split(' ').length > 8 && // has many words
!/\/|^[A-Z]:/i.test(filenameSlashes) // do NOT treat as sentence if looks like a path
) {
return false;
}
// Absolute Unix path: /hello.txt
if (/^(\/)/i.test(filenameSlashes)) {
// console.log(filename, 'Absolute Unix path: /hello.txt');
return true;
}
// Absolute Windows path: C:/ or C:\ (allow spaces and multiple dots in filename)
if (/^[A-Z]:\/.+$/i.test(filenameSlashes)) {
// console.log(filename, 'Absolute Windows path: /hello.txt');
return true;
}
// Relative path: ./hello.txt
if (/^(\.\.?\/)+/i.test(filenameSlashes)) {
// console.log(filename, 'Relative path: ./hello.txt');
return true;
}
// Allow paths like foo/hello
if (/^[^/]+\/[^/]+/i.test(filenameSlashes)) {
// console.log(filename, 'Allow paths like foo/hello');
return true;
}
// Allow paths like hello.book
if (/^[^/]+\.[^/]+$/i.test(filenameSlashes)) {
// console.log(filename, 'Allow paths like hello.book');
return true;
}
return false;
}
/**
* TODO: [🍏] Implement for MacOs
*/
/**
* Tests if given string is valid URL.
*
* Note: [🔂] This function is idempotent.
* Note: Dataurl are considered perfectly valid.
* Note: There are two similar functions:
* - `isValidUrl` which tests any URL
* - `isValidPipelineUrl` *(this one)* which tests just promptbook URL
*
* @public exported from `@promptbook/utils`
*/
function isValidUrl(url) {
if (typeof url !== 'string') {
return false;
}
try {
if (url.startsWith('blob:')) {
url = url.replace(/^blob:/, '');
}
const urlObject = new URL(url /* because fail is handled */);
if (!['http:', 'https:', 'data:'].includes(urlObject.protocol)) {
return false;
}
return true;
}
catch (error) {
return false;
}
}
const defaultDiacriticsRemovalMap = [
{
base: 'A',
letters: '\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F',
},
{ base: 'AA', letters: '\uA732' },
{ base: 'AE', letters: '\u00C6\u01FC\u01E2' },
{ base: 'AO', letters: '\uA734' },
{ base: 'AU', letters: '\uA736' },
{ base: 'AV', letters: '\uA738\uA73A' },
{ base: 'AY', letters: '\uA73C' },
{
base: 'B',
letters: '\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181',
},
{
base: 'C',
letters: '\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E',
},
{
base: 'D',
letters: '\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0',
},
{ base: 'DZ', letters: '\u01F1\u01C4' },
{ base: 'Dz', letters: '\u01F2\u01C5' },
{
base: 'E',
letters: '\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E',
},
{ base: 'F', letters: '\u0046\u24BB\uFF26\u1E1E\u0191\uA77B' },
{
base: 'G',
letters: '\u0047\u24BC\