@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 +173 -0
- package/dist/text.index.cjs +136 -0
- package/dist/text.index.cjs.map +1 -0
- package/dist/text.index.d.cts +65 -0
- package/dist/text.index.d.ts +65 -0
- package/dist/text.index.js +107 -0
- package/dist/text.index.js.map +1 -0
- package/package.json +71 -0
- package/src/text.bundle.ts +1 -0
- package/src/text.index.ts +3 -0
- package/src/text.lib.ts +215 -0
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";
|
package/src/text.lib.ts
ADDED
@@ -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;
|