@synstack/text 1.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @synstack/text
2
+
3
+ > String templating as it was meant to be
4
+
5
+ This is a strongly opinionated implementation of string templating. It's basically JSX for text and solves many quirks of string interpolation and formatting.
6
+
7
+ > [!WARNING]
8
+ > This package is included in the [@synstack/synscript](https://github.com/pAIrprogioio/synscript) package. It is not recommended to install both packages at the same time.
9
+
10
+ ## What is it for ?
11
+
12
+ Turns this:
13
+
14
+ ```ts
15
+ import { t } from "@synstack/text";
16
+
17
+ const items = ["Hello", "World"];
18
+
19
+ const text: string = await t`
20
+ Value: ${items.join(", ")}
21
+ Promise: ${Promise.resolve(items.join(", "))}
22
+
23
+ Callables:
24
+ Callable: ${() => items.join(", ")}
25
+ Callable Promise: ${() => Promise.resolve(items.join(", "))}
26
+
27
+ List of items:
28
+ ${() => Promise.resolve(items.map((item) => `- ${item}`))}
29
+ `;
30
+ ```
31
+
32
+ Into this:
33
+
34
+ ```plain
35
+ Value: Hello, World
36
+ Promise: Hello, World
37
+
38
+ Callables:
39
+ Callable: Hello, World
40
+ Callable Promise: Hello, World
41
+
42
+ List of items:
43
+ - Hello
44
+ - World
45
+ ```
46
+
47
+ **What's baked in ?**
48
+
49
+ - Functions are called
50
+ - Promises even nested in arrays are resolved in parallel
51
+ - Array values are joined with a newline
52
+ - Text is trimmed
53
+ - Base indentation is removed
54
+ - Nested indentation is preserved for multi-line values
55
+ - Returned value is either a string or a promise of a string based on interpolated values
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install @synstack/text
61
+ yarn add @synstack/text
62
+ ```
63
+
64
+ ## Features
65
+
66
+ ### Text formatting
67
+
68
+ - `t` will automatically trim unecessary whitespaces and indentations. This allows your code to remain indented and readable while still being able to use the template.
69
+ - `t` will auto-join array values with a newline.
70
+ - `t` will propagate indentation to each line of the nested values.
71
+
72
+ ```ts
73
+ const text: string = t`
74
+ Hello
75
+ ${"- Item 1\n- Item 2"}
76
+ World
77
+ `;
78
+ ```
79
+
80
+ Will be transformed into:
81
+
82
+ ```plain
83
+ Hello
84
+ - Item 1
85
+ - Item 2
86
+ World
87
+ ```
88
+
89
+ ### Async support
90
+
91
+ - `t` automatically detects if the template is async or not and handles it accordingly.
92
+ - When `t` is async, it resolves all values in parralel with a `Promise.all`
93
+ - If you want to force serial execution, use an `await` expression.
94
+
95
+ ```ts
96
+ const sync: string = t`Hello ${"World"}`;
97
+ const async: Promise<string> = t`Hello ${Promise.resolve("World")}`;
98
+
99
+ /*
100
+ Asuming retrieveUserName and retrieveUserEmail are async functions
101
+ Both queries will be resolved in parallel
102
+ */
103
+ const async: Promise<string> = t`Hello ${retrieveUserName()} ${retrieveUserEmail()}`;
104
+ ```
105
+
106
+ ### Callable values
107
+
108
+ - You can use any function without argument as a template value.
109
+ - `t` will call the function and then handle it's `sync` or `async` state through the async support logic.
110
+
111
+ ```ts
112
+ /*
113
+ Asuming retrieveUserName and retrieveUserEmail are async functions with no arguments
114
+ Both queries will be called and resolved in parallel
115
+ */
116
+ const async: Promise<string> = t`Hello ${retrieveUserName} ${retrieveUserEmail}`;
117
+ ```
118
+
119
+ ### Arrays
120
+
121
+ - Array values are resolved in parrallel with `Promise.all`
122
+ - The resulting strings are joined with a newline
123
+ - The indentation is preserved for each line
124
+
125
+ ```ts
126
+ const items = [Promise.resolve("Hello"), Promise.resolve("World")];
127
+
128
+ const text: Promise<string> = t`
129
+ This is a list of items:
130
+ ${items}
131
+ `;
132
+
133
+ console.log(await text);
134
+ ```
135
+
136
+ Will output:
137
+
138
+ ```plain
139
+ This is a list of items:
140
+ Hello
141
+ World
142
+ ```
143
+
144
+ ### Extra objects
145
+
146
+ > [!NOTE]
147
+ > This feature was built to seemlessly integrate inline content blocks in LLM messages.
148
+ > e.g. Adding images, tool responses, etc.
149
+
150
+ - `t` is able to handle non-string objects as values. As long as they have a `type` property.
151
+ - The value will be `JSON.stringify`ed and added to the template.
152
+ - The returned value will be `string & { __extra: TExtraObject }`
153
+ - The value can then be accessed through the `tParse(resultingString)` property.
154
+ - You can constrain the type of the extra object by using a type assertion from `Text.String`
155
+ - You can infer the value type of the extra object by using a type assertion from `Text.ExtraObject.Infer`
156
+
157
+ ```ts
158
+ import { t, tParse, type Text } from "@pairprog/text";
159
+
160
+ // @ts-expect-error - The non-matching extra object will be rejected
161
+ const textFail: Text.String<{ type: "extra"; value: string }> =
162
+ t`Hello ${{ type: "other-type" as const, value: "Hello" }} World`;
163
+
164
+ // string & { __extra: { type: "extra"; value: string } } is equivalent to Text.String<{ type: "extra"; value: string }>
165
+ const text: string & { __extra: { type: "extra"; value: string } } =
166
+ t`Hello ${{ type: "extra" as const, value: "Hello" }} World`;
167
+
168
+ console.log(text);
169
+ // Hello %STR_EXTRA%{"type":"extra","value":"Hello"}%!STR_EXTRA% World
170
+
171
+ console.log(tParse(text));
172
+ // ["Hello ", { type: "extra", value: "Hello" }, " World"]
173
+ ```
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/text.index.ts
21
+ var text_index_exports = {};
22
+ __export(text_index_exports, {
23
+ TextParseExtraItemException: () => TextParseExtraItemException,
24
+ t: () => t,
25
+ tParse: () => tParse
26
+ });
27
+ module.exports = __toCommonJS(text_index_exports);
28
+
29
+ // src/text.lib.ts
30
+ var import_json = require("@synstack/json");
31
+ var import_resolved = require("@synstack/resolved");
32
+ var import_callable = require("@synstack/resolved/callable");
33
+ var import_str = require("@synstack/str");
34
+ var Text = class _Text {
35
+ static EXTRA_OBJECT_PREFIX = "%STR_EXTRA%";
36
+ static EXTRA_OBJECT_SUFFIX = "%!STR_EXTRA%";
37
+ _options;
38
+ constructor(options = {}) {
39
+ this._options = {
40
+ joinString: options.joinString ?? "\n"
41
+ };
42
+ }
43
+ static options(config = {}) {
44
+ return new _Text(config);
45
+ }
46
+ options(config) {
47
+ return new _Text({ ...this._options, ...config });
48
+ }
49
+ static t(template, ...values) {
50
+ return _Text.options().t(template, ...values);
51
+ }
52
+ t(template, ...values) {
53
+ if (template.length === 0) return "";
54
+ const resolvedValues = import_callable.callable.resolveNested(values);
55
+ return import_resolved.resolvable.pipe(resolvedValues)._((values2) => {
56
+ let text = template[0];
57
+ for (let i = 0; i < values2.length; i++) {
58
+ const value = values2[i];
59
+ let wrappedValue = "";
60
+ if (Array.isArray(value)) {
61
+ wrappedValue = value.filter((inner) => inner !== null || inner !== void 0).map(_Text.wrapValue).join(this._options.joinString);
62
+ } else {
63
+ wrappedValue = _Text.wrapValue(value);
64
+ }
65
+ const nextString = template[i + 1];
66
+ const lastLine = (0, import_str.str)(text).lastLine();
67
+ const indentation = lastLine.isEmpty() ? lastLine.leadingSpacesCount() : 0;
68
+ text = (0, import_str.str)(text).chopEnd(indentation).toString() + (0, import_str.str)(wrappedValue).indent(indentation).toString() + nextString;
69
+ }
70
+ return (0, import_str.str)(text).chopEmptyLinesStart().trimEnd().dedent().trimEmptyLines().chopRepeatNewlines(2).toString();
71
+ }).$;
72
+ }
73
+ static wrapValue(value) {
74
+ if (value === null || value === void 0) return "";
75
+ if (typeof value === "object") {
76
+ if (!Object.hasOwn(value, "type")) {
77
+ throw new Error(
78
+ 'Text templating only supports objects with a "type" property'
79
+ );
80
+ }
81
+ return `${_Text.EXTRA_OBJECT_PREFIX}${JSON.stringify(value)}${_Text.EXTRA_OBJECT_SUFFIX}`;
82
+ }
83
+ return value;
84
+ }
85
+ static parse(text) {
86
+ const regex = new RegExp(
87
+ _Text.EXTRA_OBJECT_PREFIX + "(.*?)" + _Text.EXTRA_OBJECT_SUFFIX,
88
+ "g"
89
+ );
90
+ const parts = [];
91
+ let lastIndex = 0;
92
+ let match;
93
+ while ((match = regex.exec(text)) !== null) {
94
+ if (match.index > lastIndex) {
95
+ parts.push(text.slice(lastIndex, match.index));
96
+ }
97
+ try {
98
+ const jsonObject = import_json.json.deserialize(match[1]);
99
+ parts.push(jsonObject);
100
+ } catch (error) {
101
+ throw new TextParseExtraItemException(match[1], error);
102
+ }
103
+ lastIndex = regex.lastIndex;
104
+ }
105
+ if (lastIndex < text.length) {
106
+ parts.push(text.slice(lastIndex));
107
+ }
108
+ return parts;
109
+ }
110
+ };
111
+ var TextParseExtraItemException = class extends Error {
112
+ constructor(itemString, cause) {
113
+ super(
114
+ `
115
+ Failed to parse extra item serialized value
116
+
117
+ Value:
118
+ ${(0, import_str.str)(itemString).indent(2).toString()}
119
+
120
+ Cause:
121
+ ${(0, import_str.str)(cause instanceof Error ? cause.message : cause).indent(2).toString()}
122
+
123
+ `.trimStart(),
124
+ { cause }
125
+ );
126
+ }
127
+ };
128
+ var t = Text.t;
129
+ var tParse = Text.parse;
130
+ // Annotate the CommonJS export names for ESM import in node:
131
+ 0 && (module.exports = {
132
+ TextParseExtraItemException,
133
+ t,
134
+ tParse
135
+ });
136
+ //# sourceMappingURL=text.index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/text.index.ts","../src/text.lib.ts"],"sourcesContent":["export * from \"./text.bundle\";\nexport { TextParseExtraItemException } from \"./text.lib\";\nexport type { Text } from \"./text.lib\";\n","import { MaybeArray } from \"@shared/src/ts.utils\";\nimport { json } from \"@synstack/json\";\nimport { resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\n\nexport class Text {\n private static EXTRA_OBJECT_PREFIX = \"%STR_EXTRA%\";\n private static EXTRA_OBJECT_SUFFIX = \"%!STR_EXTRA%\";\n\n private readonly _options: {\n joinString: string;\n };\n\n private constructor(options: Text.Options = {}) {\n this._options = {\n joinString: options.joinString ?? \"\\n\",\n };\n }\n\n public static options(this: void, config: Text.Options = {}) {\n return new Text(config);\n }\n\n public options(config: Text.Options) {\n return new Text({ ...this._options, ...config });\n }\n\n public static t<T extends Array<Text.TemplateValue.Base>>(\n this: void,\n template: TemplateStringsArray,\n ...values: T\n ): Text.Return<T> {\n return Text.options().t(template, ...values);\n }\n\n public t<T extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...values: T\n ): Text.Return<T> {\n // @ts-expect-error - Of course the default value is \"\"\n if (template.length === 0) return \"\";\n\n const resolvedValues = callable.resolveNested(values);\n return resolvable.pipe(resolvedValues)._((values) => {\n let text = template[0];\n\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n let wrappedValue = \"\";\n\n if (Array.isArray(value)) {\n wrappedValue = value\n .filter((inner) => inner !== null || inner !== undefined)\n .map(Text.wrapValue)\n .join(this._options.joinString);\n } else {\n wrappedValue = Text.wrapValue(value);\n }\n const nextString = template[i + 1];\n const lastLine = str(text).lastLine();\n const indentation = lastLine.isEmpty()\n ? lastLine.leadingSpacesCount()\n : 0;\n text =\n str(text).chopEnd(indentation).toString() +\n str(wrappedValue).indent(indentation).toString() +\n nextString;\n }\n\n return str(text)\n .chopEmptyLinesStart()\n .trimEnd()\n .dedent()\n .trimEmptyLines()\n .chopRepeatNewlines(2)\n .toString() as string & {\n __extra: Text.ExtraObject.Infer<T>;\n };\n }).$ as Text.Return<T>;\n }\n\n private static wrapValue(this: void, value: Text.Value.Base) {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"object\") {\n if (!Object.hasOwn(value, \"type\")) {\n throw new Error(\n 'Text templating only supports objects with a \"type\" property',\n );\n }\n return `${Text.EXTRA_OBJECT_PREFIX}${JSON.stringify(value)}${\n Text.EXTRA_OBJECT_SUFFIX\n }`;\n }\n return value;\n }\n\n public static parse<E extends Text.ExtraObject.Base>(\n this: void,\n text: Text.String<E>,\n ): Array<string | E> {\n const regex = new RegExp(\n Text.EXTRA_OBJECT_PREFIX + \"(.*?)\" + Text.EXTRA_OBJECT_SUFFIX,\n \"g\",\n );\n const parts: Array<string | E> = [];\n let lastIndex = 0;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n // Add the text before the match\n if (match.index > lastIndex) {\n parts.push(text.slice(lastIndex, match.index));\n }\n\n // Parse and add the JSON object\n try {\n const jsonObject = json.deserialize<E>(match[1]);\n parts.push(jsonObject);\n } catch (error) {\n throw new TextParseExtraItemException(match[1], error);\n }\n\n lastIndex = regex.lastIndex;\n }\n\n // Add any remaining text after the last match\n if (lastIndex < text.length) {\n parts.push(text.slice(lastIndex));\n }\n\n return parts;\n }\n}\n\nexport declare namespace Text {\n export type Options = {\n joinString?: string;\n };\n\n export type OptionalString = string | undefined | null;\n\n export type String<TExtraObject extends Text.ExtraObject.Base = never> =\n string & {\n __extra: TExtraObject;\n };\n\n export namespace ExtraObject {\n export type Base = { type: string };\n\n type InferExtraObjectValue<T> = T extends Text.OptionalString ? never : T;\n type InferExtraObjectCallableResolvable<T> = InferExtraObjectValue<\n CallableResolvable.Infer<T>\n >;\n type InferExtraArrayable<T> =\n T extends Array<any>\n ? { [K in keyof T]: InferExtraObjectCallableResolvable<T[K]> }[number]\n : InferExtraObjectCallableResolvable<T>;\n\n export type Infer<T extends any[]> = {\n // Check if it's an array\n [K in keyof T]: InferExtraArrayable<T[K]>;\n }[number];\n }\n\n export type Value<TExtraObject extends Text.ExtraObject.Base = never> =\n | Text.OptionalString\n | TExtraObject;\n\n export namespace Value {\n export type Base = OptionalString | ExtraObject.Base;\n }\n\n export type TemplateValue<\n TExtraObject extends Text.ExtraObject.Base = never,\n > = CallableResolvable.MaybeArray<Value<TExtraObject>>;\n\n export namespace TemplateValue {\n export type Base = TemplateValue<ExtraObject.Base>;\n\n export type Resolved<TExtraObject extends Text.ExtraObject.Base = never> =\n MaybeArray<Value<TExtraObject>>;\n export namespace Resolved {\n export type Base = Resolved<ExtraObject.Base>;\n }\n }\n\n export type Return<T extends Array<Text.TemplateValue.Base>> =\n true extends CallableResolvable.MaybeArray.ArrayOf.IsPromise<T>\n ? Promise<string & { __extra: ExtraObject.Infer<T> }>\n : string & { __extra: ExtraObject.Infer<T> };\n}\n\nexport class TextParseExtraItemException extends Error {\n constructor(itemString: string, cause: any) {\n super(\n `\nFailed to parse extra item serialized value\n\nValue:\n${str(itemString).indent(2).toString()}\n\nCause:\n${str(cause instanceof Error ? cause.message : (cause as string))\n .indent(2)\n .toString()}\n\n`.trimStart(),\n { cause },\n );\n }\n}\n\nexport const t = Text.t;\nexport const tParse = Text.parse;\n"],"mappings":"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","names":["values"]}
@@ -0,0 +1,65 @@
1
+ import { CallableResolvable } from '@synstack/resolved/callable';
2
+
3
+ /**
4
+ * Allows using an array or a single value
5
+ */
6
+ type MaybeArray<T> = T | T[];
7
+
8
+ declare class Text {
9
+ private static EXTRA_OBJECT_PREFIX;
10
+ private static EXTRA_OBJECT_SUFFIX;
11
+ private readonly _options;
12
+ private constructor();
13
+ static options(this: void, config?: Text.Options): Text;
14
+ options(config: Text.Options): Text;
15
+ static t<T extends Array<Text.TemplateValue.Base>>(this: void, template: TemplateStringsArray, ...values: T): Text.Return<T>;
16
+ t<T extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...values: T): Text.Return<T>;
17
+ private static wrapValue;
18
+ static parse<E extends Text.ExtraObject.Base>(this: void, text: Text.String<E>): Array<string | E>;
19
+ }
20
+ declare namespace Text {
21
+ type Options = {
22
+ joinString?: string;
23
+ };
24
+ type OptionalString = string | undefined | null;
25
+ type String<TExtraObject extends Text.ExtraObject.Base = never> = string & {
26
+ __extra: TExtraObject;
27
+ };
28
+ namespace ExtraObject {
29
+ type Base = {
30
+ type: string;
31
+ };
32
+ type InferExtraObjectValue<T> = T extends Text.OptionalString ? never : T;
33
+ type InferExtraObjectCallableResolvable<T> = InferExtraObjectValue<CallableResolvable.Infer<T>>;
34
+ type InferExtraArrayable<T> = T extends Array<any> ? {
35
+ [K in keyof T]: InferExtraObjectCallableResolvable<T[K]>;
36
+ }[number] : InferExtraObjectCallableResolvable<T>;
37
+ type Infer<T extends any[]> = {
38
+ [K in keyof T]: InferExtraArrayable<T[K]>;
39
+ }[number];
40
+ }
41
+ type Value<TExtraObject extends Text.ExtraObject.Base = never> = Text.OptionalString | TExtraObject;
42
+ namespace Value {
43
+ type Base = OptionalString | ExtraObject.Base;
44
+ }
45
+ type TemplateValue<TExtraObject extends Text.ExtraObject.Base = never> = CallableResolvable.MaybeArray<Value<TExtraObject>>;
46
+ namespace TemplateValue {
47
+ type Base = TemplateValue<ExtraObject.Base>;
48
+ type Resolved<TExtraObject extends Text.ExtraObject.Base = never> = MaybeArray<Value<TExtraObject>>;
49
+ namespace Resolved {
50
+ type Base = Resolved<ExtraObject.Base>;
51
+ }
52
+ }
53
+ type Return<T extends Array<Text.TemplateValue.Base>> = true extends CallableResolvable.MaybeArray.ArrayOf.IsPromise<T> ? Promise<string & {
54
+ __extra: ExtraObject.Infer<T>;
55
+ }> : string & {
56
+ __extra: ExtraObject.Infer<T>;
57
+ };
58
+ }
59
+ declare class TextParseExtraItemException extends Error {
60
+ constructor(itemString: string, cause: any);
61
+ }
62
+ declare const t: typeof Text.t;
63
+ declare const tParse: typeof Text.parse;
64
+
65
+ export { Text, TextParseExtraItemException, t, tParse };
@@ -0,0 +1,65 @@
1
+ import { CallableResolvable } from '@synstack/resolved/callable';
2
+
3
+ /**
4
+ * Allows using an array or a single value
5
+ */
6
+ type MaybeArray<T> = T | T[];
7
+
8
+ declare class Text {
9
+ private static EXTRA_OBJECT_PREFIX;
10
+ private static EXTRA_OBJECT_SUFFIX;
11
+ private readonly _options;
12
+ private constructor();
13
+ static options(this: void, config?: Text.Options): Text;
14
+ options(config: Text.Options): Text;
15
+ static t<T extends Array<Text.TemplateValue.Base>>(this: void, template: TemplateStringsArray, ...values: T): Text.Return<T>;
16
+ t<T extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...values: T): Text.Return<T>;
17
+ private static wrapValue;
18
+ static parse<E extends Text.ExtraObject.Base>(this: void, text: Text.String<E>): Array<string | E>;
19
+ }
20
+ declare namespace Text {
21
+ type Options = {
22
+ joinString?: string;
23
+ };
24
+ type OptionalString = string | undefined | null;
25
+ type String<TExtraObject extends Text.ExtraObject.Base = never> = string & {
26
+ __extra: TExtraObject;
27
+ };
28
+ namespace ExtraObject {
29
+ type Base = {
30
+ type: string;
31
+ };
32
+ type InferExtraObjectValue<T> = T extends Text.OptionalString ? never : T;
33
+ type InferExtraObjectCallableResolvable<T> = InferExtraObjectValue<CallableResolvable.Infer<T>>;
34
+ type InferExtraArrayable<T> = T extends Array<any> ? {
35
+ [K in keyof T]: InferExtraObjectCallableResolvable<T[K]>;
36
+ }[number] : InferExtraObjectCallableResolvable<T>;
37
+ type Infer<T extends any[]> = {
38
+ [K in keyof T]: InferExtraArrayable<T[K]>;
39
+ }[number];
40
+ }
41
+ type Value<TExtraObject extends Text.ExtraObject.Base = never> = Text.OptionalString | TExtraObject;
42
+ namespace Value {
43
+ type Base = OptionalString | ExtraObject.Base;
44
+ }
45
+ type TemplateValue<TExtraObject extends Text.ExtraObject.Base = never> = CallableResolvable.MaybeArray<Value<TExtraObject>>;
46
+ namespace TemplateValue {
47
+ type Base = TemplateValue<ExtraObject.Base>;
48
+ type Resolved<TExtraObject extends Text.ExtraObject.Base = never> = MaybeArray<Value<TExtraObject>>;
49
+ namespace Resolved {
50
+ type Base = Resolved<ExtraObject.Base>;
51
+ }
52
+ }
53
+ type Return<T extends Array<Text.TemplateValue.Base>> = true extends CallableResolvable.MaybeArray.ArrayOf.IsPromise<T> ? Promise<string & {
54
+ __extra: ExtraObject.Infer<T>;
55
+ }> : string & {
56
+ __extra: ExtraObject.Infer<T>;
57
+ };
58
+ }
59
+ declare class TextParseExtraItemException extends Error {
60
+ constructor(itemString: string, cause: any);
61
+ }
62
+ declare const t: typeof Text.t;
63
+ declare const tParse: typeof Text.parse;
64
+
65
+ export { Text, TextParseExtraItemException, t, tParse };
@@ -0,0 +1,107 @@
1
+ // src/text.lib.ts
2
+ import { json } from "@synstack/json";
3
+ import { resolvable } from "@synstack/resolved";
4
+ import { callable } from "@synstack/resolved/callable";
5
+ import { str } from "@synstack/str";
6
+ var Text = class _Text {
7
+ static EXTRA_OBJECT_PREFIX = "%STR_EXTRA%";
8
+ static EXTRA_OBJECT_SUFFIX = "%!STR_EXTRA%";
9
+ _options;
10
+ constructor(options = {}) {
11
+ this._options = {
12
+ joinString: options.joinString ?? "\n"
13
+ };
14
+ }
15
+ static options(config = {}) {
16
+ return new _Text(config);
17
+ }
18
+ options(config) {
19
+ return new _Text({ ...this._options, ...config });
20
+ }
21
+ static t(template, ...values) {
22
+ return _Text.options().t(template, ...values);
23
+ }
24
+ t(template, ...values) {
25
+ if (template.length === 0) return "";
26
+ const resolvedValues = callable.resolveNested(values);
27
+ return resolvable.pipe(resolvedValues)._((values2) => {
28
+ let text = template[0];
29
+ for (let i = 0; i < values2.length; i++) {
30
+ const value = values2[i];
31
+ let wrappedValue = "";
32
+ if (Array.isArray(value)) {
33
+ wrappedValue = value.filter((inner) => inner !== null || inner !== void 0).map(_Text.wrapValue).join(this._options.joinString);
34
+ } else {
35
+ wrappedValue = _Text.wrapValue(value);
36
+ }
37
+ const nextString = template[i + 1];
38
+ const lastLine = str(text).lastLine();
39
+ const indentation = lastLine.isEmpty() ? lastLine.leadingSpacesCount() : 0;
40
+ text = str(text).chopEnd(indentation).toString() + str(wrappedValue).indent(indentation).toString() + nextString;
41
+ }
42
+ return str(text).chopEmptyLinesStart().trimEnd().dedent().trimEmptyLines().chopRepeatNewlines(2).toString();
43
+ }).$;
44
+ }
45
+ static wrapValue(value) {
46
+ if (value === null || value === void 0) return "";
47
+ if (typeof value === "object") {
48
+ if (!Object.hasOwn(value, "type")) {
49
+ throw new Error(
50
+ 'Text templating only supports objects with a "type" property'
51
+ );
52
+ }
53
+ return `${_Text.EXTRA_OBJECT_PREFIX}${JSON.stringify(value)}${_Text.EXTRA_OBJECT_SUFFIX}`;
54
+ }
55
+ return value;
56
+ }
57
+ static parse(text) {
58
+ const regex = new RegExp(
59
+ _Text.EXTRA_OBJECT_PREFIX + "(.*?)" + _Text.EXTRA_OBJECT_SUFFIX,
60
+ "g"
61
+ );
62
+ const parts = [];
63
+ let lastIndex = 0;
64
+ let match;
65
+ while ((match = regex.exec(text)) !== null) {
66
+ if (match.index > lastIndex) {
67
+ parts.push(text.slice(lastIndex, match.index));
68
+ }
69
+ try {
70
+ const jsonObject = json.deserialize(match[1]);
71
+ parts.push(jsonObject);
72
+ } catch (error) {
73
+ throw new TextParseExtraItemException(match[1], error);
74
+ }
75
+ lastIndex = regex.lastIndex;
76
+ }
77
+ if (lastIndex < text.length) {
78
+ parts.push(text.slice(lastIndex));
79
+ }
80
+ return parts;
81
+ }
82
+ };
83
+ var TextParseExtraItemException = class extends Error {
84
+ constructor(itemString, cause) {
85
+ super(
86
+ `
87
+ Failed to parse extra item serialized value
88
+
89
+ Value:
90
+ ${str(itemString).indent(2).toString()}
91
+
92
+ Cause:
93
+ ${str(cause instanceof Error ? cause.message : cause).indent(2).toString()}
94
+
95
+ `.trimStart(),
96
+ { cause }
97
+ );
98
+ }
99
+ };
100
+ var t = Text.t;
101
+ var tParse = Text.parse;
102
+ export {
103
+ TextParseExtraItemException,
104
+ t,
105
+ tParse
106
+ };
107
+ //# sourceMappingURL=text.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/text.lib.ts"],"sourcesContent":["import { MaybeArray } from \"@shared/src/ts.utils\";\nimport { json } from \"@synstack/json\";\nimport { resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\n\nexport class Text {\n private static EXTRA_OBJECT_PREFIX = \"%STR_EXTRA%\";\n private static EXTRA_OBJECT_SUFFIX = \"%!STR_EXTRA%\";\n\n private readonly _options: {\n joinString: string;\n };\n\n private constructor(options: Text.Options = {}) {\n this._options = {\n joinString: options.joinString ?? \"\\n\",\n };\n }\n\n public static options(this: void, config: Text.Options = {}) {\n return new Text(config);\n }\n\n public options(config: Text.Options) {\n return new Text({ ...this._options, ...config });\n }\n\n public static t<T extends Array<Text.TemplateValue.Base>>(\n this: void,\n template: TemplateStringsArray,\n ...values: T\n ): Text.Return<T> {\n return Text.options().t(template, ...values);\n }\n\n public t<T extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...values: T\n ): Text.Return<T> {\n // @ts-expect-error - Of course the default value is \"\"\n if (template.length === 0) return \"\";\n\n const resolvedValues = callable.resolveNested(values);\n return resolvable.pipe(resolvedValues)._((values) => {\n let text = template[0];\n\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n let wrappedValue = \"\";\n\n if (Array.isArray(value)) {\n wrappedValue = value\n .filter((inner) => inner !== null || inner !== undefined)\n .map(Text.wrapValue)\n .join(this._options.joinString);\n } else {\n wrappedValue = Text.wrapValue(value);\n }\n const nextString = template[i + 1];\n const lastLine = str(text).lastLine();\n const indentation = lastLine.isEmpty()\n ? lastLine.leadingSpacesCount()\n : 0;\n text =\n str(text).chopEnd(indentation).toString() +\n str(wrappedValue).indent(indentation).toString() +\n nextString;\n }\n\n return str(text)\n .chopEmptyLinesStart()\n .trimEnd()\n .dedent()\n .trimEmptyLines()\n .chopRepeatNewlines(2)\n .toString() as string & {\n __extra: Text.ExtraObject.Infer<T>;\n };\n }).$ as Text.Return<T>;\n }\n\n private static wrapValue(this: void, value: Text.Value.Base) {\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"object\") {\n if (!Object.hasOwn(value, \"type\")) {\n throw new Error(\n 'Text templating only supports objects with a \"type\" property',\n );\n }\n return `${Text.EXTRA_OBJECT_PREFIX}${JSON.stringify(value)}${\n Text.EXTRA_OBJECT_SUFFIX\n }`;\n }\n return value;\n }\n\n public static parse<E extends Text.ExtraObject.Base>(\n this: void,\n text: Text.String<E>,\n ): Array<string | E> {\n const regex = new RegExp(\n Text.EXTRA_OBJECT_PREFIX + \"(.*?)\" + Text.EXTRA_OBJECT_SUFFIX,\n \"g\",\n );\n const parts: Array<string | E> = [];\n let lastIndex = 0;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n // Add the text before the match\n if (match.index > lastIndex) {\n parts.push(text.slice(lastIndex, match.index));\n }\n\n // Parse and add the JSON object\n try {\n const jsonObject = json.deserialize<E>(match[1]);\n parts.push(jsonObject);\n } catch (error) {\n throw new TextParseExtraItemException(match[1], error);\n }\n\n lastIndex = regex.lastIndex;\n }\n\n // Add any remaining text after the last match\n if (lastIndex < text.length) {\n parts.push(text.slice(lastIndex));\n }\n\n return parts;\n }\n}\n\nexport declare namespace Text {\n export type Options = {\n joinString?: string;\n };\n\n export type OptionalString = string | undefined | null;\n\n export type String<TExtraObject extends Text.ExtraObject.Base = never> =\n string & {\n __extra: TExtraObject;\n };\n\n export namespace ExtraObject {\n export type Base = { type: string };\n\n type InferExtraObjectValue<T> = T extends Text.OptionalString ? never : T;\n type InferExtraObjectCallableResolvable<T> = InferExtraObjectValue<\n CallableResolvable.Infer<T>\n >;\n type InferExtraArrayable<T> =\n T extends Array<any>\n ? { [K in keyof T]: InferExtraObjectCallableResolvable<T[K]> }[number]\n : InferExtraObjectCallableResolvable<T>;\n\n export type Infer<T extends any[]> = {\n // Check if it's an array\n [K in keyof T]: InferExtraArrayable<T[K]>;\n }[number];\n }\n\n export type Value<TExtraObject extends Text.ExtraObject.Base = never> =\n | Text.OptionalString\n | TExtraObject;\n\n export namespace Value {\n export type Base = OptionalString | ExtraObject.Base;\n }\n\n export type TemplateValue<\n TExtraObject extends Text.ExtraObject.Base = never,\n > = CallableResolvable.MaybeArray<Value<TExtraObject>>;\n\n export namespace TemplateValue {\n export type Base = TemplateValue<ExtraObject.Base>;\n\n export type Resolved<TExtraObject extends Text.ExtraObject.Base = never> =\n MaybeArray<Value<TExtraObject>>;\n export namespace Resolved {\n export type Base = Resolved<ExtraObject.Base>;\n }\n }\n\n export type Return<T extends Array<Text.TemplateValue.Base>> =\n true extends CallableResolvable.MaybeArray.ArrayOf.IsPromise<T>\n ? Promise<string & { __extra: ExtraObject.Infer<T> }>\n : string & { __extra: ExtraObject.Infer<T> };\n}\n\nexport class TextParseExtraItemException extends Error {\n constructor(itemString: string, cause: any) {\n super(\n `\nFailed to parse extra item serialized value\n\nValue:\n${str(itemString).indent(2).toString()}\n\nCause:\n${str(cause instanceof Error ? cause.message : (cause as string))\n .indent(2)\n .toString()}\n\n`.trimStart(),\n { cause },\n );\n }\n}\n\nexport const t = Text.t;\nexport const tParse = Text.parse;\n"],"mappings":"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","names":["values"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@synstack/text",
3
+ "type": "module",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "packageManager": "yarn@4.4.0",
8
+ "version": "1.0.1-alpha.0",
9
+ "description": "String templating as it was meant to be",
10
+ "keywords": [
11
+ "string",
12
+ "typescript",
13
+ "ts",
14
+ "templating",
15
+ "template",
16
+ "interpolation",
17
+ "llm",
18
+ "prompt",
19
+ "ai"
20
+ ],
21
+ "author": {
22
+ "name": "pAIrprog",
23
+ "url": "https://pairprog.io"
24
+ },
25
+ "homepage": "https://github.com/pAIrprogio/synscript/tree/main/packages/text",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/pAIrprogio/syn-stack.git",
29
+ "directory": "packages/text"
30
+ },
31
+ "license": "Apache-2.0",
32
+ "scripts": {
33
+ "publish": "yarn npm publish --access public",
34
+ "prepublish": "yarn test && yarn build",
35
+ "build": "tsup",
36
+ "build:watch": "tsup --watch",
37
+ "test:types": "tsc --noEmit",
38
+ "test:unit": "node --import tsx --test src/**/*.test.ts",
39
+ "test:unit:watch": "node --import tsx --watch --test src/**/*.test.ts",
40
+ "test": "yarn test:types && yarn test:unit"
41
+ },
42
+ "exports": {
43
+ ".": {
44
+ "import": {
45
+ "types": "./dist/text.index.d.ts",
46
+ "default": "./dist/text.index.js"
47
+ },
48
+ "require": {
49
+ "types": "./dist/text.index.d.cts",
50
+ "default": "./dist/text.index.cjs"
51
+ }
52
+ }
53
+ },
54
+ "dependencies": {
55
+ "@synstack/json": "1.0.0",
56
+ "@synstack/resolved": "1.0.0",
57
+ "@synstack/str": "1.0.1-alpha.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.7.0",
61
+ "tsup": "^8.3.0",
62
+ "tsx": "^4.19.1",
63
+ "typescript": "^5.6.2"
64
+ },
65
+ "files": [
66
+ "src/**/*.ts",
67
+ "!src/**/*.test.ts",
68
+ "dist/**/*"
69
+ ],
70
+ "gitHead": "7a58577491cf08e6aad885b39c70bbd564b721b4"
71
+ }
@@ -0,0 +1 @@
1
+ export { t, tParse } from "./text.lib";
@@ -0,0 +1,3 @@
1
+ export * from "./text.bundle";
2
+ export { TextParseExtraItemException } from "./text.lib";
3
+ export type { Text } from "./text.lib";
@@ -0,0 +1,215 @@
1
+ import { MaybeArray } from "@shared/src/ts.utils";
2
+ import { json } from "@synstack/json";
3
+ import { resolvable } from "@synstack/resolved";
4
+ import { callable, type CallableResolvable } from "@synstack/resolved/callable";
5
+ import { str } from "@synstack/str";
6
+
7
+ export class Text {
8
+ private static EXTRA_OBJECT_PREFIX = "%STR_EXTRA%";
9
+ private static EXTRA_OBJECT_SUFFIX = "%!STR_EXTRA%";
10
+
11
+ private readonly _options: {
12
+ joinString: string;
13
+ };
14
+
15
+ private constructor(options: Text.Options = {}) {
16
+ this._options = {
17
+ joinString: options.joinString ?? "\n",
18
+ };
19
+ }
20
+
21
+ public static options(this: void, config: Text.Options = {}) {
22
+ return new Text(config);
23
+ }
24
+
25
+ public options(config: Text.Options) {
26
+ return new Text({ ...this._options, ...config });
27
+ }
28
+
29
+ public static t<T extends Array<Text.TemplateValue.Base>>(
30
+ this: void,
31
+ template: TemplateStringsArray,
32
+ ...values: T
33
+ ): Text.Return<T> {
34
+ return Text.options().t(template, ...values);
35
+ }
36
+
37
+ public t<T extends Array<Text.TemplateValue.Base>>(
38
+ template: TemplateStringsArray,
39
+ ...values: T
40
+ ): Text.Return<T> {
41
+ // @ts-expect-error - Of course the default value is ""
42
+ if (template.length === 0) return "";
43
+
44
+ const resolvedValues = callable.resolveNested(values);
45
+ return resolvable.pipe(resolvedValues)._((values) => {
46
+ let text = template[0];
47
+
48
+ for (let i = 0; i < values.length; i++) {
49
+ const value = values[i];
50
+ let wrappedValue = "";
51
+
52
+ if (Array.isArray(value)) {
53
+ wrappedValue = value
54
+ .filter((inner) => inner !== null || inner !== undefined)
55
+ .map(Text.wrapValue)
56
+ .join(this._options.joinString);
57
+ } else {
58
+ wrappedValue = Text.wrapValue(value);
59
+ }
60
+ const nextString = template[i + 1];
61
+ const lastLine = str(text).lastLine();
62
+ const indentation = lastLine.isEmpty()
63
+ ? lastLine.leadingSpacesCount()
64
+ : 0;
65
+ text =
66
+ str(text).chopEnd(indentation).toString() +
67
+ str(wrappedValue).indent(indentation).toString() +
68
+ nextString;
69
+ }
70
+
71
+ return str(text)
72
+ .chopEmptyLinesStart()
73
+ .trimEnd()
74
+ .dedent()
75
+ .trimEmptyLines()
76
+ .chopRepeatNewlines(2)
77
+ .toString() as string & {
78
+ __extra: Text.ExtraObject.Infer<T>;
79
+ };
80
+ }).$ as Text.Return<T>;
81
+ }
82
+
83
+ private static wrapValue(this: void, value: Text.Value.Base) {
84
+ if (value === null || value === undefined) return "";
85
+ if (typeof value === "object") {
86
+ if (!Object.hasOwn(value, "type")) {
87
+ throw new Error(
88
+ 'Text templating only supports objects with a "type" property',
89
+ );
90
+ }
91
+ return `${Text.EXTRA_OBJECT_PREFIX}${JSON.stringify(value)}${
92
+ Text.EXTRA_OBJECT_SUFFIX
93
+ }`;
94
+ }
95
+ return value;
96
+ }
97
+
98
+ public static parse<E extends Text.ExtraObject.Base>(
99
+ this: void,
100
+ text: Text.String<E>,
101
+ ): Array<string | E> {
102
+ const regex = new RegExp(
103
+ Text.EXTRA_OBJECT_PREFIX + "(.*?)" + Text.EXTRA_OBJECT_SUFFIX,
104
+ "g",
105
+ );
106
+ const parts: Array<string | E> = [];
107
+ let lastIndex = 0;
108
+ let match;
109
+
110
+ while ((match = regex.exec(text)) !== null) {
111
+ // Add the text before the match
112
+ if (match.index > lastIndex) {
113
+ parts.push(text.slice(lastIndex, match.index));
114
+ }
115
+
116
+ // Parse and add the JSON object
117
+ try {
118
+ const jsonObject = json.deserialize<E>(match[1]);
119
+ parts.push(jsonObject);
120
+ } catch (error) {
121
+ throw new TextParseExtraItemException(match[1], error);
122
+ }
123
+
124
+ lastIndex = regex.lastIndex;
125
+ }
126
+
127
+ // Add any remaining text after the last match
128
+ if (lastIndex < text.length) {
129
+ parts.push(text.slice(lastIndex));
130
+ }
131
+
132
+ return parts;
133
+ }
134
+ }
135
+
136
+ export declare namespace Text {
137
+ export type Options = {
138
+ joinString?: string;
139
+ };
140
+
141
+ export type OptionalString = string | undefined | null;
142
+
143
+ export type String<TExtraObject extends Text.ExtraObject.Base = never> =
144
+ string & {
145
+ __extra: TExtraObject;
146
+ };
147
+
148
+ export namespace ExtraObject {
149
+ export type Base = { type: string };
150
+
151
+ type InferExtraObjectValue<T> = T extends Text.OptionalString ? never : T;
152
+ type InferExtraObjectCallableResolvable<T> = InferExtraObjectValue<
153
+ CallableResolvable.Infer<T>
154
+ >;
155
+ type InferExtraArrayable<T> =
156
+ T extends Array<any>
157
+ ? { [K in keyof T]: InferExtraObjectCallableResolvable<T[K]> }[number]
158
+ : InferExtraObjectCallableResolvable<T>;
159
+
160
+ export type Infer<T extends any[]> = {
161
+ // Check if it's an array
162
+ [K in keyof T]: InferExtraArrayable<T[K]>;
163
+ }[number];
164
+ }
165
+
166
+ export type Value<TExtraObject extends Text.ExtraObject.Base = never> =
167
+ | Text.OptionalString
168
+ | TExtraObject;
169
+
170
+ export namespace Value {
171
+ export type Base = OptionalString | ExtraObject.Base;
172
+ }
173
+
174
+ export type TemplateValue<
175
+ TExtraObject extends Text.ExtraObject.Base = never,
176
+ > = CallableResolvable.MaybeArray<Value<TExtraObject>>;
177
+
178
+ export namespace TemplateValue {
179
+ export type Base = TemplateValue<ExtraObject.Base>;
180
+
181
+ export type Resolved<TExtraObject extends Text.ExtraObject.Base = never> =
182
+ MaybeArray<Value<TExtraObject>>;
183
+ export namespace Resolved {
184
+ export type Base = Resolved<ExtraObject.Base>;
185
+ }
186
+ }
187
+
188
+ export type Return<T extends Array<Text.TemplateValue.Base>> =
189
+ true extends CallableResolvable.MaybeArray.ArrayOf.IsPromise<T>
190
+ ? Promise<string & { __extra: ExtraObject.Infer<T> }>
191
+ : string & { __extra: ExtraObject.Infer<T> };
192
+ }
193
+
194
+ export class TextParseExtraItemException extends Error {
195
+ constructor(itemString: string, cause: any) {
196
+ super(
197
+ `
198
+ Failed to parse extra item serialized value
199
+
200
+ Value:
201
+ ${str(itemString).indent(2).toString()}
202
+
203
+ Cause:
204
+ ${str(cause instanceof Error ? cause.message : (cause as string))
205
+ .indent(2)
206
+ .toString()}
207
+
208
+ `.trimStart(),
209
+ { cause },
210
+ );
211
+ }
212
+ }
213
+
214
+ export const t = Text.t;
215
+ export const tParse = Text.parse;