@tannin/sprintf 1.3.1 → 1.3.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/types/index.d.ts +213 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tannin/sprintf",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "type": "module",
5
5
  "description": "printf string formatter",
6
6
  "exports": {
package/types/index.d.ts CHANGED
@@ -1,87 +1,244 @@
1
+ // ---- Base Types ----
2
+
1
3
  type Specifiers = {
2
4
  s: string;
3
5
  d: number;
4
6
  f: number;
5
7
  };
8
+
6
9
  type S = keyof Specifiers;
7
10
 
11
+ // ---- Tuple Math Utilities ----
12
+
13
+ // Builds a tuple of length L
8
14
  type BuildTuple<L extends number, T extends any[] = []> = T['length'] extends L
9
15
  ? T
10
16
  : BuildTuple<L, [any, ...T]>;
11
17
 
12
- type Subtract1<N extends number> = BuildTuple<N> extends [any, ...infer Rest]
13
- ? Rest['length']
18
+ // Adds two numbers by building and merging tuples
19
+ type Add<A extends number, B extends number> = [
20
+ ...BuildTuple<A>,
21
+ ...BuildTuple<B>,
22
+ ]['length'] extends infer R extends number
23
+ ? R
14
24
  : never;
15
25
 
26
+ // --- Escaped Percent Handling ----
27
+
28
+ // Removes escaped double-percent (%%) sequences from a format string
16
29
  type StripEscapedPercents<T extends string> =
17
30
  T extends `${infer Head}%%${infer Tail}`
18
31
  ? `${Head}${StripEscapedPercents<Tail>}`
19
32
  : T;
20
33
 
21
- type HasNamedPlaceholders<T extends string> =
22
- StripEscapedPercents<T> extends `${any}%(${string})${S}${string}`
23
- ? true
24
- : false;
34
+ // ---- Specifier Utilities ----
25
35
 
26
- type HasPositionalPlaceholders<T extends string> =
27
- StripEscapedPercents<T> extends `${any}%${number}$${S}${string}`
28
- ? true
29
- : false;
36
+ // Checks if a character is a valid format specifier
37
+ type IsValidSpec<C extends string> = C extends S ? C : never;
38
+ // ---- Format Specifier Parsing ----
30
39
 
31
- type HasUnnamedPlaceholders<T extends string> =
32
- StripEscapedPercents<T> extends `${any}%${S}${string}` ? true : false;
40
+ // Parses precision and format specifier, supports dynamic precision (e.g. %.*f)
41
+ type ParsePrecisionAndSpec<T extends string> =
42
+ T extends `.*${infer RawSpec}${infer Rest}`
43
+ ? IsValidSpec<RawSpec> extends infer Spec extends S
44
+ ? ['.*', Spec, Rest]
45
+ : never
46
+ : T extends `0.${infer PrecisionAndSpec}${infer RestAfterDigits}`
47
+ ? PrecisionAndSpec extends `${number}`
48
+ ? RestAfterDigits extends `${infer RawSpec}${infer Rest}`
49
+ ? IsValidSpec<RawSpec> extends infer Spec extends S
50
+ ? [`.${PrecisionAndSpec}`, Spec, Rest]
51
+ : never
52
+ : never
53
+ : never
54
+ : T extends `.${infer PrecisionAndSpec}${infer RestAfterDigits}`
55
+ ? PrecisionAndSpec extends `${number}`
56
+ ? RestAfterDigits extends `${infer RawSpec}${infer Rest}`
57
+ ? IsValidSpec<RawSpec> extends infer Spec extends S
58
+ ? [`.${PrecisionAndSpec}`, Spec, Rest]
59
+ : never
60
+ : never
61
+ : never
62
+ : T extends `${infer RawSpec}${infer Rest}`
63
+ ? IsValidSpec<RawSpec> extends infer Spec extends S
64
+ ? ['', Spec, Rest]
65
+ : never
66
+ : never;
33
67
 
34
- type HasDynamicPrecisionPlaceholders<T extends string> =
35
- StripEscapedPercents<T> extends `${any}%.*${S}${any}` ? true : false;
68
+ // ---- Positional Placeholder Parsing ----
36
69
 
37
- type HasStaticPrecisionPlaceholders<T extends string> =
38
- StripEscapedPercents<T> extends `${any}%.${number}${S}${any}` ? true : false;
70
+ // Recursively collects all positional placeholders and their expected types
71
+ type CollectPositionalPlaceholders<
72
+ T extends string,
73
+ Collected extends Record<string, any> = {},
74
+ > =
75
+ StripEscapedPercents<T> extends `${infer _}%${infer PosStr}$${infer AfterPos}`
76
+ ? PosStr extends `${number}`
77
+ ? ParsePrecisionAndSpec<AfterPos> extends [
78
+ infer Precision extends string,
79
+ infer Spec extends S,
80
+ infer Rest extends string,
81
+ ]
82
+ ? IsValidSpec<Spec> extends never
83
+ ? never
84
+ : Precision extends '.*'
85
+ ? CollectPositionalPlaceholders<
86
+ Rest,
87
+ Collected & { [K in PosStr]: [number, Specifiers[Spec]] }
88
+ >
89
+ : CollectPositionalPlaceholders<
90
+ Rest,
91
+ Collected & { [K in PosStr]: Specifiers[Spec] }
92
+ >
93
+ : CollectPositionalPlaceholders<AfterPos, Collected>
94
+ : Collected
95
+ : Collected;
39
96
 
40
- type ExtractNamedPlaceholders<T extends string> =
41
- StripEscapedPercents<T> extends `${any}%(${infer Key})${infer Spec}${infer Rest}`
42
- ? Spec extends S
43
- ? { [K in Key]: Specifiers[Spec] } & ExtractNamedPlaceholders<Rest>
44
- : never
45
- : {};
97
+ // ---- Positional Argument Extraction ----
46
98
 
99
+ // Gets the max index used in positional placeholders
100
+ type GetMaxPosition<T extends Record<string, any>> = {
101
+ [K in keyof T]: K extends `${infer N extends number}` ? N : never;
102
+ }[keyof T];
103
+
104
+ // Computes max of a number by building up a tuple
105
+ type Max<N extends number, A extends any[] = []> = [N] extends [
106
+ Partial<A>['length'],
107
+ ]
108
+ ? A['length']
109
+ : Max<N, [0, ...A]>;
110
+
111
+ // Builds a final tuple of arguments from the collected positional types
112
+ type BuildPositionalTuple<
113
+ Collected extends { [K: number]: any },
114
+ MaxPos extends number,
115
+ CurrentPos extends number = 1,
116
+ Result extends any[] = [],
117
+ > =
118
+ CurrentPos extends Add<MaxPos, 1>
119
+ ? Result
120
+ : `${CurrentPos}` extends keyof Collected
121
+ ? Collected[CurrentPos] extends [any, any]
122
+ ? BuildPositionalTuple<
123
+ Collected,
124
+ MaxPos,
125
+ Add<CurrentPos, 1>,
126
+ [...Result, ...Collected[CurrentPos]]
127
+ >
128
+ : BuildPositionalTuple<
129
+ Collected,
130
+ MaxPos,
131
+ Add<CurrentPos, 1>,
132
+ [...Result, Collected[CurrentPos]]
133
+ >
134
+ : BuildPositionalTuple<
135
+ Collected,
136
+ MaxPos,
137
+ Add<CurrentPos, 1>,
138
+ [...Result, unknown]
139
+ >;
140
+
141
+ // Main positional argument extractor
47
142
  type ExtractPositionalPlaceholders<T extends string> =
48
- StripEscapedPercents<T> extends `${any}%${infer Index extends number}$${infer Spec}${infer Rest}`
49
- ? Spec extends S
50
- ? {
51
- [K in Subtract1<Index>]: Specifiers[Spec];
52
- } & ExtractPositionalPlaceholders<Rest>
53
- : ExtractPositionalPlaceholders<Rest>
54
- : unknown[];
55
-
56
- type ExtractStaticPrecisionPlaceholders<T extends string> =
57
- StripEscapedPercents<T> extends `${any}%.${infer Precision extends number}${infer Spec}${infer Rest}`
58
- ? Spec extends S
59
- ? [Specifiers[Spec], ...ExtractStaticPrecisionPlaceholders<Rest>]
60
- : never
143
+ CollectPositionalPlaceholders<T> extends infer Collected extends Record<
144
+ string,
145
+ any
146
+ >
147
+ ? keyof Collected extends never
148
+ ? []
149
+ : Max<GetMaxPosition<Collected>> extends infer MaxPos extends number
150
+ ? BuildPositionalTuple<Collected, MaxPos>
151
+ : []
61
152
  : [];
62
153
 
63
- type ExtractDynamicPrecisionPlaceholder<T extends string> =
64
- StripEscapedPercents<T> extends `${any}%.*${infer Spec}${infer Rest}`
65
- ? Spec extends S
66
- ? [number, Specifiers[Spec], ...ExtractDynamicPrecisionPlaceholder<Rest>]
67
- : never
68
- : [];
154
+ // ---- Unnamed Placeholder Extraction ----
69
155
 
156
+ // Extracts unnamed placeholders like %s, %.2f, %.*d (ignores positional/named)
70
157
  type ExtractUnnamedPlaceholders<T extends string> =
71
- StripEscapedPercents<T> extends `${any}%${infer Spec}${infer Rest}`
72
- ? Spec extends S
73
- ? [Specifiers[Spec], ...ExtractUnnamedPlaceholders<Rest>]
74
- : never
158
+ StripEscapedPercents<T> extends `${infer _}%${infer AfterPercent}`
159
+ ? AfterPercent extends `${infer _Num extends `${number}`}$${infer AfterPositional}`
160
+ ? ExtractUnnamedPlaceholders<AfterPositional> // Skip positional
161
+ : ParsePrecisionAndSpec<AfterPercent> extends [
162
+ infer Precision extends string,
163
+ infer Spec extends S,
164
+ infer Rest extends string,
165
+ ]
166
+ ? [
167
+ ...(Precision extends '.*'
168
+ ? [number, Specifiers[Spec]]
169
+ : [Specifiers[Spec]]),
170
+ ...ExtractUnnamedPlaceholders<Rest>,
171
+ ]
172
+ : []
75
173
  : [];
76
174
 
77
- export type SprintfArgs<T extends string> = HasNamedPlaceholders<T> extends true
78
- ? [values: ExtractNamedPlaceholders<T>]
79
- : HasDynamicPrecisionPlaceholders<T> extends true
80
- ? ExtractDynamicPrecisionPlaceholder<T>
81
- : HasStaticPrecisionPlaceholders<T> extends true
82
- ? ExtractStaticPrecisionPlaceholders<T>
83
- : HasPositionalPlaceholders<T> extends true
84
- ? ExtractPositionalPlaceholders<T>
85
- : HasUnnamedPlaceholders<T> extends true
86
- ? ExtractUnnamedPlaceholders<T>
87
- : [];
175
+ // ---- Named Placeholder Extraction ----
176
+
177
+ // Extracts named placeholders like %(key)s, %(name).2f
178
+ type ExtractNamedPlaceholders<T extends string> =
179
+ StripEscapedPercents<T> extends `${infer _}%(${infer Key})${infer AfterKey}`
180
+ ? ParsePrecisionAndSpec<AfterKey> extends [
181
+ infer Precision extends string,
182
+ infer Spec extends S,
183
+ infer Rest extends string,
184
+ ]
185
+ ? Precision extends '.*'
186
+ ? never // Optional: disallow dynamic precision for named
187
+ : { [K in Key]: Specifiers[Spec] } & ExtractNamedPlaceholders<Rest>
188
+ : {}
189
+ : {};
190
+
191
+ // ---- Format Type Guards ----
192
+
193
+ // Checks if string has named placeholders
194
+ type HasNamedPlaceholders<T extends string> =
195
+ StripEscapedPercents<T> extends `${infer _}%(${string})${string}`
196
+ ? true
197
+ : false;
198
+
199
+ // Checks if string has positional placeholders
200
+ type HasPositionalPlaceholders<T extends string> =
201
+ StripEscapedPercents<T> extends `${infer _Before}%${infer Rest}`
202
+ ? Rest extends `${infer Index}$${infer _After}`
203
+ ? Index extends `${number}`
204
+ ? true
205
+ : HasPositionalPlaceholders<Rest>
206
+ : HasPositionalPlaceholders<Rest>
207
+ : false;
208
+
209
+ // Checks if string has unnamed placeholders
210
+ type HasUnnamedPlaceholders<T extends string> =
211
+ StripEscapedPercents<T> extends `${infer _}%${infer Body}`
212
+ ? Body extends `(${string})${string}` // Named — skip
213
+ ? HasUnnamedPlaceholders<Body>
214
+ : Body extends `${number}$${string}` // Positional — skip
215
+ ? HasUnnamedPlaceholders<Body>
216
+ : ParsePrecisionAndSpec<Body> extends [
217
+ infer _,
218
+ infer Spec extends S,
219
+ infer _,
220
+ ]
221
+ ? true
222
+ : HasUnnamedPlaceholders<Body>
223
+ : false;
224
+
225
+ // ---- Public API ----
226
+
227
+ // Extracts the argument types required for a sprintf-like string
228
+ export type SprintfArgs<T extends string> =
229
+ HasNamedPlaceholders<T> extends true
230
+ ? HasPositionalPlaceholders<T> extends true
231
+ ? [never] // Invalid: named + positional
232
+ : HasUnnamedPlaceholders<T> extends true
233
+ ? [never] // Invalid: named + unnamed
234
+ : [values: ExtractNamedPlaceholders<T>]
235
+ : HasPositionalPlaceholders<T> extends true
236
+ ? HasUnnamedPlaceholders<T> extends true
237
+ ? [
238
+ ...ExtractPositionalPlaceholders<T>,
239
+ ...ExtractUnnamedPlaceholders<T>,
240
+ ]
241
+ : ExtractPositionalPlaceholders<T>
242
+ : HasUnnamedPlaceholders<T> extends true
243
+ ? ExtractUnnamedPlaceholders<T>
244
+ : [];