UNPKG

@promptbook/pdf

Version:

Promptbook: Create persistent AI agents that turn your company's scattered knowledge into action

1,634 lines (1,588 loc) โ€ข 337 kB
import { mkdir, rm, readFile } from 'fs/promises'; import { spaceTrim as spaceTrim$1 } from 'spacetrim'; import { SHA256 } from 'crypto-js'; import hexEncoder from 'crypto-js/enc-hex'; import { basename, join, dirname, isAbsolute } from 'path'; import { randomBytes } from 'crypto'; import { Subject } from 'rxjs'; import { forTime } from 'waitasecond'; import sha256 from 'crypto-js/sha256'; import { lookup, extension } from 'mime-types'; import { parse, unparse } from 'papaparse'; // โš ๏ธ 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 = '2.0.0'; /** * The version of the Promptbook engine * * @generated * @see https://github.com/webgptorg/promptbook */ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-107'; /** * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name */ /** * Just says that the variable is not used but should be kept * No side effects. * * Note: It can be useful for: * * 1) Suppressing eager optimization of unused imports * 2) Suppressing eslint errors of unused variables in the tests * 3) Keeping the type of the variable for type testing * * @param value any values * @returns void * * @private within the repository */ function keepUnused(...valuesToKeep) { } /** * Trims string from all 4 sides * * Note: This is a re-exported function from the `spacetrim` package which is * Developed by same author @hejny as this package * * @see https://github.com/hejny/spacetrim#usage * * @public exported from `@promptbook/utils` */ const spaceTrim = spaceTrim$1; /** * Just marks a place of place where should be something implemented * No side effects. * * Note: It can be useful suppressing eslint errors of unused variables * * @param value any values * @returns void * * @private within the repository */ function TODO_USE(...value) { } /** * Class implementing take chain. * * @de * * @private util of `@promptbook/color` */ 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. * @deprecated [๐Ÿคก] Use some better functional library instead of `TakeChain` * * @private util of `@promptbook/color` */ 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 = { promptbook: '#79EAFD', 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}`); } } /** * Shared immutable channel storage and serialization helpers for `Color`. * * @private base class of Color */ class ColorValue { 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(this.createColor(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`); } } /** * 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.) * * @private function of Color */ function isHexColorString(value) { return (typeof value === 'string' && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)); } /** * Constant for short hex lengths. */ const SHORT_HEX_LENGTHS = new Set([3, 4]); /** * Constant for long hex lengths. */ const LONG_HEX_LENGTHS = new Set([6, 8]); /** * Parses a hex string into RGBA channel values. * * @param hex - Hex value such as `#09d`, `009edd`, `#009eddff`. * @returns RGBA channel values. * * @private function of Color */ function parseHexColor(hex) { const sanitized = hex.startsWith('#') ? hex.substring(1) : hex; const throwInvalidHex = () => { throw new Error(`Can not parse color from hex string "${hex}"`); }; if (SHORT_HEX_LENGTHS.has(sanitized.length)) { return { red: parseShortHexChannel(sanitized.charAt(0), throwInvalidHex), green: parseShortHexChannel(sanitized.charAt(1), throwInvalidHex), blue: parseShortHexChannel(sanitized.charAt(2), throwInvalidHex), alpha: sanitized.length === 4 ? parseShortHexChannel(sanitized.charAt(3), throwInvalidHex) : 255, }; } if (LONG_HEX_LENGTHS.has(sanitized.length)) { return { red: parseLongHexChannel(sanitized, 0, throwInvalidHex), green: parseLongHexChannel(sanitized, 2, throwInvalidHex), blue: parseLongHexChannel(sanitized, 4, throwInvalidHex), alpha: sanitized.length === 8 ? parseLongHexChannel(sanitized, 6, throwInvalidHex) : 255, }; } return throwInvalidHex(); } /** * Parses short hex channel. */ function parseShortHexChannel(char, onError) { if (!char) { return onError(); } const parsed = parseInt(char, 16); if (Number.isNaN(parsed)) { return onError(); } return parsed * 16; } /** * Parses long hex channel. */ function parseLongHexChannel(hex, start, onError) { const segment = hex.substr(start, 2); if (segment.length < 2) { return onError(); } const parsed = parseInt(segment, 16); if (Number.isNaN(parsed)) { return onError(); } return parsed; } /** * Pattern matching hsl. */ const HSL_REGEX = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/; /** * Parses an HSL string into RGBA channel values. * * @param hsl - HSL string such as `hsl(197.1, 100%, 43.3%)`. * @returns RGBA channel values. * * @private function of Color */ function parseHslColor(hsl) { const match = hsl.match(HSL_REGEX); if (!match) { throw new Error(`Invalid hsl string format: "${hsl}"`); } const hue = parseFloat(match[1]); const saturation = parseFloat(match[2]) / 100; const lightness = parseFloat(match[3]) / 100; const { red, green, blue } = convertHslToRgb(hue, saturation, lightness); return { red, green, blue, alpha: 255, }; } /** * Handles convert hsl to Rgb. */ function convertHslToRgb(h, s, l) { 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; let g1 = 0; let b1 = 0; if (h >= 0 && h < 60) { r1 = c; g1 = x; } else if (h >= 60 && h < 120) { r1 = x; g1 = c; } else if (h >= 120 && h < 180) { g1 = c; b1 = x; } else if (h >= 180 && h < 240) { g1 = x; b1 = c; } else if (h >= 240 && h < 300) { r1 = x; b1 = c; } else if (h >= 300 && h < 360) { r1 = c; b1 = x; } return { red: Math.round((r1 + m) * 255), green: Math.round((g1 + m) * 255), blue: Math.round((b1 + m) * 255), }; } /** * Pattern matching RGB. */ const RGB_REGEX = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/; /** * Pattern matching rgba. */ const RGBA_REGEX = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/; /** * Parses an RGB string into RGBA channel values. * * @param rgb - RGB string such as `rgb(0%, 62%, 86.7%)`. * @returns RGBA channel values. * * @private function of Color */ function parseRgbColor(rgb) { const match = rgb.match(RGB_REGEX); if (!match) { throw new Error(`Invalid rgb string format: "${rgb}"`); } return { red: parseChannelValue(match[1]), green: parseChannelValue(match[2]), blue: parseChannelValue(match[3]), alpha: 255, }; } /** * Parses an RGBA string into RGBA channel values. * * @param rgba - RGBA string such as `rgba(0, 158, 221, 0.5)`. * @returns RGBA channel values. * * @private function of Color */ function parseRgbaColor(rgba) { const match = rgba.match(RGBA_REGEX); if (!match) { throw new Error(`Invalid rgba string format: "${rgba}"`); } return { red: parseChannelValue(match[1]), green: parseChannelValue(match[2]), blue: parseChannelValue(match[3]), alpha: parseAlphaValue(match[4]), }; } /** * Parses channel value. */ function parseChannelValue(value) { if (value.endsWith('%')) { const percent = parseFloat(value); return Math.round((percent / 100) * 255); } return Math.round(parseFloat(value)); } /** * Parses alpha value. */ function parseAlphaValue(value) { if (value.endsWith('%')) { const percent = parseFloat(value); return Math.round((percent / 100) * 255); } const parsed = parseFloat(value); if (parsed <= 1) { return Math.round(parsed * 255); } return Math.round(parsed); } /** * Pattern matching hsl regex. * * @private function of Color */ const HSL_REGEX_PATTERN = /^hsl\(\s*([0-9.]+)\s*,\s*([0-9.]+)%\s*,\s*([0-9.]+)%\s*\)$/; /** * Pattern matching RGB regex. * * @private function of Color */ const RGB_REGEX_PATTERN = /^rgb\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/; /** * Pattern matching rgba regex. * * @private function of Color */ const RGBA_REGEX_PATTERN = /^rgba\(\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*,\s*([0-9.%-]+)\s*\)$/; /** * Parses a supported color string into RGBA channels. * * @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 RGBA channel values. * * @private function of Color */ function parseColorString(color) { const trimmed = color.trim(); const cssColor = CSS_COLORS[trimmed]; if (cssColor) { return parseColorString(cssColor); } else if (isHexColorString(trimmed)) { return parseHexColor(trimmed); } if (HSL_REGEX_PATTERN.test(trimmed)) { return parseHslColor(trimmed); } else if (RGB_REGEX_PATTERN.test(trimmed)) { return parseRgbColor(trimmed); } else if (RGBA_REGEX_PATTERN.test(trimmed)) { return parseRgbaColor(trimmed); } else { throw new Error(`Can not create a new Color instance from string "${trimmed}".`); } } /** * 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 extends ColorValue { /** * 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, _isSingleValue = false) { if (color === '') { throw new Error(`Can not create color from empty string`); } else if (color instanceof Color) { return take(color); } else if (Color.isColor(color)) { return take(color); } else if (typeof color === 'string') { try { return Color.fromString(color); } catch (error) { // <- Note: Can not use `assertsError(error)` here because it causes circular dependency if (_isSingleValue) { throw error; } const parts = color.split(/[\s+,;|]/); if (parts.length > 0) { return Color.from(parts[0].trim(), true); } else { throw new Error(`Can not create color from given string "${color}"`); } } } else { console.error({ color }); throw new Error(`Can not create color from given object`); } } /** * Creates a new Color instance from miscellaneous formats * It just does not throw error when it fails, it returns PROMPTBOOK_COLOR instead * * @param color * @returns Color object */ static fromSafe(color) { try { return Color.from(color); } catch (error) { // <- Note: Can not use `assertsError(error)` here because it causes circular dependency console.warn(spaceTrim((block) => ` Color.fromSafe error: ${block(error.message)} Returning default PROMPTBOOK_COLOR. `)); return Color.fromString('promptbook'); } } /** * 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) { return Color.fromColorChannels(parseColorString(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) { return Color.fromColorChannels(parseHexColor(hex)); } /** * 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) { return Color.fromColorChannels(parseHslColor(hsl)); } /** * 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) { return Color.fromColorChannels(parseRgbColor(rgb)); } /** * 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) { return Color.fromColorChannels(parseRgbaColor(rgba)); } /** * 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 Color.fromColorChannels({ 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 isHexColorString(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) { super(red, green, blue, alpha); } createColor(red, green, blue, alpha) { return new Color(red, green, blue, alpha); } static fromColorChannels({ red, green, blue, alpha }) { return take(new Color(red, green, blue, alpha)); } } /** * 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); }; } /** * 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 /** * 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 /** * Stable root directory used for Promptbook-owned temporary files and caches. * * @private internal utility for Promptbook temporary folders */ const PROMPTBOOK_TEMPORARY_DIRECTORY = '.promptbook'; /** * Builds one normalized project-relative path inside Promptbook's dedicated temporary root. * * The returned path intentionally uses `/` separators so the same helper can be reused from * Node.js and edge-safe code without depending on the Node `path` module. * * @private internal utility for Promptbook temporary folders */ function getPromptbookTemporaryPath(...pathSegments) { const normalizedPathSegments = pathSegments.flatMap(splitPathSegments).filter(Boolean); return [PROMPTBOOK_TEMPORARY_DIRECTORY, ...normalizedPathSegments].join('/'); } /** * Normalizes one raw path segment into slash-delimited pieces without empty items. */ function splitPathSegments(pathSegment) { return pathSegment .split(/[\\/]+/u) .map((segment) => segment.trim()) .filter(Boolean); } // Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name /** * Returns the same value that is passed as argument. * No side effects. * * Note: It can be useful for: * * 1) Leveling indentation * 2) Putting always-true or always-false conditions without getting eslint errors * * @param value any values * @returns the same values * * @private within the repository */ function just(value) { if (value === undefined) { return undefined; } return value; } /** * 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.fromString('promptbook'); // <- 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'), SEPARATOR: Color.fromHex('#cccccc'), COMMITMENT: Color.fromHex('#DA0F78'), NOTE_COMMITMENT: Color.fromHex('#8080807e'), TODO_COMMITMENT_TEXT: Color.fromHex('#000000'), TODO_COMMITMENT_BACKGROUND: Color.fromHex('#FFEB3B'), PARAMETER: Color.fromHex('#8e44ad'), CODE_BLOCK: Color.fromHex('#7700ffff'), }); // <- 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: [๐Ÿง ][๐Ÿˆต] Using `Color` here increases the package size approx 3kb, maybe remove it /** * When the title is not provided, the default title is used * * @public exported from `@promptbook/core` */ const DEFAULT_BOOK_TITLE = `๐Ÿ™ Untitled agent`; /** * Maximum file size limit * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB /** * Threshold value that determines when a dataset is considered "big" * and may require special handling or optimizations * * For example, when error occurs in one item of the big dataset, it will not fail the whole pipeline * * @public exported from `@promptbook/core` */ const BIG_DATASET_TRESHOLD = 50; /** * Placeholder text used to represent a placeholder value of failed operation * * @public exported from `@promptbook/core` */ const FAILED_VALUE_PLACEHOLDER = '!?'; // <- 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; /** * Short time interval to prevent race conditions in milliseconds * * @private within the repository - too low-level in comparison with other `MAX_...` */ const IMMEDIATE_TIME = 10; /** * The maximum length of the (generated) filename * * @public exported from `@promptbook/core` */ const MAX_FILENAME_LENGTH = 30; /** * Strategy for caching the intermediate results for knowledge sources * * @public exported from `@promptbook/core` */ const DEFAULT_INTERMEDIATE_FILES_STRATEGY = 'HIDE_AND_KEEP'; // <- TODO: [๐Ÿ˜ก] Change to 'VISIBLE' /** * The maximum number of (LLM) tasks running in parallel * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_PARALLEL_COUNT = 5; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] /** * The maximum number of attempts to execute LLM task before giving up * * @public exported from `@promptbook/core` */ const DEFAULT_MAX_EXECUTION_ATTEMPTS = 7; // <- TODO: [๐Ÿคนโ€โ™‚๏ธ] // <- TODO: [๐Ÿ•] Make also `AGENTS_DIRNAME_ALTERNATIVES` /** * Where to store the temporary downloads * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_DOWNLOAD_CACHE_DIRNAME = `./${getPromptbookTemporaryPath('download-cache')}`; /** * Where to store the cache of executions for promptbook CLI * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ `./${getPromptbookTemporaryPath('execution-cache')}`; /** * Where to store the scrape cache * * Note: When the folder does not exist, it is created recursively * * @public exported from `@promptbook/core` */ const DEFAULT_SCRAPE_CACHE_DIRNAME = `./${getPromptbookTemporaryPath('scrape-cache')}`; // <- TODO: [๐Ÿงœโ€โ™‚๏ธ] /** * Default settings for parsing and generating CSV files in Promptbook. * * @public exported from `@promptbook/core` */ const DEFAULT_CSV_SETTINGS = Object.freeze({ delimiter: ',', quoteChar: '"', newline: '\n', skipEmptyLines: true, }); /** * Controls whether verbose logging is enabled by default throughout the application. * * @public exported from `@promptbook/core` */ let DEFAULT_IS_VERBOSE = false; /** * Controls whether auto-installation of dependencies is enabled by default. * * @public exported from `@promptbook/core` */ const DEFAULT_IS_AUTO_INSTALLED = false; /** * Default simulated duration for a task in milliseconds (used for progress reporting) * * @public exported from `@promptbook/core` */ const DEFAULT_TASK_SIMULATED_DURATION_MS = 5 * 60 * 1000; // 5 minutes /** * 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'); /** * Indicates whether pipeline logic validation is enabled. When true, the pipeline logic is checked for consistency. * * @private within the repository */ const IS_PIPELINE_LOGIC_VALIDATED = just( /**/ // Note: In normal situations, we check the pipeline logic: true); /** * Note: [๐Ÿ’ž] Ignore a discrepancy between file name and entity name * TODO: [๐Ÿง ][๐Ÿงœโ€โ™‚๏ธ] Maybe join remoteServerUrl and path into single value */ /** * This error type indicates that you try to use a feature that is not available in the current environment * * @public exported from `@promptbook/core` */ class EnvironmentMismatchError extends Error { constructor(message) { super(message); this.name = 'EnvironmentMismatchError'; Object.setPrototypeOf(this, EnvironmentMismatchError.prototype); } } /** * This error indicates that the promptbook can not retrieve knowledge from external sources * * @public exported from `@promptbook/core` */ class KnowledgeScrapeError extends Error { constructor(message) { super(message); this.name = 'KnowledgeScrapeError'; Object.setPrototypeOf(this, KnowledgeScrapeError.prototype); } } /** * 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$1((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); } } /** * Detects if the code is running in a Node.js environment * * Note: `$` is used to indicate that this function is not a pure function - it looks at the global object to determine the environment * * @public exported from `@promptbook/utils` */ function $isRunningInNode() { try { return typeof process !== 'undefined' && process.versions != null && process.versions.node != null; } catch (e) { return false; } } // TODO: [๐ŸŽบ] /** * Checks if the file exists * * @private within the repository */ async function isFileExisting(filename, fs) { const isReadAccessAllowed = await fs .access(filename, fs.constants.R_OK) .then(() => true) .catch(() => false); if (!isReadAccessAllowed) { return false; } const isFile = await fs .stat(filename) .then((fileStat) => fileStat.isFile()) .catch(() => false); return isFile; } // Note: Not [~๐ŸŸข~] because it is not directly dependent on `fs // TODO: [๐Ÿ ] This can be a validator - with variants that return true/false and variants that throw errors with meaningless messages // TODO: [๐Ÿ–‡] What about symlinks? /** * Converts a name to a properly formatted subfolder path for cache storage. * Handles normalization and path formatting to create consistent cache directory structures. * * @private for `FileCacheStorage` */ function nameToSubfolderPath(name) { return [name.substr(0, 1).toLowerCase(), name.substr(1, 1).toLowerCase()]; } /** * Collection of default diacritics removal map. */ 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\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E', }, { base: 'H', letters: '\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D', }, { base: 'I', letters: '\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197', }, { base: 'J', letters: '\u004A\u24BF\uFF2A\u0134\u0248' }, { base: 'K', letters: '\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2', }, { base: 'L', letters: '\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780', }, { base: 'LJ', letters: '\u01C7' }, { base: 'Lj', letters: '\u01C8' }, { base: 'M', letters: '\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C' }, { base: 'N', letters: '\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4', }, { base: 'NJ', letters: '\u01CA' }, { base: 'Nj', letters: '\u01CB' }, { base: 'O', letters: '\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C', }, { base: 'OI', letters: '\u01A2' }, { base: 'OO', letters: '\uA74E' }, { base: 'OU', letters: '\u0222' }, { base: 'OE', letters: '\u008C\u0152' }, { base: 'oe', letters: '\u009C\u0153' }, { base: 'P', letters: '\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754', }, { base: 'Q', letters: '\u0051\u24C6\uFF31\uA756\uA758\u024A' }, { base: 'R', letters: '\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782', }, { base: 'S', letters: '\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784', }, { base: 'T', letters: '\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786', }, { base: 'TZ', letters: '\uA728' }, { base: 'U', letters: '\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244', }, { base: 'V', letters: '\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245' }, { base: 'VY', letters: '\uA760' }, { base: 'W', letters: '\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72', }, { base: 'X', letters: '\u0058\u24CD\uFF38\u1E8A\u1E8C' }, { base: 'Y', letters: '\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE', }, { base: 'Z', letters: '\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762', }, { base: 'a', letters: '\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250', }, { base: 'aa', letters: '\uA733' }, { base: 'ae', letters: '\u00E6\u01FD\u01E3' }, { base: 'ao', letters: '\uA735' }, { base: 'au', letters: '\uA737' }, { base: 'av', letters: '\uA739\uA73B' }, { base: 'ay', letters: '\uA73D' }, { base: 'b', letters: '\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253', }, { base: 'c', letters: '\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184', }, { base: 'd', letters: '\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A', }, { base: 'dz', letters: '\u01F3\u01C6' }, { base: 'e', letters: '\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD', }, { base: 'f', letters: '\u0066\u24D5\uFF46\u1E1F\u0192\uA77C' }, { base: 'g', letters: '\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F', }, { base: 'h', letters: '\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265', }, { base: 'hv', letters: '\u0195' }, { base: 'i', letters: '\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131', }, { base: 'j', letters: '\u006A\u24D9\uFF4A\u0135\u01F0\u0249' }, { base: 'k', letters: '\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3', }, { base: 'l', letters: '\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747', }, { base: 'lj', letters: '\u01C9' }, { base: 'm', letters: '\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F' }, { base: 'n', letters: '\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5', }, { base: 'nj', letters: '\u01CC' }, { base: 'o', letters: '\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275', }, { base: 'oi', letters: '\u01A3' }, { base: 'ou', letters: '\u0223' }, { base: 'oo', letters: '\uA74F' }, { base: 'p', letters: '\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755', }, { base: 'q', letters: '\u0071\u24E0\uFF51\u024B\uA757\uA759' }, { base: 'r', letters: '\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783', }, { base: 's', letters: '\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B', }, { base: 't', letters: '\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787', }, { b