@synstack/text 1.1.9 → 1.2.1

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 CHANGED
@@ -4,9 +4,6 @@
4
4
 
5
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
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
7
  ## What is it for ?
11
8
 
12
9
  Turns this:
@@ -59,8 +59,10 @@ var Text = class _Text {
59
59
  const value = values2[i];
60
60
  let wrappedValue = "";
61
61
  if (Array.isArray(value)) {
62
- wrappedValue = value.filter((inner) => inner !== null || inner !== void 0).map(_Text.wrapValue).join(this._options.joinString);
63
- } else {
62
+ wrappedValue = value.filter(
63
+ (inner) => inner !== null && inner !== void 0 && inner !== false
64
+ ).map(_Text.wrapValue).join(this._options.joinString);
65
+ } else if (value !== null && value !== void 0 && value !== false) {
64
66
  wrappedValue = _Text.wrapValue(value);
65
67
  }
66
68
  const nextString = template[i + 1];
@@ -72,7 +74,7 @@ var Text = class _Text {
72
74
  }).$;
73
75
  }
74
76
  static wrapValue(value) {
75
- if (value === null || value === void 0) return "";
77
+ if (value === null || value === void 0 || value === false) return "";
76
78
  if (typeof value === "object") {
77
79
  if (!Object.hasOwn(value, "type")) {
78
80
  throw new Error(
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/text.index.ts","../src/text.lib.ts"],"sourcesContent":["export * from \"./text.bundle.ts\";\nexport { TextParseExtraItemException } from \"./text.lib.ts\";\nexport type { Text } from \"./text.lib.ts\";\n","import { json } from \"@synstack/json\";\nimport { resolvable, type Resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\nimport { type MaybeArray } from \"../../shared/src/ts.utils.ts\";\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\n/**\n * @param condition - The condition to check, can be:\n * - primitive values\n * - promise of a primitive value\n * @returns The result of the template string if the resolved value is true, otherwise an empty string\n *\n * @example\n * ```ts\n * const text = tIf(true)`Hello World`;\n * assert.equal(text, \"Hello World\");\n * ```\n */\nexport const tIf =\n <CHECK extends Resolvable<any>>(condition: CHECK) =>\n <TValues extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...args: TValues\n ): CHECK extends Promise<any>\n ? // CHECK is a promise\n Text.Return<TValues> extends Promise<any>\n ? // Text.Return<TValues> is a promise as well, so we don't need to wrap it in a promise\n Text.Return<TValues>\n : // Text.Return<TValues> is not a promise, so we need to wrap it in a promise\n Promise<Text.Return<TValues>>\n : // Neither are promises, so we don't need to wrap the return value in a promise\n Text.Return<TValues> => {\n if (!condition) return \"\" as any;\n\n if (condition instanceof Promise)\n return condition.then((c) =>\n c ? t(template, ...args) : (\"\" as any),\n ) as any;\n\n return t(template, ...args) as any;\n };\n\n/**\n * Serializes and formats a template string with values into a single string while preserving extra values typings\n * @returns The serialized and formatted string\n *\n * @features\n * - Promises even nested in arrays are resolved in parallel\n * - Array values are joined with a newline\n * - Text is trimmed\n * - Base indentation is removed\n * - Nested indentation is preserved for multi-line values\n * - Returned value is either a string or a promise of a string based on interpolated values\n * - Extra values are serialized and added to the returned value type\n *\n * @example\n * For the following template:\n * ```ts\n * const text: string = t`\n * Hello\n * ${\"- Item 1\\n- Item 2\"}\n * World\n * `;\n * ```\n *\n * The output will be:\n * ```plain\n * Hello\n * - Item 1\n * - Item 2\n * World\n * ```\n */\nexport const t = Text.t;\n\n/**\n * Deserializes a serialized string with extra values\n *\n * @param text - The text string to parse\n * @returns Array of strings and extra objects\n *\n * @example\n * ```ts\n * const text = t`Hello ${{type: \"item\", value: \"World\"}}`;\n * assert.equal(tParse(text), [\"Hello\", { type: \"item\", value: \"World\" }]);\n * ```\n */\nexport const tParse = Text.parse;\n"],"mappings":"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","names":["values"]}
1
+ {"version":3,"sources":["../src/text.index.ts","../src/text.lib.ts"],"sourcesContent":["export * from \"./text.bundle.ts\";\nexport { TextParseExtraItemException } from \"./text.lib.ts\";\nexport type { Text } from \"./text.lib.ts\";\n","import { json } from \"@synstack/json\";\nimport { resolvable, type Resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\nimport { type MaybeArray } from \"../../shared/src/ts.utils.ts\";\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(\n (inner) =>\n inner !== null && inner !== undefined && inner !== false,\n )\n .map(Text.wrapValue)\n .join(this._options.joinString);\n } else if (value !== null && value !== undefined && value !== false) {\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): string {\n if (value === null || value === undefined || value === false) 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 | false;\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\n/**\n * @deprecated\n * Use the `&&` operator instead. `t` will filter out null, undefined, and false values.\n * Conditions are prefered over `tIf` because with `tIf` the interpolated values are still executed even if the value is falsy and the condition is not met.\n * `tIf` also doesn't support type narrowing to the interpolated values so this will trigger a type error:\n * ```ts\n * tIf(typeof value === \"string\")`${value}` // Likely type error as the type of value is not narrowed to a string\n * ```\n *\n * @param condition - The condition to check, can be:\n * - primitive values\n * - promise of a primitive value\n *\n * @returns The result of the template string if the resolved value is true, otherwise an empty string\n *\n * @example\n * ```ts\n * const text = tIf(true)`Hello World`;\n * assert.equal(text, \"Hello World\");\n * ```\n */\nexport const tIf =\n <CHECK extends Resolvable<any>>(condition: CHECK) =>\n <TValues extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...args: TValues\n ): CHECK extends Promise<any>\n ? // CHECK is a promise\n Text.Return<TValues> extends Promise<any>\n ? // Text.Return<TValues> is a promise as well, so we don't need to wrap it in a promise\n Text.Return<TValues>\n : // Text.Return<TValues> is not a promise, so we need to wrap it in a promise\n Promise<Text.Return<TValues>>\n : // Neither are promises, so we don't need to wrap the return value in a promise\n Text.Return<TValues> => {\n if (!condition) return \"\" as any;\n\n if (condition instanceof Promise)\n return condition.then((c) =>\n c ? t(template, ...args) : (\"\" as any),\n ) as any;\n\n return t(template, ...args) as any;\n };\n\n/**\n * Serializes and formats a template string with values into a single string while preserving extra values typings\n * @returns The serialized and formatted string\n *\n * @features\n * - Promises even nested in arrays are resolved in parallel\n * - Array values are joined with a newline\n * - Text is trimmed\n * - Base indentation is removed\n * - Nested indentation is preserved for multi-line values\n * - Returned value is either a string or a promise of a string based on interpolated values\n * - Extra values are serialized and added to the returned value type\n *\n * @example\n * For the following template:\n * ```ts\n * const text: string = t`\n * Hello\n * ${\"- Item 1\\n- Item 2\"}\n * World\n * `;\n * ```\n *\n * The output will be:\n * ```plain\n * Hello\n * - Item 1\n * - Item 2\n * World\n * ```\n */\nexport const t = Text.t;\n\n/**\n * Deserializes a serialized string with extra values\n *\n * @param text - The text string to parse\n * @returns Array of strings and extra objects\n *\n * @example\n * ```ts\n * const text = t`Hello ${{type: \"item\", value: \"World\"}}`;\n * assert.equal(tParse(text), [\"Hello\", { type: \"item\", value: \"World\" }]);\n * ```\n */\nexport const tParse = Text.parse;\n"],"mappings":"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","names":["values"]}
@@ -1,3 +1,4 @@
1
+ import { Resolvable } from '@synstack/resolved';
1
2
  import { CallableResolvable } from '@synstack/resolved/callable';
2
3
 
3
4
  /**
@@ -21,7 +22,7 @@ declare namespace Text {
21
22
  type Options = {
22
23
  joinString?: string;
23
24
  };
24
- type OptionalString = string | undefined | null;
25
+ type OptionalString = string | undefined | null | false;
25
26
  type String<TExtraObject extends Text.ExtraObject.Base = never> = string & {
26
27
  __extra: TExtraObject;
27
28
  };
@@ -60,9 +61,18 @@ declare class TextParseExtraItemException extends Error {
60
61
  constructor(itemString: string, cause: any);
61
62
  }
62
63
  /**
64
+ * @deprecated
65
+ * Use the `&&` operator instead. `t` will filter out null, undefined, and false values.
66
+ * Conditions are prefered over `tIf` because with `tIf` the interpolated values are still executed even if the value is falsy and the condition is not met.
67
+ * `tIf` also doesn't support type narrowing to the interpolated values so this will trigger a type error:
68
+ * ```ts
69
+ * tIf(typeof value === "string")`${value}` // Likely type error as the type of value is not narrowed to a string
70
+ * ```
71
+ *
63
72
  * @param condition - The condition to check, can be:
64
73
  * - primitive values
65
74
  * - promise of a primitive value
75
+ *
66
76
  * @returns The result of the template string if the resolved value is true, otherwise an empty string
67
77
  *
68
78
  * @example
@@ -71,7 +81,7 @@ declare class TextParseExtraItemException extends Error {
71
81
  * assert.equal(text, "Hello World");
72
82
  * ```
73
83
  */
74
- declare const tIf: <CHECK extends unknown>(condition: CHECK) => <TValues extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...args: TValues) => CHECK extends Promise<any> ? Text.Return<TValues> extends Promise<any> ? Text.Return<TValues> : Promise<Text.Return<TValues>> : Text.Return<TValues>;
84
+ declare const tIf: <CHECK extends Resolvable<any>>(condition: CHECK) => <TValues extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...args: TValues) => CHECK extends Promise<any> ? Text.Return<TValues> extends Promise<any> ? Text.Return<TValues> : Promise<Text.Return<TValues>> : Text.Return<TValues>;
75
85
  /**
76
86
  * Serializes and formats a template string with values into a single string while preserving extra values typings
77
87
  * @returns The serialized and formatted string
@@ -1,3 +1,4 @@
1
+ import { Resolvable } from '@synstack/resolved';
1
2
  import { CallableResolvable } from '@synstack/resolved/callable';
2
3
 
3
4
  /**
@@ -21,7 +22,7 @@ declare namespace Text {
21
22
  type Options = {
22
23
  joinString?: string;
23
24
  };
24
- type OptionalString = string | undefined | null;
25
+ type OptionalString = string | undefined | null | false;
25
26
  type String<TExtraObject extends Text.ExtraObject.Base = never> = string & {
26
27
  __extra: TExtraObject;
27
28
  };
@@ -60,9 +61,18 @@ declare class TextParseExtraItemException extends Error {
60
61
  constructor(itemString: string, cause: any);
61
62
  }
62
63
  /**
64
+ * @deprecated
65
+ * Use the `&&` operator instead. `t` will filter out null, undefined, and false values.
66
+ * Conditions are prefered over `tIf` because with `tIf` the interpolated values are still executed even if the value is falsy and the condition is not met.
67
+ * `tIf` also doesn't support type narrowing to the interpolated values so this will trigger a type error:
68
+ * ```ts
69
+ * tIf(typeof value === "string")`${value}` // Likely type error as the type of value is not narrowed to a string
70
+ * ```
71
+ *
63
72
  * @param condition - The condition to check, can be:
64
73
  * - primitive values
65
74
  * - promise of a primitive value
75
+ *
66
76
  * @returns The result of the template string if the resolved value is true, otherwise an empty string
67
77
  *
68
78
  * @example
@@ -71,7 +81,7 @@ declare class TextParseExtraItemException extends Error {
71
81
  * assert.equal(text, "Hello World");
72
82
  * ```
73
83
  */
