@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 +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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAAqB;AACrB,sBAA2B;AAC3B,sBAAkD;AAClD,iBAAoB;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,yBAAS,cAAc,MAAM;AACpD,WAAO,2BAAW,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,eAAW,gBAAI,IAAI,EAAE,SAAS;AACpC,cAAM,cAAc,SAAS,QAAQ,IACjC,SAAS,mBAAmB,IAC5B;AACJ,mBACE,gBAAI,IAAI,EAAE,QAAQ,WAAW,EAAE,SAAS,QACxC,gBAAI,YAAY,EAAE,OAAO,WAAW,EAAE,SAAS,IAC/C;AAAA,MACJ;AAEA,iBAAO,gBAAI,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,iBAAK,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,MAIJ,gBAAI,UAAU,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC;AAAA;AAAA;AAAA,MAGpC,gBAAI,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"]}
|
@@ -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;
|