@synstack/text 1.0.1-alpha.0

Sign up to get free protection for your applications and to get access to all the features.
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":";AACA,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,gBAAyC;AAClD,SAAS,WAAW;AAEb,IAAM,OAAN,MAAM,MAAK;AAAA,EAChB,OAAe,sBAAsB;AAAA,EACrC,OAAe,sBAAsB;AAAA,EAEpB;AAAA,EAIT,YAAY,UAAwB,CAAC,GAAG;AAC9C,SAAK,WAAW;AAAA,MACd,YAAY,QAAQ,cAAc;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,OAAc,QAAoB,SAAuB,CAAC,GAAG;AAC3D,WAAO,IAAI,MAAK,MAAM;AAAA,EACxB;AAAA,EAEO,QAAQ,QAAsB;AACnC,WAAO,IAAI,MAAK,EAAE,GAAG,KAAK,UAAU,GAAG,OAAO,CAAC;AAAA,EACjD;AAAA,EAEA,OAAc,EAEZ,aACG,QACa;AAChB,WAAO,MAAK,QAAQ,EAAE,EAAE,UAAU,GAAG,MAAM;AAAA,EAC7C;AAAA,EAEO,EACL,aACG,QACa;AAEhB,QAAI,SAAS,WAAW,EAAG,QAAO;AAElC,UAAM,iBAAiB,SAAS,cAAc,MAAM;AACpD,WAAO,WAAW,KAAK,cAAc,EAAE,EAAE,CAACA,YAAW;AACnD,UAAI,OAAO,SAAS,CAAC;AAErB,eAAS,IAAI,GAAG,IAAIA,QAAO,QAAQ,KAAK;AACtC,cAAM,QAAQA,QAAO,CAAC;AACtB,YAAI,eAAe;AAEnB,YAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,yBAAe,MACZ,OAAO,CAAC,UAAU,UAAU,QAAQ,UAAU,MAAS,EACvD,IAAI,MAAK,SAAS,EAClB,KAAK,KAAK,SAAS,UAAU;AAAA,QAClC,OAAO;AACL,yBAAe,MAAK,UAAU,KAAK;AAAA,QACrC;AACA,cAAM,aAAa,SAAS,IAAI,CAAC;AACjC,cAAM,WAAW,IAAI,IAAI,EAAE,SAAS;AACpC,cAAM,cAAc,SAAS,QAAQ,IACjC,SAAS,mBAAmB,IAC5B;AACJ,eACE,IAAI,IAAI,EAAE,QAAQ,WAAW,EAAE,SAAS,IACxC,IAAI,YAAY,EAAE,OAAO,WAAW,EAAE,SAAS,IAC/C;AAAA,MACJ;AAEA,aAAO,IAAI,IAAI,EACZ,oBAAoB,EACpB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,mBAAmB,CAAC,EACpB,SAAS;AAAA,IAGd,CAAC,EAAE;AAAA,EACL;AAAA,EAEA,OAAe,UAAsB,OAAwB;AAC3D,QAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,QAAI,OAAO,UAAU,UAAU;AAC7B,UAAI,CAAC,OAAO,OAAO,OAAO,MAAM,GAAG;AACjC,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,aAAO,GAAG,MAAK,mBAAmB,GAAG,KAAK,UAAU,KAAK,CAAC,GACxD,MAAK,mBACP;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA,EAEA,OAAc,MAEZ,MACmB;AACnB,UAAM,QAAQ,IAAI;AAAA,MAChB,MAAK,sBAAsB,UAAU,MAAK;AAAA,MAC1C;AAAA,IACF;AACA,UAAM,QAA2B,CAAC;AAClC,QAAI,YAAY;AAChB,QAAI;AAEJ,YAAQ,QAAQ,MAAM,KAAK,IAAI,OAAO,MAAM;AAE1C,UAAI,MAAM,QAAQ,WAAW;AAC3B,cAAM,KAAK,KAAK,MAAM,WAAW,MAAM,KAAK,CAAC;AAAA,MAC/C;AAGA,UAAI;AACF,cAAM,aAAa,KAAK,YAAe,MAAM,CAAC,CAAC;AAC/C,cAAM,KAAK,UAAU;AAAA,MACvB,SAAS,OAAO;AACd,cAAM,IAAI,4BAA4B,MAAM,CAAC,GAAG,KAAK;AAAA,MACvD;AAEA,kBAAY,MAAM;AAAA,IACpB;AAGA,QAAI,YAAY,KAAK,QAAQ;AAC3B,YAAM,KAAK,KAAK,MAAM,SAAS,CAAC;AAAA,IAClC;AAEA,WAAO;AAAA,EACT;AACF;AA4DO,IAAM,8BAAN,cAA0C,MAAM;AAAA,EACrD,YAAY,YAAoB,OAAY;AAC1C;AAAA,MACE;AAAA;AAAA;AAAA;AAAA,EAIJ,IAAI,UAAU,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC;AAAA;AAAA;AAAA,EAGpC,IAAI,iBAAiB,QAAQ,MAAM,UAAW,KAAgB,EAC7D,OAAO,CAAC,EACR,SAAS,CAAC;AAAA;AAAA,EAEX,UAAU;AAAA,MACN,EAAE,MAAM;AAAA,IACV;AAAA,EACF;AACF;AAEO,IAAM,IAAI,KAAK;AACf,IAAM,SAAS,KAAK;","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;