74
- declare const tIf: <CHECK extends unknown>(condition: CHECK) => <TValues extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...args: TValues) => CHECK extends Promise<any> ? Text.Return<TValues> extends Promise<any> ? Text.Return<TValues> : Promise<Text.Return<TValues>> : Text.Return<TValues>;
84
+ declare const tIf: <CHECK extends Resolvable<any>>(condition: CHECK) => <TValues extends Array<Text.TemplateValue.Base>>(template: TemplateStringsArray, ...args: TValues) => CHECK extends Promise<any> ? Text.Return<TValues> extends Promise<any> ? Text.Return<TValues> : Promise<Text.Return<TValues>> : Text.Return<TValues>;
75
85
  /**
76
86
  * Serializes and formats a template string with values into a single string while preserving extra values typings
77
87
  * @returns The serialized and formatted string
@@ -30,8 +30,10 @@ var Text = class _Text {
30
30
  const value = values2[i];
31
31
  let wrappedValue = "";
32
32
  if (Array.isArray(value)) {
33
- wrappedValue = value.filter((inner) => inner !== null || inner !== void 0).map(_Text.wrapValue).join(this._options.joinString);
34
- } else {
33
+ wrappedValue = value.filter(
34
+ (inner) => inner !== null && inner !== void 0 && inner !== false
35
+ ).map(_Text.wrapValue).join(this._options.joinString);
36
+ } else if (value !== null && value !== void 0 && value !== false) {
35
37
  wrappedValue = _Text.wrapValue(value);
36
38
  }
37
39
  const nextString = template[i + 1];
@@ -43,7 +45,7 @@ var Text = class _Text {
43
45
  }).$;
44
46
  }
45
47
  static wrapValue(value) {
46
- if (value === null || value === void 0) return "";
48
+ if (value === null || value === void 0 || value === false) return "";
47
49
  if (typeof value === "object") {
48
50
  if (!Object.hasOwn(value, "type")) {
49
51
  throw new Error(
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/text.lib.ts"],"sourcesContent":["import { json } from \"@synstack/json\";\nimport { resolvable, type Resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\nimport { type MaybeArray } from \"../../shared/src/ts.utils.ts\";\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\n/**\n * @param condition - The condition to check, can be:\n * - primitive values\n * - promise of a primitive value\n * @returns The result of the template string if the resolved value is true, otherwise an empty string\n *\n * @example\n * ```ts\n * const text = tIf(true)`Hello World`;\n * assert.equal(text, \"Hello World\");\n * ```\n */\nexport const tIf =\n <CHECK extends Resolvable<any>>(condition: CHECK) =>\n <TValues extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...args: TValues\n ): CHECK extends Promise<any>\n ? // CHECK is a promise\n Text.Return<TValues> extends Promise<any>\n ? // Text.Return<TValues> is a promise as well, so we don't need to wrap it in a promise\n Text.Return<TValues>\n : // Text.Return<TValues> is not a promise, so we need to wrap it in a promise\n Promise<Text.Return<TValues>>\n : // Neither are promises, so we don't need to wrap the return value in a promise\n Text.Return<TValues> => {\n if (!condition) return \"\" as any;\n\n if (condition instanceof Promise)\n return condition.then((c) =>\n c ? t(template, ...args) : (\"\" as any),\n ) as any;\n\n return t(template, ...args) as any;\n };\n\n/**\n * Serializes and formats a template string with values into a single string while preserving extra values typings\n * @returns The serialized and formatted string\n *\n * @features\n * - Promises even nested in arrays are resolved in parallel\n * - Array values are joined with a newline\n * - Text is trimmed\n * - Base indentation is removed\n * - Nested indentation is preserved for multi-line values\n * - Returned value is either a string or a promise of a string based on interpolated values\n * - Extra values are serialized and added to the returned value type\n *\n * @example\n * For the following template:\n * ```ts\n * const text: string = t`\n * Hello\n * ${\"- Item 1\\n- Item 2\"}\n * World\n * `;\n * ```\n *\n * The output will be:\n * ```plain\n * Hello\n * - Item 1\n * - Item 2\n * World\n * ```\n */\nexport const t = Text.t;\n\n/**\n * Deserializes a serialized string with extra values\n *\n * @param text - The text string to parse\n * @returns Array of strings and extra objects\n *\n * @example\n * ```ts\n * const text = t`Hello ${{type: \"item\", value: \"World\"}}`;\n * assert.equal(tParse(text), [\"Hello\", { type: \"item\", value: \"World\" }]);\n * ```\n */\nexport const tParse = Text.parse;\n"],"mappings":"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","names":["values"]}
1
+ {"version":3,"sources":["../src/text.lib.ts"],"sourcesContent":["import { json } from \"@synstack/json\";\nimport { resolvable, type Resolvable } from \"@synstack/resolved\";\nimport { callable, type CallableResolvable } from \"@synstack/resolved/callable\";\nimport { str } from \"@synstack/str\";\nimport { type MaybeArray } from \"../../shared/src/ts.utils.ts\";\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(\n (inner) =>\n inner !== null && inner !== undefined && inner !== false,\n )\n .map(Text.wrapValue)\n .join(this._options.joinString);\n } else if (value !== null && value !== undefined && value !== false) {\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): string {\n if (value === null || value === undefined || value === false) 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 | false;\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\n/**\n * @deprecated\n * Use the `&&` operator instead. `t` will filter out null, undefined, and false values.\n * Conditions are prefered over `tIf` because with `tIf` the interpolated values are still executed even if the value is falsy and the condition is not met.\n * `tIf` also doesn't support type narrowing to the interpolated values so this will trigger a type error:\n * ```ts\n * tIf(typeof value === \"string\")`${value}` // Likely type error as the type of value is not narrowed to a string\n * ```\n *\n * @param condition - The condition to check, can be:\n * - primitive values\n * - promise of a primitive value\n *\n * @returns The result of the template string if the resolved value is true, otherwise an empty string\n *\n * @example\n * ```ts\n * const text = tIf(true)`Hello World`;\n * assert.equal(text, \"Hello World\");\n * ```\n */\nexport const tIf =\n <CHECK extends Resolvable<any>>(condition: CHECK) =>\n <TValues extends Array<Text.TemplateValue.Base>>(\n template: TemplateStringsArray,\n ...args: TValues\n ): CHECK extends Promise<any>\n ? // CHECK is a promise\n Text.Return<TValues> extends Promise<any>\n ? // Text.Return<TValues> is a promise as well, so we don't need to wrap it in a promise\n Text.Return<TValues>\n : // Text.Return<TValues> is not a promise, so we need to wrap it in a promise\n Promise<Text.Return<TValues>>\n : // Neither are promises, so we don't need to wrap the return value in a promise\n Text.Return<TValues> => {\n if (!condition) return \"\" as any;\n\n if (condition instanceof Promise)\n return condition.then((c) =>\n c ? t(template, ...args) : (\"\" as any),\n ) as any;\n\n return t(template, ...args) as any;\n };\n\n/**\n * Serializes and formats a template string with values into a single string while preserving extra values typings\n * @returns The serialized and formatted string\n *\n * @features\n * - Promises even nested in arrays are resolved in parallel\n * - Array values are joined with a newline\n * - Text is trimmed\n * - Base indentation is removed\n * - Nested indentation is preserved for multi-line values\n * - Returned value is either a string or a promise of a string based on interpolated values\n * - Extra values are serialized and added to the returned value type\n *\n * @example\n * For the following template:\n * ```ts\n * const text: string = t`\n * Hello\n * ${\"- Item 1\\n- Item 2\"}\n * World\n * `;\n * ```\n *\n * The output will be:\n * ```plain\n * Hello\n * - Item 1\n * - Item 2\n * World\n * ```\n */\nexport const t = Text.t;\n\n/**\n * Deserializes a serialized string with extra values\n *\n * @param text - The text string to parse\n * @returns Array of strings and extra objects\n *\n * @example\n * ```ts\n * const text = t`Hello ${{type: \"item\", value: \"World\"}}`;\n * assert.equal(tParse(text), [\"Hello\", { type: \"item\", value: \"World\" }]);\n * ```\n */\nexport const tParse = Text.parse;\n"],"mappings":";AAAA,SAAS,YAAY;AACrB,SAAS,kBAAmC;AAC5C,SAAS,gBAAyC;AAClD,SAAS,WAAW;AAGb,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;AAAA,YACC,CAAC,UACC,UAAU,QAAQ,UAAU,UAAa,UAAU;AAAA,UACvD,EACC,IAAI,MAAK,SAAS,EAClB,KAAK,KAAK,SAAS,UAAU;AAAA,QAClC,WAAW,UAAU,QAAQ,UAAU,UAAa,UAAU,OAAO;AACnE,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,OAAgC;AACnE,QAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,MAAO,QAAO;AACrE,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;AAuBO,IAAM,MACX,CAAgC,cAChC,CACE,aACG,SASuB;AAC1B,MAAI,CAAC,UAAW,QAAO;AAEvB,MAAI,qBAAqB;AACvB,WAAO,UAAU;AAAA,MAAK,CAAC,MACrB,IAAI,EAAE,UAAU,GAAG,IAAI,IAAK;AAAA,IAC9B;AAEF,SAAO,EAAE,UAAU,GAAG,IAAI;AAC5B;AAiCK,IAAM,IAAI,KAAK;AAcf,IAAM,SAAS,KAAK;","names":["values"]}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "1.1.9",
7
+ "version": "1.2.1",
8
8
  "description": "String templating as it was meant to be",
