@joshuahhh/pretty-print 0.0.1 → 0.0.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/pretty-print.d.ts +10 -11
- package/dist/pretty-print.d.ts.map +1 -1
- package/dist/pretty-print.js +75 -66
- package/dist/pretty-print.test.d.ts +2 -0
- package/dist/pretty-print.test.d.ts.map +1 -0
- package/dist/pretty-print.test.js +168 -0
- package/dist/string-tagger.d.ts +17 -9
- package/dist/string-tagger.d.ts.map +1 -1
- package/dist/string-tagger.js +44 -51
- package/dist/string-tagger.test.d.ts +2 -0
- package/dist/string-tagger.test.d.ts.map +1 -0
- package/dist/string-tagger.test.js +181 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,QAAQ,GACT,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,QAAQ,GACT,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/pretty-print.d.ts
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export interface PrettyPrintOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
useColor?: boolean;
|
|
5
|
+
niceId?: boolean;
|
|
6
|
+
niceType?: boolean;
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* Converts ANSI color codes to browser console %c format with CSS styles.
|
|
4
10
|
*/
|
|
5
|
-
export declare function prettyPrintForBrowser(value: unknown,
|
|
11
|
+
export declare function prettyPrintForBrowser(value: unknown, options?: PrettyPrintOptions): [string, ...string[]];
|
|
6
12
|
/**
|
|
7
13
|
* Pretty-print a value to the browser console with colors.
|
|
8
14
|
* This is the easiest way to use the pretty-printer in browser code.
|
|
9
15
|
*/
|
|
10
|
-
export declare function prettyLog(value: unknown, { label,
|
|
16
|
+
export declare function prettyLog(value: unknown, { label, ...options }?: PrettyPrintOptions & {
|
|
11
17
|
label?: string;
|
|
12
|
-
width?: number;
|
|
13
18
|
}): void;
|
|
14
19
|
/**
|
|
15
20
|
* React component that pretty-prints a value and automatically adjusts
|
|
16
21
|
* to the width of its container.
|
|
17
22
|
*/
|
|
18
|
-
export declare function PrettyPrint({ value, style, className, }: {
|
|
23
|
+
export declare function PrettyPrint({ value, style, className, ...options }: PrettyPrintOptions & {
|
|
19
24
|
value: unknown;
|
|
20
25
|
style?: React.CSSProperties;
|
|
21
26
|
className?: string;
|
|
22
27
|
}): import("react/jsx-runtime").JSX.Element;
|
|
23
|
-
|
|
24
|
-
* Pretty-print a JavaScript value to a string.
|
|
25
|
-
* @param value The value to pretty-print
|
|
26
|
-
* @param printWidth Maximum line width (default: 80)
|
|
27
|
-
* @param useColor Whether to include colors (default: true)
|
|
28
|
-
*/
|
|
29
|
-
export declare function prettyPrintToString(value: unknown, printWidth?: number, useColor?: boolean): string;
|
|
28
|
+
export declare function prettyPrintToString(value: unknown, options?: PrettyPrintOptions): string;
|
|
30
29
|
export declare const testData: {
|
|
31
30
|
primitives: {
|
|
32
31
|
number: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pretty-print.d.ts","sourceRoot":"","sources":["../src/pretty-print.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"pretty-print.d.ts","sourceRoot":"","sources":["../src/pretty-print.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA2BD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,kBAAuB,GAC/B,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CA2BvB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,GAAE,kBAAkB,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAClE,IAAI,CAQN;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,SAAS,EACT,GAAG,OAAO,EACX,EAAE,kBAAkB,GAAG;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAyFA;AAyTD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CAWR;AAGD,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;CAYpB,CAAC"}
|
package/dist/pretty-print.js
CHANGED
|
@@ -21,8 +21,8 @@ const ANSI_TO_HEX = Object.fromEntries(COLORS.map((c) => [c.ansi.match(/\d+/)?.[
|
|
|
21
21
|
/**
|
|
22
22
|
* Converts ANSI color codes to browser console %c format with CSS styles.
|
|
23
23
|
*/
|
|
24
|
-
export function prettyPrintForBrowser(value,
|
|
25
|
-
const textWithAnsi = prettyPrintToString(value,
|
|
24
|
+
export function prettyPrintForBrowser(value, options = {}) {
|
|
25
|
+
const textWithAnsi = prettyPrintToString(value, options);
|
|
26
26
|
const ansiRegex = /\x1b\[(\d+)m/g;
|
|
27
27
|
const parts = [];
|
|
28
28
|
const styles = [];
|
|
@@ -50,11 +50,11 @@ export function prettyPrintForBrowser(value, printWidth = 80) {
|
|
|
50
50
|
* Pretty-print a value to the browser console with colors.
|
|
51
51
|
* This is the easiest way to use the pretty-printer in browser code.
|
|
52
52
|
*/
|
|
53
|
-
export function prettyLog(value, { label,
|
|
53
|
+
export function prettyLog(value, { label, ...options } = {}) {
|
|
54
54
|
if (label) {
|
|
55
55
|
console.group(label);
|
|
56
56
|
}
|
|
57
|
-
console.log(...prettyPrintForBrowser(value,
|
|
57
|
+
console.log(...prettyPrintForBrowser(value, options));
|
|
58
58
|
if (label) {
|
|
59
59
|
console.groupEnd();
|
|
60
60
|
}
|
|
@@ -63,10 +63,10 @@ export function prettyLog(value, { label, width = 120 } = {}) {
|
|
|
63
63
|
* React component that pretty-prints a value and automatically adjusts
|
|
64
64
|
* to the width of its container.
|
|
65
65
|
*/
|
|
66
|
-
export function PrettyPrint({ value, style, className, }) {
|
|
66
|
+
export function PrettyPrint({ value, style, className, ...options }) {
|
|
67
67
|
const containerRef = React.useRef(null);
|
|
68
68
|
const measureRef = React.useRef(null);
|
|
69
|
-
const [
|
|
69
|
+
const [measuredWidth, setMeasuredWidth] = React.useState(80);
|
|
70
70
|
React.useEffect(() => {
|
|
71
71
|
if (!containerRef.current || !measureRef.current)
|
|
72
72
|
return;
|
|
@@ -79,7 +79,7 @@ export function PrettyPrint({ value, style, className, }) {
|
|
|
79
79
|
const charWidth = sampleWidth / sampleLength;
|
|
80
80
|
const containerWidth = containerRef.current.offsetWidth;
|
|
81
81
|
const charsPerLine = Math.floor(containerWidth / charWidth);
|
|
82
|
-
|
|
82
|
+
setMeasuredWidth(charsPerLine);
|
|
83
83
|
};
|
|
84
84
|
// Initial measurement (with slight delay to ensure fonts are loaded)
|
|
85
85
|
setTimeout(updateWidth, 0);
|
|
@@ -89,7 +89,10 @@ export function PrettyPrint({ value, style, className, }) {
|
|
|
89
89
|
return () => resizeObserver.disconnect();
|
|
90
90
|
}, []);
|
|
91
91
|
// Convert ANSI color codes to React elements with inline styles
|
|
92
|
-
const textWithAnsi = prettyPrintToString(value,
|
|
92
|
+
const textWithAnsi = prettyPrintToString(value, {
|
|
93
|
+
...options,
|
|
94
|
+
width: options.width ?? measuredWidth,
|
|
95
|
+
});
|
|
93
96
|
const ansiRegex = /\x1b\[(\d+)m/g;
|
|
94
97
|
const elements = [];
|
|
95
98
|
let lastIndex = 0;
|
|
@@ -119,13 +122,13 @@ export function PrettyPrint({ value, style, className, }) {
|
|
|
119
122
|
* Pretty-print a JavaScript value using Prettier's doc builder API.
|
|
120
123
|
* Returns a Doc that can be printed with prettier.printDocToString()
|
|
121
124
|
*/
|
|
122
|
-
function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
123
|
-
|
|
124
|
-
const colorize = (text, colorType) => {
|
|
125
|
+
function prettyPrintToDoc(value, tagger, options, visited = new Set(), path = []) {
|
|
126
|
+
const { niceType = true, niceId = true } = options;
|
|
127
|
+
const colorize = (text, colorType, path) => {
|
|
125
128
|
if (!tagger)
|
|
126
129
|
return text;
|
|
127
130
|
const ansi = COLOR_NAME_TO_ANSI[colorType];
|
|
128
|
-
return tagger.tag(text, ansi, ANSI_RESET);
|
|
131
|
+
return tagger.tag(text, ansi, ANSI_RESET, path);
|
|
129
132
|
};
|
|
130
133
|
// Handle JSX elements
|
|
131
134
|
if (true) {
|
|
@@ -137,18 +140,27 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
137
140
|
: element.type.name || "Component";
|
|
138
141
|
const props = element.props;
|
|
139
142
|
const { children, ...otherProps } = props;
|
|
140
|
-
const openTag = colorize(`<${type}`, "keyword");
|
|
141
|
-
const closeTag = colorize(`</${type}>`, "keyword");
|
|
142
|
-
const selfCloseTag = colorize("/>", "keyword");
|
|
143
|
+
const openTag = colorize(`<${type}`, "keyword", [...path, 0]);
|
|
144
|
+
const closeTag = colorize(`</${type}>`, "keyword", [...path, 4]);
|
|
145
|
+
const selfCloseTag = colorize("/>", "keyword", [...path, 2]);
|
|
143
146
|
// Format props
|
|
144
147
|
const propEntries = Object.entries(otherProps);
|
|
145
148
|
const propDocs = [];
|
|
146
149
|
for (let i = 0; i < propEntries.length; i++) {
|
|
147
150
|
const [key, val] = propEntries[i];
|
|
148
|
-
const keyStr = colorize(key, "key");
|
|
151
|
+
const keyStr = colorize(key, "key", [...path, 1, i, 0]);
|
|
149
152
|
const valDoc = typeof val === "string"
|
|
150
|
-
? colorize(JSON.stringify(val), "string")
|
|
151
|
-
: [
|
|
153
|
+
? colorize(JSON.stringify(val), "string", [...path, 1, i, 1])
|
|
154
|
+
: [
|
|
155
|
+
"{",
|
|
156
|
+
prettyPrintToDoc(val, tagger, options, visited, [
|
|
157
|
+
...path,
|
|
158
|
+
1,
|
|
159
|
+
i,
|
|
160
|
+
1,
|
|
161
|
+
]),
|
|
162
|
+
"}",
|
|
163
|
+
];
|
|
152
164
|
propDocs.push(ifBreak(line, " "), keyStr, "=", valDoc);
|
|
153
165
|
}
|
|
154
166
|
const childrenArray = React.Children.toArray(children);
|
|
@@ -164,11 +176,10 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
164
176
|
selfCloseTag,
|
|
165
177
|
]);
|
|
166
178
|
}
|
|
167
|
-
const childDocs = childrenArray.map((child) => typeof child === "string" || typeof child === "number"
|
|
179
|
+
const childDocs = childrenArray.map((child, i) => typeof child === "string" || typeof child === "number"
|
|
168
180
|
? String(child)
|
|
169
|
-
: prettyPrintToDoc(child, tagger, visited));
|
|
181
|
+
: prettyPrintToDoc(child, tagger, options, visited, [...path, 3, i]));
|
|
170
182
|
// Add conditional line breaks between children
|
|
171
|
-
// When inline, no separator. When broken, each child on its own line.
|
|
172
183
|
const childDocsWithSeparators = [];
|
|
173
184
|
for (let i = 0; i < childDocs.length; i++) {
|
|
174
185
|
childDocsWithSeparators.push(childDocs[i]);
|
|
@@ -176,12 +187,10 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
176
187
|
childDocsWithSeparators.push(ifBreak(line, ""));
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
|
-
// Group the opening tag separately so it can stay on one line if it fits,
|
|
180
|
-
// then group the whole element to allow compact inline formatting when possible
|
|
181
190
|
const openingTag = group([
|
|
182
191
|
openTag,
|
|
183
192
|
indent(propDocs),
|
|
184
|
-
colorize(">", "keyword"),
|
|
193
|
+
colorize(">", "keyword", [...path, 2]),
|
|
185
194
|
]);
|
|
186
195
|
return group([
|
|
187
196
|
openingTag,
|
|
@@ -193,17 +202,17 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
193
202
|
}
|
|
194
203
|
// Handle primitives
|
|
195
204
|
if (value === null)
|
|
196
|
-
return colorize("null", "null");
|
|
205
|
+
return colorize("null", "null", path);
|
|
197
206
|
if (value === undefined)
|
|
198
|
-
return colorize("undefined", "null");
|
|
207
|
+
return colorize("undefined", "null", path);
|
|
199
208
|
if (typeof value === "string") {
|
|
200
|
-
return colorize(JSON.stringify(value), "string");
|
|
209
|
+
return colorize(JSON.stringify(value), "string", path);
|
|
201
210
|
}
|
|
202
211
|
if (typeof value === "number") {
|
|
203
|
-
return colorize(String(value), "number");
|
|
212
|
+
return colorize(String(value), "number", path);
|
|
204
213
|
}
|
|
205
214
|
if (typeof value === "boolean") {
|
|
206
|
-
return colorize(String(value), "boolean");
|
|
215
|
+
return colorize(String(value), "boolean", path);
|
|
207
216
|
}
|
|
208
217
|
if (typeof value === "function") {
|
|
209
218
|
return value.name ? `[Function: ${value.name}]` : "[Function]";
|
|
@@ -212,12 +221,12 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
212
221
|
return value.toString();
|
|
213
222
|
}
|
|
214
223
|
if (typeof value === "bigint") {
|
|
215
|
-
return colorize(`${value}n`, "number");
|
|
224
|
+
return colorize(`${value}n`, "number", path);
|
|
216
225
|
}
|
|
217
226
|
// Check for circular references (objects and arrays)
|
|
218
227
|
if (typeof value === "object" && value !== null) {
|
|
219
228
|
if (visited.has(value)) {
|
|
220
|
-
return colorize("[Circular]", "null");
|
|
229
|
+
return colorize("[Circular]", "null", path);
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
// Mark this object/array as visited
|
|
@@ -228,7 +237,7 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
228
237
|
if (value.length === 0) {
|
|
229
238
|
return "[]";
|
|
230
239
|
}
|
|
231
|
-
const elements = value.map((item) => prettyPrintToDoc(item, tagger, visited));
|
|
240
|
+
const elements = value.map((item, i) => prettyPrintToDoc(item, tagger, options, visited, [...path, i]));
|
|
232
241
|
// Use commas when inline, line breaks when multi-line
|
|
233
242
|
const withSeparators = [];
|
|
234
243
|
for (let i = 0; i < elements.length; i++) {
|
|
@@ -243,29 +252,29 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
243
252
|
if (typeof value === "object") {
|
|
244
253
|
// Handle special objects
|
|
245
254
|
if (value instanceof Date) {
|
|
246
|
-
const keyword = colorize("new", "keyword");
|
|
247
|
-
const ctor = colorize("Date", "keyword");
|
|
255
|
+
const keyword = colorize("new", "keyword", [...path, 0]);
|
|
256
|
+
const ctor = colorize("Date", "keyword", [...path, 1]);
|
|
248
257
|
const str = tagger
|
|
249
|
-
? colorize(`"${value.toISOString()}"`, "string")
|
|
258
|
+
? colorize(`"${value.toISOString()}"`, "string", [...path, 2])
|
|
250
259
|
: `"${value.toISOString()}"`;
|
|
251
260
|
return [keyword, " ", ctor, "(", str, ")"];
|
|
252
261
|
}
|
|
253
262
|
if (value instanceof RegExp) {
|
|
254
|
-
return colorize(value.toString(), "string");
|
|
263
|
+
return colorize(value.toString(), "string", path);
|
|
255
264
|
}
|
|
256
265
|
if (value instanceof Map) {
|
|
257
266
|
if (value.size === 0) {
|
|
258
|
-
const keyword = colorize("new", "keyword");
|
|
259
|
-
const ctor = colorize("Map", "keyword");
|
|
267
|
+
const keyword = colorize("new", "keyword", [...path, 0]);
|
|
268
|
+
const ctor = colorize("Map", "keyword", [...path, 1]);
|
|
260
269
|
return [keyword, " ", ctor, "()"];
|
|
261
270
|
}
|
|
262
|
-
const keyword = colorize("new", "keyword");
|
|
263
|
-
const ctor = colorize("Map", "keyword");
|
|
264
|
-
const entries = Array.from(value.entries()).map(([k, v]) => [
|
|
271
|
+
const keyword = colorize("new", "keyword", [...path, 0]);
|
|
272
|
+
const ctor = colorize("Map", "keyword", [...path, 1]);
|
|
273
|
+
const entries = Array.from(value.entries()).map(([k, v], i) => [
|
|
265
274
|
"[",
|
|
266
|
-
prettyPrintToDoc(k, tagger, visited),
|
|
275
|
+
prettyPrintToDoc(k, tagger, options, visited, [...path, 2, i, 0]),
|
|
267
276
|
", ",
|
|
268
|
-
prettyPrintToDoc(v, tagger, visited),
|
|
277
|
+
prettyPrintToDoc(v, tagger, options, visited, [...path, 2, i, 1]),
|
|
269
278
|
"]",
|
|
270
279
|
]);
|
|
271
280
|
const withSeparators = [];
|
|
@@ -287,13 +296,13 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
287
296
|
}
|
|
288
297
|
if (value instanceof Set) {
|
|
289
298
|
if (value.size === 0) {
|
|
290
|
-
const keyword = colorize("new", "keyword");
|
|
291
|
-
const ctor = colorize("Set", "keyword");
|
|
299
|
+
const keyword = colorize("new", "keyword", [...path, 0]);
|
|
300
|
+
const ctor = colorize("Set", "keyword", [...path, 1]);
|
|
292
301
|
return [keyword, " ", ctor, "()"];
|
|
293
302
|
}
|
|
294
|
-
const keyword = colorize("new", "keyword");
|
|
295
|
-
const ctor = colorize("Set", "keyword");
|
|
296
|
-
const items = Array.from(value).map((v) => prettyPrintToDoc(v, tagger, visited));
|
|
303
|
+
const keyword = colorize("new", "keyword", [...path, 0]);
|
|
304
|
+
const ctor = colorize("Set", "keyword", [...path, 1]);
|
|
305
|
+
const items = Array.from(value).map((v, i) => prettyPrintToDoc(v, tagger, options, visited, [...path, 2, i]));
|
|
297
306
|
const withSeparators = [];
|
|
298
307
|
for (let i = 0; i < items.length; i++) {
|
|
299
308
|
withSeparators.push(items[i]);
|
|
@@ -317,17 +326,23 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
317
326
|
return "{}";
|
|
318
327
|
}
|
|
319
328
|
// Check if object has "type" and/or "id" fields
|
|
320
|
-
const typeEntry =
|
|
329
|
+
const typeEntry = niceType
|
|
330
|
+
? entries.find(([key]) => key === "type")
|
|
331
|
+
: undefined;
|
|
321
332
|
const typeValue = typeEntry?.[1];
|
|
322
|
-
const idEntry = entries.find(([key]) => key === "id");
|
|
333
|
+
const idEntry = niceId ? entries.find(([key]) => key === "id") : undefined;
|
|
323
334
|
const idValue = idEntry?.[1];
|
|
324
|
-
const remainingEntries = entries.filter(([key]) => key !== "type" && key !== "id");
|
|
325
|
-
const props = remainingEntries.map(([key, val]) => {
|
|
335
|
+
const remainingEntries = entries.filter(([key]) => (key !== "type" || !niceType) && (key !== "id" || !niceId));
|
|
336
|
+
const props = remainingEntries.map(([key, val], i) => {
|
|
326
337
|
const keyStr = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
|
|
327
338
|
? key
|
|
328
339
|
: JSON.stringify(key);
|
|
329
|
-
const coloredKey = colorize(keyStr, "key");
|
|
330
|
-
return [
|
|
340
|
+
const coloredKey = colorize(keyStr, "key", [...path, 2, i, 0]);
|
|
341
|
+
return [
|
|
342
|
+
coloredKey,
|
|
343
|
+
": ",
|
|
344
|
+
prettyPrintToDoc(val, tagger, options, visited, [...path, 2, i, 1]),
|
|
345
|
+
];
|
|
331
346
|
});
|
|
332
347
|
// Use commas when inline, line breaks when multi-line
|
|
333
348
|
const withSeparators = [];
|
|
@@ -337,13 +352,12 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
337
352
|
withSeparators.push(ifBreak(line, ", "));
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
|
-
// Build the prefix: "type#id" or "type" or "#id"
|
|
341
355
|
const prefix = [];
|
|
342
356
|
if (typeValue && typeof typeValue === "string") {
|
|
343
|
-
prefix.push(colorize(typeValue, "type"));
|
|
357
|
+
prefix.push(colorize(typeValue, "type", [...path, 0]));
|
|
344
358
|
}
|
|
345
359
|
if (idValue !== undefined) {
|
|
346
|
-
prefix.push(colorize("#" + String(idValue), "id"));
|
|
360
|
+
prefix.push(colorize("#" + String(idValue), "id", [...path, 1]));
|
|
347
361
|
}
|
|
348
362
|
return group([
|
|
349
363
|
"{",
|
|
@@ -356,17 +370,12 @@ function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
|
356
370
|
}
|
|
357
371
|
return "[Unknown]";
|
|
358
372
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* @param value The value to pretty-print
|
|
362
|
-
* @param printWidth Maximum line width (default: 80)
|
|
363
|
-
* @param useColor Whether to include colors (default: true)
|
|
364
|
-
*/
|
|
365
|
-
export function prettyPrintToString(value, printWidth = 80, useColor = true) {
|
|
373
|
+
export function prettyPrintToString(value, options = {}) {
|
|
374
|
+
const { width = 80, useColor = true } = options;
|
|
366
375
|
const tagger = useColor ? new StringTagger() : null;
|
|
367
|
-
const doc = prettyPrintToDoc(value, tagger);
|
|
376
|
+
const doc = prettyPrintToDoc(value, tagger, options);
|
|
368
377
|
const formatted = prettier.doc.printer.printDocToString(doc, {
|
|
369
|
-
printWidth,
|
|
378
|
+
printWidth: width,
|
|
370
379
|
tabWidth: 2,
|
|
371
380
|
useTabs: false,
|
|
372
381
|
}).formatted;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretty-print.test.d.ts","sourceRoot":"","sources":["../src/pretty-print.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { prettyPrintToString } from "./pretty-print.js";
|
|
4
|
+
describe("prettyPrintToString", () => {
|
|
5
|
+
it("should format with and without ANSI codes", () => {
|
|
6
|
+
const longArray = Array.from({ length: 20 }, (_, i) => i);
|
|
7
|
+
const withoutAnsi = prettyPrintToString(longArray, { width: 200, useColor: false });
|
|
8
|
+
const withAnsi = prettyPrintToString(longArray, { width: 200, useColor: true });
|
|
9
|
+
// Count ANSI escape sequences
|
|
10
|
+
const ansiMatches = withAnsi.match(/\x1b\[\d+m/g);
|
|
11
|
+
expect(ansiMatches).toBeTruthy();
|
|
12
|
+
expect(withAnsi.length).toBeGreaterThan(withoutAnsi.length);
|
|
13
|
+
});
|
|
14
|
+
it("should format long arrays inline with wide printWidth", () => {
|
|
15
|
+
const longArray = Array.from({ length: 20 }, (_, i) => i);
|
|
16
|
+
const result = prettyPrintToString(longArray, { width: 200, useColor: false });
|
|
17
|
+
// With width 200, this should be all on one line
|
|
18
|
+
expect(result).not.toContain("\n");
|
|
19
|
+
});
|
|
20
|
+
it("should format long arrays with line breaks when narrow", () => {
|
|
21
|
+
const longArray = Array.from({ length: 20 }, (_, i) => i);
|
|
22
|
+
const result = prettyPrintToString(longArray, { width: 40, useColor: false });
|
|
23
|
+
// With width 40, this should break across multiple lines
|
|
24
|
+
expect(result).toContain("\n");
|
|
25
|
+
});
|
|
26
|
+
it("should format objects with wide printWidth", () => {
|
|
27
|
+
const obj = {
|
|
28
|
+
a: 1,
|
|
29
|
+
b: 2,
|
|
30
|
+
c: 3,
|
|
31
|
+
d: 4,
|
|
32
|
+
e: 5,
|
|
33
|
+
f: 6,
|
|
34
|
+
g: 7,
|
|
35
|
+
h: 8,
|
|
36
|
+
};
|
|
37
|
+
const result = prettyPrintToString(obj, { width: 200, useColor: false });
|
|
38
|
+
// Should be relatively compact
|
|
39
|
+
expect(result.split("\n").length).toBeLessThan(10);
|
|
40
|
+
});
|
|
41
|
+
it("should format nested structures", () => {
|
|
42
|
+
const nested = {
|
|
43
|
+
users: [
|
|
44
|
+
{ id: 1, name: "Alice", age: 30 },
|
|
45
|
+
{ id: 2, name: "Bob", age: 25 },
|
|
46
|
+
{ id: 3, name: "Charlie", age: 35 },
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
const wide = prettyPrintToString(nested, { width: 200, useColor: false });
|
|
50
|
+
const narrow = prettyPrintToString(nested, { width: 40, useColor: false });
|
|
51
|
+
// Wide version should have fewer line breaks
|
|
52
|
+
expect(wide.split("\n").length).toBeLessThan(narrow.split("\n").length);
|
|
53
|
+
});
|
|
54
|
+
it("should handle very long arrays", () => {
|
|
55
|
+
const veryLongArray = Array.from({ length: 50 }, (_, i) => i + 1);
|
|
56
|
+
const wide = prettyPrintToString(veryLongArray, { width: 300, useColor: false });
|
|
57
|
+
const narrow = prettyPrintToString(veryLongArray, { width: 60, useColor: false });
|
|
58
|
+
// Both should contain all elements
|
|
59
|
+
expect(wide).toContain("49");
|
|
60
|
+
expect(narrow).toContain("49");
|
|
61
|
+
// Narrow should have more line breaks
|
|
62
|
+
expect(narrow.split("\n").length).toBeGreaterThan(wide.split("\n").length);
|
|
63
|
+
});
|
|
64
|
+
it("should detect circular references in objects", () => {
|
|
65
|
+
const obj = { a: 1, b: 2 };
|
|
66
|
+
obj.self = obj;
|
|
67
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false });
|
|
68
|
+
expect(result).toBe("{a: 1, b: 2, self: [Circular]}");
|
|
69
|
+
});
|
|
70
|
+
it("should detect circular references in arrays", () => {
|
|
71
|
+
const arr = [1, 2, 3];
|
|
72
|
+
arr.push(arr);
|
|
73
|
+
const result = prettyPrintToString(arr, { width: 80, useColor: false });
|
|
74
|
+
expect(result).toBe("[1, 2, 3, [Circular]]");
|
|
75
|
+
});
|
|
76
|
+
it("should detect circular references in nested structures", () => {
|
|
77
|
+
const parent = { name: "parent", children: [] };
|
|
78
|
+
const child = { name: "child", parent: parent };
|
|
79
|
+
parent.children.push(child);
|
|
80
|
+
const result = prettyPrintToString(parent, { width: 80, useColor: false });
|
|
81
|
+
expect(result).toBe('{name: "parent", children: [{name: "child", parent: [Circular]}]}');
|
|
82
|
+
});
|
|
83
|
+
it("should print repeated references distinctly", () => {
|
|
84
|
+
const shared = { value: 42 };
|
|
85
|
+
const obj = { first: shared, second: shared };
|
|
86
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false });
|
|
87
|
+
expect(result).toBe("{first: {value: 42}, second: {value: 42}}");
|
|
88
|
+
});
|
|
89
|
+
it("should print JSX elements", () => {
|
|
90
|
+
const element = React.createElement("div", { className: "foo" }, "hello");
|
|
91
|
+
const result = prettyPrintToString(element, { width: 80, useColor: false });
|
|
92
|
+
expect(result).toBe('<div className="foo">hello</div>');
|
|
93
|
+
});
|
|
94
|
+
it("should print JSX elements with no children", () => {
|
|
95
|
+
const element = React.createElement("br", {});
|
|
96
|
+
const result = prettyPrintToString(element, { width: 80, useColor: false });
|
|
97
|
+
expect(result).toBe("<br />");
|
|
98
|
+
});
|
|
99
|
+
it("should print JSX elements with props and no children", () => {
|
|
100
|
+
const element = React.createElement("img", {
|
|
101
|
+
src: "test.png",
|
|
102
|
+
alt: "test",
|
|
103
|
+
});
|
|
104
|
+
const result = prettyPrintToString(element, { width: 80, useColor: false });
|
|
105
|
+
expect(result).toBe('<img src="test.png" alt="test" />');
|
|
106
|
+
});
|
|
107
|
+
it("should print nested JSX elements", () => {
|
|
108
|
+
const element = React.createElement("div", {}, React.createElement("span", {}, "hello"), React.createElement("span", {}, "world"));
|
|
109
|
+
const result = prettyPrintToString(element, { width: 80, useColor: false });
|
|
110
|
+
expect(result).toBe("<div><span>hello</span><span>world</span></div>");
|
|
111
|
+
});
|
|
112
|
+
it("should break props onto new lines when narrow", () => {
|
|
113
|
+
const element = React.createElement("img", {
|
|
114
|
+
src: "test.png",
|
|
115
|
+
alt: "description",
|
|
116
|
+
width: 100,
|
|
117
|
+
height: 200,
|
|
118
|
+
});
|
|
119
|
+
const result = prettyPrintToString(element, { width: 30, useColor: false });
|
|
120
|
+
expect(result).toContain("\n");
|
|
121
|
+
});
|
|
122
|
+
it("should extract type field as prefix by default", () => {
|
|
123
|
+
const obj = { type: "person", name: "Alice" };
|
|
124
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false });
|
|
125
|
+
expect(result).toBe('{person name: "Alice"}');
|
|
126
|
+
});
|
|
127
|
+
it("should keep type as normal property when niceType is false", () => {
|
|
128
|
+
const obj = { type: "person", name: "Alice" };
|
|
129
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false, niceType: false });
|
|
130
|
+
expect(result).toBe('{type: "person", name: "Alice"}');
|
|
131
|
+
});
|
|
132
|
+
it("should extract id field as prefix by default", () => {
|
|
133
|
+
const obj = { id: 42, name: "Alice" };
|
|
134
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false });
|
|
135
|
+
expect(result).toBe('{#42 name: "Alice"}');
|
|
136
|
+
});
|
|
137
|
+
it("should keep id as normal property when niceId is false", () => {
|
|
138
|
+
const obj = { id: 42, name: "Alice" };
|
|
139
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false, niceId: false });
|
|
140
|
+
expect(result).toBe('{id: 42, name: "Alice"}');
|
|
141
|
+
});
|
|
142
|
+
it("should extract both type and id as prefixes by default", () => {
|
|
143
|
+
const obj = { type: "user", id: 1, name: "Alice" };
|
|
144
|
+
const result = prettyPrintToString(obj, { width: 80, useColor: false });
|
|
145
|
+
expect(result).toBe('{user#1 name: "Alice"}');
|
|
146
|
+
});
|
|
147
|
+
it("should keep both type and id as normal properties when disabled", () => {
|
|
148
|
+
const obj = { type: "user", id: 1, name: "Alice" };
|
|
149
|
+
const result = prettyPrintToString(obj, {
|
|
150
|
+
width: 80,
|
|
151
|
+
useColor: false,
|
|
152
|
+
niceType: false,
|
|
153
|
+
niceId: false,
|
|
154
|
+
});
|
|
155
|
+
expect(result).toBe('{type: "user", id: 1, name: "Alice"}');
|
|
156
|
+
});
|
|
157
|
+
it("should keep opening tag together when it fits", () => {
|
|
158
|
+
const element = React.createElement("g", { "data-path": "/" }, React.createElement("circle", { cx: 0, cy: 0 }));
|
|
159
|
+
const result = prettyPrintToString(element, { width: 20, useColor: false });
|
|
160
|
+
// The opening tag should stay together if it fits
|
|
161
|
+
expect(result).toBe(`<g data-path="/">
|
|
162
|
+
<circle
|
|
163
|
+
cx={0}
|
|
164
|
+
cy={0}
|
|
165
|
+
/>
|
|
166
|
+
</g>`);
|
|
167
|
+
});
|
|
168
|
+
});
|
package/dist/string-tagger.d.ts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
2
|
+
* A string tagging system that supports arbitrary Unicode characters.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
4
|
+
* This system:
|
|
5
|
+
* 1. Replaces the first/last characters of tagged spans with fixed PUA markers
|
|
6
|
+
* 2. Records the original characters and associated before/after strings separately
|
|
7
|
+
* 3. Uses a caller-provided path (number[]) to determine the left-to-right order of spans
|
|
8
8
|
* 4. Can expand the tagged string back to the original with tags applied
|
|
9
|
+
*
|
|
10
|
+
* The path-based approach means callers can tag() in any order — expand() sorts
|
|
11
|
+
* spans by path to match them with markers left-to-right in the string.
|
|
9
12
|
*/
|
|
13
|
+
export type TagPath = number[];
|
|
10
14
|
export declare class StringTagger {
|
|
11
15
|
private spans;
|
|
12
|
-
private nextMarkerIndex;
|
|
13
16
|
/**
|
|
14
|
-
* Tags a substring by replacing the first and last characters
|
|
15
|
-
* with unique PUA markers.
|
|
17
|
+
* Tags a substring by replacing the first and last characters with PUA markers.
|
|
16
18
|
*
|
|
17
19
|
* @param text The text to tag (can be any Unicode)
|
|
18
20
|
* @param before String to insert before the span when expanded
|
|
19
21
|
* @param after String to insert after the span when expanded
|
|
22
|
+
* @param path A number[] that determines this span's left-to-right position.
|
|
23
|
+
* Paths are compared element-wise — spans with earlier paths are matched
|
|
24
|
+
* to earlier (leftward) markers in the string.
|
|
20
25
|
* @returns A string with same length, but first and last chars are PUA markers
|
|
21
26
|
*/
|
|
22
|
-
tag(text: string, before: string, after: string): string;
|
|
27
|
+
tag(text: string, before: string, after: string, path: TagPath): string;
|
|
23
28
|
/**
|
|
24
29
|
* Expands a tagged string back to the original text with before/after strings applied.
|
|
25
30
|
*
|
|
31
|
+
* Spans are matched to markers by sorting paths element-wise — the span with
|
|
32
|
+
* the earliest path is matched to the leftmost START_MARKER in the string.
|
|
33
|
+
*
|
|
26
34
|
* @param encoded The string containing PUA markers
|
|
27
35
|
* @returns The fully expanded string with before/after strings wrapped around tagged spans
|
|
28
36
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"string-tagger.d.ts","sourceRoot":"","sources":["../src/string-tagger.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"string-tagger.d.ts","sourceRoot":"","sources":["../src/string-tagger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC;AAiB/B,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAkB;IAE/B;;;;;;;;;;OAUG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAsBvE;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAkEhC"}
|
package/dist/string-tagger.js
CHANGED
|
@@ -1,64 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
2
|
+
* A string tagging system that supports arbitrary Unicode characters.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
4
|
+
* This system:
|
|
5
|
+
* 1. Replaces the first/last characters of tagged spans with fixed PUA markers
|
|
6
|
+
* 2. Records the original characters and associated before/after strings separately
|
|
7
|
+
* 3. Uses a caller-provided path (number[]) to determine the left-to-right order of spans
|
|
8
8
|
* 4. Can expand the tagged string back to the original with tags applied
|
|
9
|
+
*
|
|
10
|
+
* The path-based approach means callers can tag() in any order — expand() sorts
|
|
11
|
+
* spans by path to match them with markers left-to-right in the string.
|
|
9
12
|
*/
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Two fixed PUA codepoints — that's all we need
|
|
14
|
+
const START_MARKER = 0xe000;
|
|
15
|
+
const END_MARKER = 0xe001;
|
|
16
|
+
function comparePaths(a, b) {
|
|
17
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
18
|
+
if (a[i] !== b[i])
|
|
19
|
+
return a[i] - b[i];
|
|
20
|
+
}
|
|
21
|
+
return a.length - b.length;
|
|
22
|
+
}
|
|
15
23
|
export class StringTagger {
|
|
16
24
|
constructor() {
|
|
17
|
-
this.spans =
|
|
18
|
-
this.nextMarkerIndex = 0;
|
|
25
|
+
this.spans = [];
|
|
19
26
|
}
|
|
20
27
|
/**
|
|
21
|
-
* Tags a substring by replacing the first and last characters
|
|
22
|
-
* with unique PUA markers.
|
|
28
|
+
* Tags a substring by replacing the first and last characters with PUA markers.
|
|
23
29
|
*
|
|
24
30
|
* @param text The text to tag (can be any Unicode)
|
|
25
31
|
* @param before String to insert before the span when expanded
|
|
26
32
|
* @param after String to insert after the span when expanded
|
|
33
|
+
* @param path A number[] that determines this span's left-to-right position.
|
|
34
|
+
* Paths are compared element-wise — spans with earlier paths are matched
|
|
35
|
+
* to earlier (leftward) markers in the string.
|
|
27
36
|
* @returns A string with same length, but first and last chars are PUA markers
|
|
28
37
|
*/
|
|
29
|
-
tag(text, before, after) {
|
|
38
|
+
tag(text, before, after, path) {
|
|
30
39
|
if (text.length === 0) {
|
|
31
40
|
return text;
|
|
32
41
|
}
|
|
33
|
-
if (this.nextMarkerIndex >= MAX_MARKERS) {
|
|
34
|
-
throw new Error(`Exceeded maximum number of tagged spans (${MAX_MARKERS})`);
|
|
35
|
-
}
|
|
36
|
-
// Get the next PUA codepoint for the start marker
|
|
37
|
-
const startMarker = PUA_START + this.nextMarkerIndex;
|
|
38
|
-
this.nextMarkerIndex++;
|
|
39
|
-
// Record the original characters and before/after strings
|
|
40
42
|
const originalStart = text[0];
|
|
41
43
|
const originalEnd = text.length === 1 ? null : text[text.length - 1];
|
|
42
|
-
this.spans.
|
|
43
|
-
startMarker,
|
|
44
|
-
originalStart,
|
|
45
|
-
originalEnd,
|
|
46
|
-
before,
|
|
47
|
-
after,
|
|
48
|
-
});
|
|
49
|
-
// Build the result: marker + middle + marker
|
|
44
|
+
this.spans.push({ path, originalStart, originalEnd, before, after });
|
|
50
45
|
if (originalEnd === null) {
|
|
51
|
-
|
|
52
|
-
return String.fromCharCode(startMarker);
|
|
46
|
+
return String.fromCharCode(START_MARKER);
|
|
53
47
|
}
|
|
54
48
|
const middle = text.slice(1, -1);
|
|
55
|
-
return (String.fromCharCode(
|
|
49
|
+
return (String.fromCharCode(START_MARKER) +
|
|
56
50
|
middle +
|
|
57
51
|
String.fromCharCode(END_MARKER));
|
|
58
52
|
}
|
|
59
53
|
/**
|
|
60
54
|
* Expands a tagged string back to the original text with before/after strings applied.
|
|
61
55
|
*
|
|
56
|
+
* Spans are matched to markers by sorting paths element-wise — the span with
|
|
57
|
+
* the earliest path is matched to the leftmost START_MARKER in the string.
|
|
58
|
+
*
|
|
62
59
|
* @param encoded The string containing PUA markers
|
|
63
60
|
* @returns The fully expanded string with before/after strings wrapped around tagged spans
|
|
64
61
|
*/
|
|
@@ -66,45 +63,41 @@ export class StringTagger {
|
|
|
66
63
|
if (encoded.length === 0) {
|
|
67
64
|
return encoded;
|
|
68
65
|
}
|
|
69
|
-
//
|
|
70
|
-
const
|
|
66
|
+
// Sort spans by path — this determines left-to-right marker assignment
|
|
67
|
+
const sortedSpans = [...this.spans].sort((a, b) => comparePaths(a.path, b.path));
|
|
68
|
+
let spanIndex = 0;
|
|
71
69
|
const parts = [];
|
|
72
70
|
let lastIndex = 0;
|
|
73
|
-
let
|
|
74
|
-
while ((match = puaRegex.exec(encoded)) !== null) {
|
|
75
|
-
const i = match.index;
|
|
71
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
76
72
|
const charCode = encoded.charCodeAt(i);
|
|
77
|
-
|
|
78
|
-
const spanInfo = this.spans.get(charCode);
|
|
79
|
-
if (spanInfo) {
|
|
73
|
+
if (charCode === START_MARKER) {
|
|
80
74
|
// Copy any regular text before this marker
|
|
81
75
|
if (i > lastIndex) {
|
|
82
76
|
parts.push(encoded.slice(lastIndex, i));
|
|
83
77
|
}
|
|
84
|
-
|
|
78
|
+
const spanInfo = sortedSpans[spanIndex++];
|
|
79
|
+
if (!spanInfo) {
|
|
80
|
+
throw new Error("More start markers in string than registered spans");
|
|
81
|
+
}
|
|
85
82
|
if (spanInfo.originalEnd === null) {
|
|
86
|
-
// Single-character span
|
|
83
|
+
// Single-character span
|
|
87
84
|
parts.push(spanInfo.before, spanInfo.originalStart, spanInfo.after);
|
|
88
85
|
lastIndex = i + 1;
|
|
89
86
|
}
|
|
90
87
|
else {
|
|
91
|
-
// Multi-character span
|
|
88
|
+
// Multi-character span — find the end marker
|
|
92
89
|
const endIndex = encoded.indexOf(String.fromCharCode(END_MARKER), i + 1);
|
|
93
90
|
if (endIndex === -1) {
|
|
94
91
|
throw new Error("Tagged span missing end marker");
|
|
95
92
|
}
|
|
96
|
-
// Extract the middle part (between start marker and end marker)
|
|
97
93
|
const middle = encoded.slice(i + 1, endIndex);
|
|
98
|
-
// Build the expanded span
|
|
99
94
|
parts.push(spanInfo.before, spanInfo.originalStart, middle, spanInfo.originalEnd, spanInfo.after);
|
|
100
95
|
lastIndex = endIndex + 1;
|
|
101
|
-
|
|
102
|
-
puaRegex.lastIndex = lastIndex;
|
|
96
|
+
i = endIndex; // skip past end marker
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
|
-
else {
|
|
106
|
-
|
|
107
|
-
throw new Error(`Unrecognized PUA marker at index ${i}`);
|
|
99
|
+
else if (charCode === END_MARKER) {
|
|
100
|
+
throw new Error(`Unexpected end marker at index ${i}`);
|
|
108
101
|
}
|
|
109
102
|
}
|
|
110
103
|
// Copy any remaining regular text
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string-tagger.test.d.ts","sourceRoot":"","sources":["../src/string-tagger.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { StringTagger } from "./string-tagger.js";
|
|
3
|
+
describe("StringTagger", () => {
|
|
4
|
+
describe("tag", () => {
|
|
5
|
+
it("should preserve string length for multi-char strings", () => {
|
|
6
|
+
const tagger = new StringTagger();
|
|
7
|
+
const result = tagger.tag("hello", "<b>", "</b>", [0]);
|
|
8
|
+
expect(result.length).toBe("hello".length);
|
|
9
|
+
});
|
|
10
|
+
it("should preserve string length for single-char strings", () => {
|
|
11
|
+
const tagger = new StringTagger();
|
|
12
|
+
const result = tagger.tag("x", "<b>", "</b>", [0]);
|
|
13
|
+
expect(result.length).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
it("should preserve string length for two-char strings", () => {
|
|
16
|
+
const tagger = new StringTagger();
|
|
17
|
+
const result = tagger.tag("ab", "<b>", "</b>", [0]);
|
|
18
|
+
expect(result.length).toBe(2);
|
|
19
|
+
});
|
|
20
|
+
it("should return empty string for empty input", () => {
|
|
21
|
+
const tagger = new StringTagger();
|
|
22
|
+
const result = tagger.tag("", "<b>", "</b>", [0]);
|
|
23
|
+
expect(result).toBe("");
|
|
24
|
+
});
|
|
25
|
+
it("should preserve middle characters", () => {
|
|
26
|
+
const tagger = new StringTagger();
|
|
27
|
+
const result = tagger.tag("hello", "<b>", "</b>", [0]);
|
|
28
|
+
// middle characters "ell" should be preserved
|
|
29
|
+
expect(result.slice(1, -1)).toBe("ell");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("expand", () => {
|
|
33
|
+
it("should wrap a multi-char string with before/after", () => {
|
|
34
|
+
const tagger = new StringTagger();
|
|
35
|
+
const tagged = tagger.tag("hello", "<b>", "</b>", [0]);
|
|
36
|
+
const expanded = tagger.expand(tagged);
|
|
37
|
+
expect(expanded).toBe("<b>hello</b>");
|
|
38
|
+
});
|
|
39
|
+
it("should wrap a single-char string with before/after", () => {
|
|
40
|
+
const tagger = new StringTagger();
|
|
41
|
+
const tagged = tagger.tag("x", "[", "]", [0]);
|
|
42
|
+
const expanded = tagger.expand(tagged);
|
|
43
|
+
expect(expanded).toBe("[x]");
|
|
44
|
+
});
|
|
45
|
+
it("should wrap a two-char string with before/after", () => {
|
|
46
|
+
const tagger = new StringTagger();
|
|
47
|
+
const tagged = tagger.tag("ab", "(", ")", [0]);
|
|
48
|
+
const expanded = tagger.expand(tagged);
|
|
49
|
+
expect(expanded).toBe("(ab)");
|
|
50
|
+
});
|
|
51
|
+
it("should handle empty before/after strings", () => {
|
|
52
|
+
const tagger = new StringTagger();
|
|
53
|
+
const tagged = tagger.tag("hello", "", "", [0]);
|
|
54
|
+
const expanded = tagger.expand(tagged);
|
|
55
|
+
expect(expanded).toBe("hello");
|
|
56
|
+
});
|
|
57
|
+
it("should return empty string for empty input", () => {
|
|
58
|
+
const tagger = new StringTagger();
|
|
59
|
+
const expanded = tagger.expand("");
|
|
60
|
+
expect(expanded).toBe("");
|
|
61
|
+
});
|
|
62
|
+
it("should handle multiple tagged spans in sequence", () => {
|
|
63
|
+
const tagger = new StringTagger();
|
|
64
|
+
const a = tagger.tag("hello", "<a>", "</a>", [0]);
|
|
65
|
+
const b = tagger.tag("world", "<b>", "</b>", [1]);
|
|
66
|
+
const expanded = tagger.expand(a + " " + b);
|
|
67
|
+
expect(expanded).toBe("<a>hello</a> <b>world</b>");
|
|
68
|
+
});
|
|
69
|
+
it("should handle many tagged spans", () => {
|
|
70
|
+
const tagger = new StringTagger();
|
|
71
|
+
const parts = [];
|
|
72
|
+
for (let i = 0; i < 100; i++) {
|
|
73
|
+
parts.push(tagger.tag(`w${i}`, `<${i}>`, `</${i}>`, [i]));
|
|
74
|
+
}
|
|
75
|
+
const expanded = tagger.expand(parts.join(","));
|
|
76
|
+
for (let i = 0; i < 100; i++) {
|
|
77
|
+
expect(expanded).toContain(`<${i}>w${i}</${i}>`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
it("should handle tagged spans mixed with plain text", () => {
|
|
81
|
+
const tagger = new StringTagger();
|
|
82
|
+
const tagged = tagger.tag("bold", "<b>", "</b>", [0]);
|
|
83
|
+
const expanded = tagger.expand("plain " + tagged + " also plain");
|
|
84
|
+
expect(expanded).toBe("plain <b>bold</b> also plain");
|
|
85
|
+
});
|
|
86
|
+
it("should handle adjacent tagged spans with no gap", () => {
|
|
87
|
+
const tagger = new StringTagger();
|
|
88
|
+
const a = tagger.tag("red", "<r>", "</r>", [0]);
|
|
89
|
+
const b = tagger.tag("blue", "<b>", "</b>", [1]);
|
|
90
|
+
const expanded = tagger.expand(a + b);
|
|
91
|
+
expect(expanded).toBe("<r>red</r><b>blue</b>");
|
|
92
|
+
});
|
|
93
|
+
it("should handle adjacent single-char tagged spans", () => {
|
|
94
|
+
const tagger = new StringTagger();
|
|
95
|
+
const a = tagger.tag("x", "(", ")", [0]);
|
|
96
|
+
const b = tagger.tag("y", "[", "]", [1]);
|
|
97
|
+
const expanded = tagger.expand(a + b);
|
|
98
|
+
expect(expanded).toBe("(x)[y]");
|
|
99
|
+
});
|
|
100
|
+
it("should handle unicode content", () => {
|
|
101
|
+
const tagger = new StringTagger();
|
|
102
|
+
const tagged = tagger.tag("café", "<i>", "</i>", [0]);
|
|
103
|
+
const expanded = tagger.expand(tagged);
|
|
104
|
+
expect(expanded).toBe("<i>café</i>");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("tag + expand roundtrip with ANSI codes", () => {
|
|
108
|
+
const ANSI_GREEN = "\x1b[32m";
|
|
109
|
+
const ANSI_RESET = "\x1b[0m";
|
|
110
|
+
it("should produce correct ANSI-wrapped output", () => {
|
|
111
|
+
const tagger = new StringTagger();
|
|
112
|
+
const tagged = tagger.tag('"hello"', ANSI_GREEN, ANSI_RESET, [0]);
|
|
113
|
+
const expanded = tagger.expand(tagged);
|
|
114
|
+
expect(expanded).toBe(`${ANSI_GREEN}"hello"${ANSI_RESET}`);
|
|
115
|
+
});
|
|
116
|
+
it("should not affect line width measurement", () => {
|
|
117
|
+
const tagger = new StringTagger();
|
|
118
|
+
const tagged = tagger.tag("hello", ANSI_GREEN, ANSI_RESET, [0]);
|
|
119
|
+
expect(tagged.length).toBe("hello".length);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("path-based ordering", () => {
|
|
123
|
+
it("should expand correctly when tagged out of order (paths determine order)", () => {
|
|
124
|
+
const tagger = new StringTagger();
|
|
125
|
+
// Tag in order a, b, c — but paths say the string order is c, a, b
|
|
126
|
+
const a = tagger.tag("first", "<a>", "</a>", [1]);
|
|
127
|
+
const b = tagger.tag("second", "<b>", "</b>", [2]);
|
|
128
|
+
const c = tagger.tag("third", "<c>", "</c>", [0]);
|
|
129
|
+
// Assemble in string order matching paths: c ([0]), a ([1]), b ([2])
|
|
130
|
+
const expanded = tagger.expand(c + " " + a + " " + b);
|
|
131
|
+
expect(expanded).toBe("<c>third</c> <a>first</a> <b>second</b>");
|
|
132
|
+
});
|
|
133
|
+
it("should expand correctly when single-char tags are out of order", () => {
|
|
134
|
+
const tagger = new StringTagger();
|
|
135
|
+
const a = tagger.tag("x", "(", ")", [1]);
|
|
136
|
+
const b = tagger.tag("y", "[", "]", [0]);
|
|
137
|
+
// String order: b first, a second (matching paths [0], [1])
|
|
138
|
+
const expanded = tagger.expand(b + a);
|
|
139
|
+
expect(expanded).toBe("[y](x)");
|
|
140
|
+
});
|
|
141
|
+
it("should sort hierarchical paths correctly", () => {
|
|
142
|
+
const tagger = new StringTagger();
|
|
143
|
+
// Simulate a nested structure: key0: val0, key1: val1
|
|
144
|
+
const k0 = tagger.tag("key0", "<k>", "</k>", [0, 0]);
|
|
145
|
+
const v0 = tagger.tag("val0", "<v>", "</v>", [0, 1]);
|
|
146
|
+
const k1 = tagger.tag("key1", "<k>", "</k>", [1, 0]);
|
|
147
|
+
const v1 = tagger.tag("val1", "<v>", "</v>", [1, 1]);
|
|
148
|
+
const expanded = tagger.expand(k0 + ": " + v0 + ", " + k1 + ": " + v1);
|
|
149
|
+
expect(expanded).toBe("<k>key0</k>: <v>val0</v>, <k>key1</k>: <v>val1</v>");
|
|
150
|
+
});
|
|
151
|
+
it("should handle hierarchical paths tagged in arbitrary order", () => {
|
|
152
|
+
const tagger = new StringTagger();
|
|
153
|
+
// Tag in reverse order
|
|
154
|
+
const v1 = tagger.tag("val1", "<v>", "</v>", [1, 1]);
|
|
155
|
+
const k1 = tagger.tag("key1", "<k>", "</k>", [1, 0]);
|
|
156
|
+
const v0 = tagger.tag("val0", "<v>", "</v>", [0, 1]);
|
|
157
|
+
const k0 = tagger.tag("key0", "<k>", "</k>", [0, 0]);
|
|
158
|
+
// Assemble in correct string order
|
|
159
|
+
const expanded = tagger.expand(k0 + ": " + v0 + ", " + k1 + ": " + v1);
|
|
160
|
+
expect(expanded).toBe("<k>key0</k>: <v>val0</v>, <k>key1</k>: <v>val1</v>");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe("interaction with string operations", () => {
|
|
164
|
+
it("should survive concatenation and still expand correctly", () => {
|
|
165
|
+
const tagger = new StringTagger();
|
|
166
|
+
const a = tagger.tag("key", "<k>", "</k>", [0]);
|
|
167
|
+
const b = tagger.tag('"value"', "<v>", "</v>", [1]);
|
|
168
|
+
const line = "{ " + a + ": " + b + " }";
|
|
169
|
+
const expanded = tagger.expand(line);
|
|
170
|
+
expect(expanded).toBe('{ <k>key</k>: <v>"value"</v> }');
|
|
171
|
+
});
|
|
172
|
+
it("should survive being split across lines and rejoined", () => {
|
|
173
|
+
const tagger = new StringTagger();
|
|
174
|
+
const a = tagger.tag("hello", "<a>", "</a>", [0]);
|
|
175
|
+
const b = tagger.tag("world", "<b>", "</b>", [1]);
|
|
176
|
+
const text = a + "\n" + b;
|
|
177
|
+
const expanded = tagger.expand(text);
|
|
178
|
+
expect(expanded).toBe("<a>hello</a>\n<b>world</b>");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joshuahhh/pretty-print",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"prepublishOnly": "npm run build"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"prettier": "^
|
|
22
|
+
"prettier": "^3.8.1"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=17"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/prettier": "^2.7.3",
|
|
29
29
|
"@types/react": "^19.2.4",
|
|
30
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
30
31
|
"react": "^19.2.0",
|
|
31
32
|
"typescript": "^5.2.2",
|
|
32
33
|
"vitest": "^4.0.6"
|