pomljs
Version:
Prompt Orchestration Markup Language
724 lines (721 loc) • 25.8 kB
JavaScript
import * as React from 'react';
import { distance } from 'closest-match';
import { deepMerge } from './util/index.js';
import componentDocs from './assets/componentDocs.json.js';
import path__default from 'path';
import flattenChildren from 'react-keyed-flatten-children';
/**
* The very basic logics that drive "every" component in the system.
* The "every" is the criteria whether the logic should serve as a base.
* For example, the stylesheet is considered as a base, as it's supported in every component,
* but markup presentation is not.
*/
const ValidSpeakers = ['system', 'human', 'ai', 'tool'];
function richContentFromSourceMap(contents) {
const parts = [];
const append = (txt) => {
if (parts.length > 0 && typeof parts[parts.length - 1] === 'string') {
parts[parts.length - 1] = parts[parts.length - 1] + txt;
}
else if (txt.length > 0) {
parts.push(txt);
}
};
for (const seg of contents) {
const c = seg.content;
if (typeof c === 'string') {
append(c);
}
else if (Array.isArray(c)) {
for (const item of c) {
if (typeof item === 'string') {
append(item);
}
else {
parts.push(item);
}
}
}
else {
parts.push(c);
}
}
if (parts.length === 1) {
return typeof parts[0] === 'string' ? parts[0] : [parts[0]];
}
return parts;
}
/**
* Create an element that will be visible in the IR.
* Helper function for logging and debugging purposes.
*/
const irElement = (type, props, ...children) => {
if (props.speaker && !ValidSpeakers.includes(props.speaker)) {
ErrorCollection.add(ReadError.fromProps(`"${props.speaker}" is not a valid speaker.`, props));
props.speaker = undefined;
}
const propsWithoutUndefined = Object.fromEntries(Object.entries(props)
.filter(([_, v]) => v !== undefined)
.map(([k, v]) => {
const hyphenCaseKey = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
if (typeof v === 'boolean') {
return [hyphenCaseKey, v.toString()];
}
else if (typeof v === 'number') {
return [hyphenCaseKey, v.toString()];
}
else if (typeof v === 'object') {
return [hyphenCaseKey, JSON.stringify(v)];
}
else {
return [hyphenCaseKey, v];
}
}));
const trimmedChildren = trimChildrenWhiteSpace(children, props);
return React.createElement(type, propsWithoutUndefined, ...trimmedChildren);
};
function trimChildrenWhiteSpace(children, props) {
// This is exposed for providers.
// The children directly under a context provider also needs to be trimmed,
// otherwise they do not have a chance to be trimmed.
let flattenedChildren = flattenChildren(children);
// Merge consecutive strings.
if (props.whiteSpace !== 'pre') {
const mergedChildren = [];
let currentString = '';
for (const child of flattenedChildren) {
if (typeof child === 'string') {
currentString += child;
}
else {
if (currentString) {
mergedChildren.push(currentString);
currentString = '';
}
mergedChildren.push(child);
}
}
if (currentString) {
mergedChildren.push(currentString);
}
flattenedChildren = mergedChildren;
}
const trimmedChildren = flattenedChildren
.map((child, index) => {
if (typeof child === 'string') {
if (props.whiteSpace === 'pre') {
return child;
}
else if (props.whiteSpace === 'filter' || props.whiteSpace === undefined) {
return trimText(child, index === 0, index === flattenedChildren.length - 1);
}
else if (props.whiteSpace === 'trim') {
let trimmed = child;
if (index === 0) {
trimmed = trimmed.trimStart();
}
if (index === flattenedChildren.length - 1) {
trimmed = trimmed.trimEnd();
}
return trimmed;
}
else {
ErrorCollection.add(ReadError.fromProps(`"${props.whiteSpace}" is not a valid whiteSpace option.`, props));
return child;
}
}
else {
return child;
}
})
.filter(c => c !== '');
return trimmedChildren;
}
/**
* Trim the element tree following the CSS rules
* https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
*/
const trimText = (text, isFirst, isLast) => {
// 1. all spaces and tabs immediately before and after a line break are ignored
text = text.replace(/[\t\n\r ]*\n[\t\n\r ]*/g, '\n');
// 2. all tab characters and line breaks are handled as space characters
text = text.replace(/[\t\n\r]/g, ' ');
// 3. multiple space characters are handled as one space character
text = text.replace(/ +/g, ' ');
// 4. sequences of spaces at the beginning and end of an element are removed
if (isFirst) {
text = text.replace(/^ +/, '');
}
if (isLast) {
text = text.replace(/ +$/, '');
}
return text;
};
class PomlError extends Error {
severity = 'error';
constructor(message, options) {
super(message, options);
this.name = 'PomlError';
if (options?.severity) {
this.severity = options.severity;
}
}
}
class SystemError extends PomlError {
constructor(message, options) {
super(message, options);
this.name = 'SystemError';
}
}
class ReadError extends PomlError {
startIndex;
endIndex;
sourcePath;
constructor(message, startIndex, endIndex, sourcePath, options) {
super(message, options);
this.startIndex = startIndex;
this.endIndex = endIndex;
this.sourcePath = sourcePath;
this.name = 'ReadError';
}
static fromProps(message, props, options) {
return new ReadError(message, props.originalStartIndex, props.originalEndIndex, props.sourcePath, options);
}
}
class WriteError extends PomlError {
startIndex;
endIndex;
sourcePath;
irStartIndex;
irEndIndex;
relatedIr;
constructor(message, startIndex, endIndex, sourcePath, irStartIndex, irEndIndex, relatedIr, options) {
super(message, options);
this.startIndex = startIndex;
this.endIndex = endIndex;
this.sourcePath = sourcePath;
this.irStartIndex = irStartIndex;
this.irEndIndex = irEndIndex;
this.relatedIr = relatedIr;
this.name = 'WriteError';
}
}
/**
* A can to hold all the errors.
*/
class ErrorCollection {
errors = [];
static _instance;
constructor() { }
static get instance() {
if (!this._instance) {
this._instance = new ErrorCollection();
}
return this._instance;
}
static add(error) {
this.instance.errors.push(error);
}
static first() {
return this.instance.errors[0];
}
static last() {
return this.instance.errors[this.instance.errors.length - 1];
}
static list() {
return this.instance.errors;
}
static empty() {
return this.instance.errors.length === 0;
}
static clear() {
this.instance.errors = [];
}
}
function calculateSize(value, visited = new Set()) {
if (value === null || value === undefined) {
return 0;
}
if (visited.has(value)) {
return 0;
}
if (Buffer.isBuffer(value)) {
return value.length;
}
const t = typeof value;
if (t === 'string') {
return Buffer.byteLength(value);
}
if (t === 'number' || t === 'boolean' || t === 'bigint') {
return 8;
}
if (Array.isArray(value)) {
visited.add(value);
let size = 0;
for (const item of value) {
try {
size += calculateSize(item, visited);
}
catch {
// ignore
}
}
visited.delete(value);
return size;
}
if (t === 'object') {
visited.add(value);
let size = 0;
for (const key in value) {
try {
size += calculateSize(value[key], visited);
}
catch {
// ignore
}
}
visited.delete(value);
return size;
}
return 0;
}
class BufferCollection {
buffers = new Map();
totalSize = 0;
limit = 10 * 1024 * 1024; // 10MB default
static _instance;
constructor() { }
static get instance() {
if (!this._instance) {
this._instance = new BufferCollection();
}
return this._instance;
}
evict() {
while (this.totalSize > this.limit && this.buffers.size > 0) {
const oldestKey = this.buffers.keys().next().value;
const entry = this.buffers.get(oldestKey);
if (entry) {
this.totalSize -= entry.size;
}
this.buffers.delete(oldestKey);
}
}
static get(key) {
const entry = this.instance.buffers.get(key);
return entry ? entry.value : undefined;
}
static set(key, value) {
const inst = this.instance;
const prev = inst.buffers.get(key);
if (prev) {
inst.totalSize -= prev.size;
}
const entrySize = calculateSize(value);
if (entrySize > inst.limit) {
return;
}
inst.buffers.set(key, { value, size: entrySize });
inst.totalSize += entrySize;
inst.evict();
}
static clear() {
this.instance.buffers.clear();
this.instance.totalSize = 0;
}
}
const useWithCatch = (promise, props) => {
const catchedPromise = promise.catch((err) => {
if (err instanceof PomlError) {
ErrorCollection.add(err);
}
else {
ErrorCollection.add(ReadError.fromProps(err && err.message
? err.message
: 'Unknown error happened during asynchroneous process of rendering.', props, { cause: err }));
}
});
return React.use(catchedPromise);
};
const StyleSheetContext = React.createContext({});
const StyleSheetProvider = ({ stylesheet, children }) => {
const currentStylesheet = React.useContext(StyleSheetContext);
// Deep merge stylesheet
stylesheet = deepMerge(currentStylesheet, stylesheet);
return React.createElement(StyleSheetContext.Provider, { value: stylesheet }, children);
};
const useStyleSheet = () => React.useContext(StyleSheetContext);
const computeStyles = (currentProps, component, _stylesheet) => {
const stylesheet = _stylesheet !== undefined ? _stylesheet : useStyleSheet();
// priority, order, props
const matches = [];
Object.entries(stylesheet).forEach(([match, props], index) => {
if (match === '*') {
matches.push([0, index, props]);
}
else {
const matchResult = match.split(/\s+/g).map(indiv => {
// FIXME: this is different from css rule
if (indiv.startsWith('.')) {
const currentClassName = currentProps?.className;
const currentClasses = currentClassName ? currentClassName.split(/\s+/g) : [];
return currentClasses.includes(indiv.slice(1)) ? 2 : 0;
}
else {
return component.getAliases().includes(indiv.toLowerCase()) ? 1 : 0;
}
});
if (matchResult.every(r => r > 0)) {
matches.push([matchResult.reduce((a, b) => a + b, 0), index, props]);
}
}
});
matches.sort((a, b) => (a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]));
const { className, ...restProps } = currentProps;
matches.push([999, -1, restProps]);
let finalProps = {};
matches.forEach(([, , props]) => {
finalProps = deepMerge(finalProps, props);
});
return finalProps;
};
// Source provider provides a path to the source file.
// It's used to find related files specified in the source file.
// It's also used to locate the source file for debugging purposes.
const SourceContext = React.createContext('');
const SourceProvider = ({ source, children }) => {
return React.createElement(SourceContext.Provider, { value: source }, children);
};
const expandRelative = (src) => {
if (path__default.isAbsolute(src)) {
return src;
}
const pomlSource = React.useContext(SourceContext);
if (!pomlSource) {
return src;
}
return path__default.resolve(path__default.dirname(pomlSource), src);
};
class PomlComponent {
officialName;
componentFunc;
options;
constructor(officialName, componentFunc, options) {
this.officialName = officialName;
this.componentFunc = componentFunc;
this.options = options;
}
get name() {
return this.officialName;
}
getAliases(lower = true) {
if (lower) {
return this.options.aliases.map(alias => alias.toLowerCase());
}
else {
return this.options.aliases;
}
}
warnsIfProvided(props) {
if (!props) {
return;
}
this.options.unwantedProps.forEach(key => {
if (props[key] !== undefined) {
ErrorCollection.add(ReadError.fromProps(`"${key}" is not supported (but provided) in ${this.officialName}.`, props, { severity: 'warning' }));
}
});
}
throwIfMissing(props) {
this.options.requiredProps.forEach(key => {
if (!props || props[key] === undefined) {
throw ReadError.fromProps(`"${key}" is required but not provided for ${this.officialName}, available props are ${props ? Object.keys(props) : []}.`, props);
}
});
}
isPublic() {
return this.spec() !== undefined;
}
static fromSpec(spec) {
const found = findComponentByAliasOrUndefined(spec.name ?? '');
if (found !== undefined) {
return found;
}
throw new SystemError(`Component ${spec.name} not found.`);
}
spec() {
return componentDocs.find(document => document.name === this.name);
}
parameters() {
const spec = this.spec();
if (!spec) {
return [];
}
const bases = this.mro();
const parameters = [...spec.params];
for (const base of bases) {
const baseSpec = base.spec();
if (baseSpec) {
parameters.push(...baseSpec.params.filter(p => !parameters.map(p => p.name).includes(p.name)));
}
}
return parameters;
}
mro() {
const spec = this.spec();
if (!spec) {
return [];
}
const toSearch = [...spec.baseComponents];
const result = [];
let searchPointer = 0;
while (searchPointer < toSearch.length) {
const component = findComponentByAliasOrUndefined(toSearch[searchPointer]);
if (component !== undefined) {
result.push(component);
const componentSpec = component.spec();
if (componentSpec) {
for (const base of componentSpec.baseComponents) {
if (!toSearch.includes(base) && !result.map(c => c.name).includes(base)) {
toSearch.push(base);
}
}
}
}
searchPointer++;
}
return result;
}
style(props, stylesheet) {
return computeStyles(props, this, stylesheet);
}
preprocessProps(props) {
const params = this.parameters();
return Object.entries(props).reduce((acc, [key, value]) => {
const param = params.find(param => param.name.toLowerCase() === key.toLowerCase());
if (!param) {
// Keep it.
acc[key] = value;
return acc;
}
const formalKey = param.name;
if (value === undefined) {
// TODO: check required parameters
acc[key] = value;
return acc;
}
if (param.type === 'string') {
if (typeof value !== 'string' && value !== undefined) {
value = value.toString();
}
}
else if (param.type === 'number') {
if (typeof value !== 'number' && value !== undefined) {
value = parseFloat(value);
}
}
else if (param.type === 'boolean') {
if (typeof value !== 'boolean') {
const isTrue = ['1', 'true'].includes(value.toString().toLowerCase());
const isFalse = ['0', 'false'].includes(value.toString().toLowerCase());
if (!isTrue && !isFalse) {
ErrorCollection.add(ReadError.fromProps(`"${key}" should be a boolean`, props));
value = undefined;
}
value = isTrue;
}
}
else if (param.type === 'object' || param.type === 'object|string') {
if (typeof value === 'string') {
try {
value = JSON.parse(value);
}
catch (e) {
if (param.fallbackType !== 'string') {
ErrorCollection.add(ReadError.fromProps(`Fail to parse \"${key}\" with JSON parser`, props));
}
}
}
}
else if (param.type === 'RegExp' || param.type === 'RegExp|string') {
if (typeof value === 'string') {
if (value.startsWith('/')) {
// Extract flags if present
const lastSlashIndex = value.lastIndexOf('/');
if (lastSlashIndex > 0) {
const pattern = value.substring(1, lastSlashIndex);
const flags = value.substring(lastSlashIndex + 1);
// Only create RegExp with flags if flags exist and are valid
if (flags && /^[gimsuy]*$/.test(flags)) {
value = new RegExp(pattern, flags);
}
else if (lastSlashIndex === value.length - 1) {
// Format is /pattern/ with no flags
value = new RegExp(pattern);
}
}
}
else {
// Default behavior for strings not in /pattern/ format
value = new RegExp(value);
}
}
}
else ;
if (param.choices.length > 0) {
if (!param.choices.includes(value)) {
ErrorCollection.add(ReadError.fromProps(`"${key}" should be one of ${param.choices.join(', ')}, not ${value}`, props));
}
}
acc[formalKey] = value;
return acc;
}, {});
}
render(props) {
this.warnsIfProvided(props);
try {
// If one of the following steps has error, abort the process.
this.throwIfMissing(props);
if (this.options.applyStyleSheet) {
props = this.style(props);
}
props = this.preprocessProps(props);
if (this.options.asynchorous) {
const msg = 'This prompt is asynchorous and still loading. Users should not see this message. ' +
'If you see this message, please report it to the developer.';
return (React.createElement(React.Suspense, { fallback: React.createElement("div", null, msg) }, this.componentFunc(props)));
}
else {
return this.componentFunc(props);
}
}
catch (e) {
if (e &&
typeof e.message === 'string' &&
e.message.startsWith('Suspense Exception:')) {
throw e;
}
if (e instanceof PomlError) {
ErrorCollection.add(e);
}
else {
ErrorCollection.add(ReadError.fromProps(`Error in component render of ${this.officialName}: ${e}`, props, {
cause: e
}));
}
return null;
}
}
}
class ComponentRegistry {
static _instance;
components = [];
constructor() { }
static get instance() {
if (!this._instance) {
this._instance = new ComponentRegistry();
}
return this._instance;
}
registerComponent(officialName, component, options) {
if (!options.aliases.includes(officialName)) {
options.aliases = [officialName, ...options.aliases];
}
options.aliases.forEach(alias => {
const aliasExisting = this.components.filter(c => c.getAliases().includes(alias.toLowerCase()));
if (aliasExisting.length > 0) {
throw new SystemError(`Alias "${alias}" is already used by ${aliasExisting[0]}.`);
}
});
const registered = new PomlComponent(officialName, component, options);
this.components.push(registered);
return registered;
}
unregisterComponent(name) {
const component = this.getComponent(name);
this.components = this.components.filter(c => c !== component);
}
listComponents() {
return [...this.components];
}
getComponent(name, returnReasonIfNotFound = false, disabled = undefined) {
if (returnReasonIfNotFound instanceof Set) {
disabled = returnReasonIfNotFound;
returnReasonIfNotFound = false;
}
const hyphenToCamelCase = (s) => {
return s.toLowerCase().replace(/-([a-z])/g, g => g[1].toUpperCase());
};
const nameVariants = [name.toLowerCase(), hyphenToCamelCase(name).toLowerCase()];
for (const variant of nameVariants) {
for (const component of this.components) {
const aliases = component.getAliases();
if (!aliases.includes(variant)) {
continue;
}
if (disabled?.has(variant)) {
continue;
}
return component;
}
}
if (!returnReasonIfNotFound) {
return undefined;
}
const availableAliases = this.components
.flatMap(c => c.getAliases())
.filter(a => !disabled?.has(a));
const distances = availableAliases.map(alias => {
return {
alias: alias,
dist: distance(alias.toLowerCase(), name.toLowerCase())
};
});
distances.sort((a, b) => a.dist - b.dist);
const doYouMean = distances.filter((d, index) => index < 1 || d.dist <= 2);
return `Component ${name} not found. Do you mean: ${doYouMean.map(d => d.alias).join(', ')}?`;
}
}
function component(name, options) {
return (target) => {
const registered = ComponentRegistry.instance.registerComponent(name, target, options
? Array.isArray(options)
? {
aliases: options,
requiredProps: [],
unwantedProps: [],
applyStyleSheet: true,
asynchorous: false
}
: {
aliases: options.aliases ?? [],
requiredProps: options.requiredProps ?? [],
unwantedProps: options.unwantedProps ?? [],
applyStyleSheet: options.applyStyleSheet ?? true,
asynchorous: options.asynchorous ?? false
}
: {
aliases: [],
requiredProps: [],
unwantedProps: [],
applyStyleSheet: true,
asynchorous: false
});
return registered.render.bind(registered);
};
}
/**
* Find a component by its alias. If not found, return a string that suggests the closest match.
* @param alias Alias or official name.
*/
function findComponentByAlias(alias, disabled) {
return ComponentRegistry.instance.getComponent(alias, true, disabled);
}
function findComponentByAliasOrUndefined(alias, disabled) {
return ComponentRegistry.instance.getComponent(alias, disabled);
}
function listComponents() {
return ComponentRegistry.instance.listComponents();
}
export { BufferCollection, ErrorCollection, PomlComponent, ReadError, SourceProvider, StyleSheetProvider, SystemError, ValidSpeakers, WriteError, component, expandRelative, findComponentByAlias, findComponentByAliasOrUndefined, irElement, listComponents, richContentFromSourceMap, trimChildrenWhiteSpace, useWithCatch };
//# sourceMappingURL=base.js.map