9
9
  "keywords": [
10
10
  "string",
@@ -50,19 +50,19 @@
50
50
  }
51
51
  },
52
52
  "dependencies": {
53
- "@synstack/json": "1.1.6",
54
- "@synstack/resolved": "1.2.3",
55
- "@synstack/str": "1.2.5"
53
+ "@synstack/json": "1.1.7",
54
+ "@synstack/resolved": "1.2.4",
55
+ "@synstack/str": "1.2.6"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "^22.10.1",
59
59
  "tsup": "^8.4.0",
60
- "typescript": "^5.7.3"
60
+ "typescript": "^5.8.2"
61
61
  },
62
62
  "files": [
63
63
  "src/**/*.ts",
64
64
  "!src/**/*.test.ts",
65
65
  "dist/**/*"
66
66
  ],
67
- "gitHead": "9351f651b39cf757bc37c3e2ab9c516c0e2ed127"
67
+ "gitHead": "4aae6e86f7664e5b928ecb3a492ec035307168d1"
68
68
  }
package/src/text.lib.ts CHANGED
@@ -51,10 +51,13 @@ export class Text {
51
51
 
52
52
  if (Array.isArray(value)) {
53
53
  wrappedValue = value
54
- .filter((inner) => inner !== null || inner !== undefined)
54
+ .filter(
55
+ (inner) =>
56
+ inner !== null && inner !== undefined && inner !== false,
57
+ )
55
58
  .map(Text.wrapValue)
56
59
  .join(this._options.joinString);
57
- } else {
60
+ } else if (value !== null && value !== undefined && value !== false) {
58
61
  wrappedValue = Text.wrapValue(value);
59
62
  }
60
63
  const nextString = template[i + 1];
@@ -80,8 +83,8 @@ export class Text {
80
83
  }).$ as Text.Return<T>;
81
84
  }
82
85
 
83
- private static wrapValue(this: void, value: Text.Value.Base) {
84
- if (value === null || value === undefined) return "";
86
+ private static wrapValue(this: void, value: Text.Value.Base): string {
87
+ if (value === null || value === undefined || value === false) return "";
85
88
  if (typeof value === "object") {
86
89
  if (!Object.hasOwn(value, "type")) {
87
90
  throw new Error(
@@ -138,7 +141,7 @@ export declare namespace Text {
138
141
  joinString?: string;
139
142
  };
140
143
 
141
- export type OptionalString = string | undefined | null;
144
+ export type OptionalString = string | undefined | null | false;
142
145
 
143
146
  export type String<TExtraObject extends Text.ExtraObject.Base = never> =
144
147
  string & {
@@ -212,9 +215,18 @@ ${str(cause instanceof Error ? cause.message : (cause as string))
212
215
  }
213
216
 
214
217
  /**
218
+ * @deprecated
219
+ * Use the `&&` operator instead. `t` will filter out null, undefined, and false values.
220
+ * Conditions are prefered over `tIf` because with `tIf` the interpolated values are still executed even if the value is falsy and the condition is not met.
221
+ * `tIf` also doesn't support type narrowing to the interpolated values so this will trigger a type error:
222
+ * ```ts
223
+ * tIf(typeof value === "string")`${value}` // Likely type error as the type of value is not narrowed to a string
224
+ * ```
225
+ *
215
226
  * @param condition - The condition to check, can be:
216
227
  * - primitive values
217
228
  * - promise of a primitive value
229
+ *
218
230
  * @returns The result of the template string if the resolved value is true, otherwise an empty string
219
231
  *
220
232
  * @example