@joshuahhh/pretty-print 0.0.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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/pretty-print.d.ts +55 -0
- package/dist/pretty-print.d.ts.map +1 -0
- package/dist/pretty-print.js +388 -0
- package/dist/string-tagger.d.ts +31 -0
- package/dist/string-tagger.d.ts.map +1 -0
- package/dist/string-tagger.js +116 -0
- package/package.json +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +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;AAE3B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Converts ANSI color codes to browser console %c format with CSS styles.
|
|
4
|
+
*/
|
|
5
|
+
export declare function prettyPrintForBrowser(value: unknown, printWidth?: number): [string, ...string[]];
|
|
6
|
+
/**
|
|
7
|
+
* Pretty-print a value to the browser console with colors.
|
|
8
|
+
* This is the easiest way to use the pretty-printer in browser code.
|
|
9
|
+
*/
|
|
10
|
+
export declare function prettyLog(value: unknown, { label, width }?: {
|
|
11
|
+
label?: string;
|
|
12
|
+
width?: number;
|
|
13
|
+
}): void;
|
|
14
|
+
/**
|
|
15
|
+
* React component that pretty-prints a value and automatically adjusts
|
|
16
|
+
* to the width of its container.
|
|
17
|
+
*/
|
|
18
|
+
export declare function PrettyPrint({ value, style, className, }: {
|
|
19
|
+
value: unknown;
|
|
20
|
+
style?: React.CSSProperties;
|
|
21
|
+
className?: string;
|
|
22
|
+
}): 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;
|
|
30
|
+
export declare const testData: {
|
|
31
|
+
primitives: {
|
|
32
|
+
number: number;
|
|
33
|
+
string: string;
|
|
34
|
+
bool: boolean;
|
|
35
|
+
nil: null;
|
|
36
|
+
};
|
|
37
|
+
arrays: number[];
|
|
38
|
+
longArray: number[];
|
|
39
|
+
object: {
|
|
40
|
+
a: number;
|
|
41
|
+
b: number;
|
|
42
|
+
};
|
|
43
|
+
typePrefix: {
|
|
44
|
+
type: string;
|
|
45
|
+
first: string;
|
|
46
|
+
last: string;
|
|
47
|
+
};
|
|
48
|
+
nested: {
|
|
49
|
+
users: {
|
|
50
|
+
id: number;
|
|
51
|
+
name: string;
|
|
52
|
+
}[];
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=pretty-print.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretty-print.d.ts","sourceRoot":"","sources":["../src/pretty-print.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAgC1B;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,UAAU,GAAE,MAAW,GACtB,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,CA2BvB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,OAAO,EACd,EAAE,KAAK,EAAE,KAAW,EAAE,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9D,IAAI,CAQN;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,KAAK,EACL,SAAS,GACV,EAAE;IACD,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,2CAsFA;AAuSD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,UAAU,GAAE,MAAW,EACvB,QAAQ,GAAE,OAAc,GACvB,MAAM,CAUR;AAGD,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;CAYpB,CAAC"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as prettier from "prettier";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { StringTagger } from "./string-tagger.js";
|
|
5
|
+
const { group, indent, line, softline, ifBreak } = prettier.doc.builders;
|
|
6
|
+
// Single source of truth for all color information
|
|
7
|
+
const COLORS = [
|
|
8
|
+
{ name: "string", ansi: "\x1b[32m", hex: "#22c55e" }, // green
|
|
9
|
+
{ name: "number", ansi: "\x1b[33m", hex: "#eab308" }, // yellow
|
|
10
|
+
{ name: "boolean", ansi: "\x1b[35m", hex: "#a855f7" }, // magenta
|
|
11
|
+
{ name: "null", ansi: "\x1b[90m", hex: "#6b7280" }, // gray
|
|
12
|
+
{ name: "key", ansi: "\x1b[36m", hex: "#06b6d4" }, // cyan
|
|
13
|
+
{ name: "keyword", ansi: "\x1b[34m", hex: "#3b82f6" }, // blue
|
|
14
|
+
{ name: "type", ansi: "\x1b[95m", hex: "#ec4899" }, // bright magenta
|
|
15
|
+
{ name: "id", ansi: "\x1b[90m", hex: "#9ca3af" }, // faded gray
|
|
16
|
+
];
|
|
17
|
+
const ANSI_RESET = "\x1b[0m";
|
|
18
|
+
const COLOR_NAME_TO_ANSI = Object.fromEntries(COLORS.map((c) => [c.name, c.ansi]));
|
|
19
|
+
// Derived: ANSI code -> hex color lookup
|
|
20
|
+
const ANSI_TO_HEX = Object.fromEntries(COLORS.map((c) => [c.ansi.match(/\d+/)?.[0] || "", c.hex]));
|
|
21
|
+
/**
|
|
22
|
+
* Converts ANSI color codes to browser console %c format with CSS styles.
|
|
23
|
+
*/
|
|
24
|
+
export function prettyPrintForBrowser(value, printWidth = 80) {
|
|
25
|
+
const textWithAnsi = prettyPrintToString(value, printWidth, true);
|
|
26
|
+
const ansiRegex = /\x1b\[(\d+)m/g;
|
|
27
|
+
const parts = [];
|
|
28
|
+
const styles = [];
|
|
29
|
+
let lastIndex = 0;
|
|
30
|
+
let currentStyle = "";
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = ansiRegex.exec(textWithAnsi)) !== null) {
|
|
33
|
+
const textBefore = textWithAnsi.slice(lastIndex, match.index);
|
|
34
|
+
if (textBefore) {
|
|
35
|
+
parts.push("%c" + textBefore);
|
|
36
|
+
styles.push(currentStyle);
|
|
37
|
+
}
|
|
38
|
+
const hexColor = ANSI_TO_HEX[match[1]];
|
|
39
|
+
currentStyle = hexColor ? `color: ${hexColor}` : "";
|
|
40
|
+
lastIndex = match.index + match[0].length;
|
|
41
|
+
}
|
|
42
|
+
const remaining = textWithAnsi.slice(lastIndex);
|
|
43
|
+
if (remaining) {
|
|
44
|
+
parts.push("%c" + remaining);
|
|
45
|
+
styles.push(currentStyle);
|
|
46
|
+
}
|
|
47
|
+
return [parts.join(""), ...styles];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pretty-print a value to the browser console with colors.
|
|
51
|
+
* This is the easiest way to use the pretty-printer in browser code.
|
|
52
|
+
*/
|
|
53
|
+
export function prettyLog(value, { label, width = 120 } = {}) {
|
|
54
|
+
if (label) {
|
|
55
|
+
console.group(label);
|
|
56
|
+
}
|
|
57
|
+
console.log(...prettyPrintForBrowser(value, width));
|
|
58
|
+
if (label) {
|
|
59
|
+
console.groupEnd();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* React component that pretty-prints a value and automatically adjusts
|
|
64
|
+
* to the width of its container.
|
|
65
|
+
*/
|
|
66
|
+
export function PrettyPrint({ value, style, className, }) {
|
|
67
|
+
const containerRef = React.useRef(null);
|
|
68
|
+
const measureRef = React.useRef(null);
|
|
69
|
+
const [printWidth, setPrintWidth] = React.useState(80);
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
if (!containerRef.current || !measureRef.current)
|
|
72
|
+
return;
|
|
73
|
+
const updateWidth = () => {
|
|
74
|
+
if (!containerRef.current || !measureRef.current)
|
|
75
|
+
return;
|
|
76
|
+
// Measure actual character width by measuring a sample string
|
|
77
|
+
const sampleWidth = measureRef.current.offsetWidth;
|
|
78
|
+
const sampleLength = 100; // We measure a 100-character string
|
|
79
|
+
const charWidth = sampleWidth / sampleLength;
|
|
80
|
+
const containerWidth = containerRef.current.offsetWidth;
|
|
81
|
+
const charsPerLine = Math.floor(containerWidth / charWidth);
|
|
82
|
+
setPrintWidth(charsPerLine); // minimum 40 chars
|
|
83
|
+
};
|
|
84
|
+
// Initial measurement (with slight delay to ensure fonts are loaded)
|
|
85
|
+
setTimeout(updateWidth, 0);
|
|
86
|
+
// Watch for container size changes
|
|
87
|
+
const resizeObserver = new ResizeObserver(updateWidth);
|
|
88
|
+
resizeObserver.observe(containerRef.current);
|
|
89
|
+
return () => resizeObserver.disconnect();
|
|
90
|
+
}, []);
|
|
91
|
+
// Convert ANSI color codes to React elements with inline styles
|
|
92
|
+
const textWithAnsi = prettyPrintToString(value, printWidth, true);
|
|
93
|
+
const ansiRegex = /\x1b\[(\d+)m/g;
|
|
94
|
+
const elements = [];
|
|
95
|
+
let lastIndex = 0;
|
|
96
|
+
let currentColor = "";
|
|
97
|
+
let match;
|
|
98
|
+
let key = 0;
|
|
99
|
+
while ((match = ansiRegex.exec(textWithAnsi)) !== null) {
|
|
100
|
+
const textBefore = textWithAnsi.slice(lastIndex, match.index);
|
|
101
|
+
if (textBefore) {
|
|
102
|
+
elements.push(_jsx("span", { style: { color: currentColor || "inherit" }, children: textBefore }, key++));
|
|
103
|
+
}
|
|
104
|
+
currentColor = ANSI_TO_HEX[match[1]] || "";
|
|
105
|
+
lastIndex = match.index + match[0].length;
|
|
106
|
+
}
|
|
107
|
+
const remaining = textWithAnsi.slice(lastIndex);
|
|
108
|
+
if (remaining) {
|
|
109
|
+
elements.push(_jsx("span", { style: { color: currentColor || "inherit" }, children: remaining }, key++));
|
|
110
|
+
}
|
|
111
|
+
return (_jsxs("div", { ref: containerRef, style: { width: "100%", ...style, overflowX: "auto" }, className: className, children: [_jsx("span", { ref: measureRef, style: {
|
|
112
|
+
position: "absolute",
|
|
113
|
+
visibility: "hidden",
|
|
114
|
+
fontFamily: "monospace",
|
|
115
|
+
whiteSpace: "pre",
|
|
116
|
+
}, children: "0123456789".repeat(10) }), _jsx("pre", { style: { margin: 0, fontFamily: "monospace", maxWidth: "100%" }, children: elements })] }));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pretty-print a JavaScript value using Prettier's doc builder API.
|
|
120
|
+
* Returns a Doc that can be printed with prettier.printDocToString()
|
|
121
|
+
*/
|
|
122
|
+
function prettyPrintToDoc(value, tagger, visited = new Set()) {
|
|
123
|
+
// Helper to colorize text if tagger is available
|
|
124
|
+
const colorize = (text, colorType) => {
|
|
125
|
+
if (!tagger)
|
|
126
|
+
return text;
|
|
127
|
+
const ansi = COLOR_NAME_TO_ANSI[colorType];
|
|
128
|
+
return tagger.tag(text, ansi, ANSI_RESET);
|
|
129
|
+
};
|
|
130
|
+
// Handle JSX elements
|
|
131
|
+
if (true) {
|
|
132
|
+
// Change to false to disable JSX printing
|
|
133
|
+
if (React.isValidElement(value)) {
|
|
134
|
+
const element = value;
|
|
135
|
+
const type = typeof element.type === "string"
|
|
136
|
+
? element.type
|
|
137
|
+
: element.type.name || "Component";
|
|
138
|
+
const props = element.props;
|
|
139
|
+
const { children, ...otherProps } = props;
|
|
140
|
+
const openTag = colorize(`<${type}`, "keyword");
|
|
141
|
+
const closeTag = colorize(`</${type}>`, "keyword");
|
|
142
|
+
const selfCloseTag = colorize("/>", "keyword");
|
|
143
|
+
// Format props
|
|
144
|
+
const propEntries = Object.entries(otherProps);
|
|
145
|
+
const propDocs = [];
|
|
146
|
+
for (let i = 0; i < propEntries.length; i++) {
|
|
147
|
+
const [key, val] = propEntries[i];
|
|
148
|
+
const keyStr = colorize(key, "key");
|
|
149
|
+
const valDoc = typeof val === "string"
|
|
150
|
+
? colorize(JSON.stringify(val), "string")
|
|
151
|
+
: ["{", prettyPrintToDoc(val, tagger, visited), "}"];
|
|
152
|
+
propDocs.push(ifBreak(line, " "), keyStr, "=", valDoc);
|
|
153
|
+
}
|
|
154
|
+
const childrenArray = React.Children.toArray(children);
|
|
155
|
+
const hasChildren = childrenArray.length > 0;
|
|
156
|
+
if (!hasChildren && propEntries.length === 0) {
|
|
157
|
+
return [openTag, " ", selfCloseTag];
|
|
158
|
+
}
|
|
159
|
+
if (!hasChildren) {
|
|
160
|
+
return group([
|
|
161
|
+
openTag,
|
|
162
|
+
indent(propDocs),
|
|
163
|
+
ifBreak(line, " "),
|
|
164
|
+
selfCloseTag,
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
const childDocs = childrenArray.map((child) => typeof child === "string" || typeof child === "number"
|
|
168
|
+
? String(child)
|
|
169
|
+
: prettyPrintToDoc(child, tagger, visited));
|
|
170
|
+
// Add conditional line breaks between children
|
|
171
|
+
// When inline, no separator. When broken, each child on its own line.
|
|
172
|
+
const childDocsWithSeparators = [];
|
|
173
|
+
for (let i = 0; i < childDocs.length; i++) {
|
|
174
|
+
childDocsWithSeparators.push(childDocs[i]);
|
|
175
|
+
if (i < childDocs.length - 1) {
|
|
176
|
+
childDocsWithSeparators.push(ifBreak(line, ""));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
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
|
+
const openingTag = group([
|
|
182
|
+
openTag,
|
|
183
|
+
indent(propDocs),
|
|
184
|
+
colorize(">", "keyword"),
|
|
185
|
+
]);
|
|
186
|
+
return group([
|
|
187
|
+
openingTag,
|
|
188
|
+
indent([softline, ...childDocsWithSeparators]),
|
|
189
|
+
softline,
|
|
190
|
+
closeTag,
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Handle primitives
|
|
195
|
+
if (value === null)
|
|
196
|
+
return colorize("null", "null");
|
|
197
|
+
if (value === undefined)
|
|
198
|
+
return colorize("undefined", "null");
|
|
199
|
+
if (typeof value === "string") {
|
|
200
|
+
return colorize(JSON.stringify(value), "string");
|
|
201
|
+
}
|
|
202
|
+
if (typeof value === "number") {
|
|
203
|
+
return colorize(String(value), "number");
|
|
204
|
+
}
|
|
205
|
+
if (typeof value === "boolean") {
|
|
206
|
+
return colorize(String(value), "boolean");
|
|
207
|
+
}
|
|
208
|
+
if (typeof value === "function") {
|
|
209
|
+
return value.name ? `[Function: ${value.name}]` : "[Function]";
|
|
210
|
+
}
|
|
211
|
+
if (typeof value === "symbol") {
|
|
212
|
+
return value.toString();
|
|
213
|
+
}
|
|
214
|
+
if (typeof value === "bigint") {
|
|
215
|
+
return colorize(`${value}n`, "number");
|
|
216
|
+
}
|
|
217
|
+
// Check for circular references (objects and arrays)
|
|
218
|
+
if (typeof value === "object" && value !== null) {
|
|
219
|
+
if (visited.has(value)) {
|
|
220
|
+
return colorize("[Circular]", "null");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Mark this object/array as visited
|
|
224
|
+
visited = new Set(visited);
|
|
225
|
+
visited.add(value);
|
|
226
|
+
// Handle arrays
|
|
227
|
+
if (Array.isArray(value)) {
|
|
228
|
+
if (value.length === 0) {
|
|
229
|
+
return "[]";
|
|
230
|
+
}
|
|
231
|
+
const elements = value.map((item) => prettyPrintToDoc(item, tagger, visited));
|
|
232
|
+
// Use commas when inline, line breaks when multi-line
|
|
233
|
+
const withSeparators = [];
|
|
234
|
+
for (let i = 0; i < elements.length; i++) {
|
|
235
|
+
withSeparators.push(elements[i]);
|
|
236
|
+
if (i < elements.length - 1) {
|
|
237
|
+
withSeparators.push(ifBreak(line, ", "));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return group(["[", indent([softline, ...withSeparators]), softline, "]"]);
|
|
241
|
+
}
|
|
242
|
+
// Handle objects
|
|
243
|
+
if (typeof value === "object") {
|
|
244
|
+
// Handle special objects
|
|
245
|
+
if (value instanceof Date) {
|
|
246
|
+
const keyword = colorize("new", "keyword");
|
|
247
|
+
const ctor = colorize("Date", "keyword");
|
|
248
|
+
const str = tagger
|
|
249
|
+
? colorize(`"${value.toISOString()}"`, "string")
|
|
250
|
+
: `"${value.toISOString()}"`;
|
|
251
|
+
return [keyword, " ", ctor, "(", str, ")"];
|
|
252
|
+
}
|
|
253
|
+
if (value instanceof RegExp) {
|
|
254
|
+
return colorize(value.toString(), "string");
|
|
255
|
+
}
|
|
256
|
+
if (value instanceof Map) {
|
|
257
|
+
if (value.size === 0) {
|
|
258
|
+
const keyword = colorize("new", "keyword");
|
|
259
|
+
const ctor = colorize("Map", "keyword");
|
|
260
|
+
return [keyword, " ", ctor, "()"];
|
|
261
|
+
}
|
|
262
|
+
const keyword = colorize("new", "keyword");
|
|
263
|
+
const ctor = colorize("Map", "keyword");
|
|
264
|
+
const entries = Array.from(value.entries()).map(([k, v]) => [
|
|
265
|
+
"[",
|
|
266
|
+
prettyPrintToDoc(k, tagger, visited),
|
|
267
|
+
", ",
|
|
268
|
+
prettyPrintToDoc(v, tagger, visited),
|
|
269
|
+
"]",
|
|
270
|
+
]);
|
|
271
|
+
const withSeparators = [];
|
|
272
|
+
for (let i = 0; i < entries.length; i++) {
|
|
273
|
+
withSeparators.push(entries[i]);
|
|
274
|
+
if (i < entries.length - 1) {
|
|
275
|
+
withSeparators.push(ifBreak(line, ", "));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return group([
|
|
279
|
+
keyword,
|
|
280
|
+
" ",
|
|
281
|
+
ctor,
|
|
282
|
+
"([",
|
|
283
|
+
indent([softline, ...withSeparators]),
|
|
284
|
+
softline,
|
|
285
|
+
"])",
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
if (value instanceof Set) {
|
|
289
|
+
if (value.size === 0) {
|
|
290
|
+
const keyword = colorize("new", "keyword");
|
|
291
|
+
const ctor = colorize("Set", "keyword");
|
|
292
|
+
return [keyword, " ", ctor, "()"];
|
|
293
|
+
}
|
|
294
|
+
const keyword = colorize("new", "keyword");
|
|
295
|
+
const ctor = colorize("Set", "keyword");
|
|
296
|
+
const items = Array.from(value).map((v) => prettyPrintToDoc(v, tagger, visited));
|
|
297
|
+
const withSeparators = [];
|
|
298
|
+
for (let i = 0; i < items.length; i++) {
|
|
299
|
+
withSeparators.push(items[i]);
|
|
300
|
+
if (i < items.length - 1) {
|
|
301
|
+
withSeparators.push(ifBreak(line, ", "));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return group([
|
|
305
|
+
keyword,
|
|
306
|
+
" ",
|
|
307
|
+
ctor,
|
|
308
|
+
"([",
|
|
309
|
+
indent([softline, ...withSeparators]),
|
|
310
|
+
softline,
|
|
311
|
+
"])",
|
|
312
|
+
]);
|
|
313
|
+
}
|
|
314
|
+
// Handle plain objects
|
|
315
|
+
const entries = Object.entries(value);
|
|
316
|
+
if (entries.length === 0) {
|
|
317
|
+
return "{}";
|
|
318
|
+
}
|
|
319
|
+
// Check if object has "type" and/or "id" fields
|
|
320
|
+
const typeEntry = entries.find(([key]) => key === "type");
|
|
321
|
+
const typeValue = typeEntry?.[1];
|
|
322
|
+
const idEntry = entries.find(([key]) => key === "id");
|
|
323
|
+
const idValue = idEntry?.[1];
|
|
324
|
+
const remainingEntries = entries.filter(([key]) => key !== "type" && key !== "id");
|
|
325
|
+
const props = remainingEntries.map(([key, val]) => {
|
|
326
|
+
const keyStr = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)
|
|
327
|
+
? key
|
|
328
|
+
: JSON.stringify(key);
|
|
329
|
+
const coloredKey = colorize(keyStr, "key");
|
|
330
|
+
return [coloredKey, ": ", prettyPrintToDoc(val, tagger, visited)];
|
|
331
|
+
});
|
|
332
|
+
// Use commas when inline, line breaks when multi-line
|
|
333
|
+
const withSeparators = [];
|
|
334
|
+
for (let i = 0; i < props.length; i++) {
|
|
335
|
+
withSeparators.push(props[i]);
|
|
336
|
+
if (i < props.length - 1) {
|
|
337
|
+
withSeparators.push(ifBreak(line, ", "));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Build the prefix: "type#id" or "type" or "#id"
|
|
341
|
+
const prefix = [];
|
|
342
|
+
if (typeValue && typeof typeValue === "string") {
|
|
343
|
+
prefix.push(colorize(typeValue, "type"));
|
|
344
|
+
}
|
|
345
|
+
if (idValue !== undefined) {
|
|
346
|
+
prefix.push(colorize("#" + String(idValue), "id"));
|
|
347
|
+
}
|
|
348
|
+
return group([
|
|
349
|
+
"{",
|
|
350
|
+
prefix.length > 0 ? prefix : "",
|
|
351
|
+
prefix.length > 0 ? " " : "",
|
|
352
|
+
remainingEntries.length > 0 ? indent([softline, ...withSeparators]) : "",
|
|
353
|
+
remainingEntries.length > 0 ? softline : "",
|
|
354
|
+
"}",
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
357
|
+
return "[Unknown]";
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Pretty-print a JavaScript value to a string.
|
|
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) {
|
|
366
|
+
const tagger = useColor ? new StringTagger() : null;
|
|
367
|
+
const doc = prettyPrintToDoc(value, tagger);
|
|
368
|
+
const formatted = prettier.doc.printer.printDocToString(doc, {
|
|
369
|
+
printWidth,
|
|
370
|
+
tabWidth: 2,
|
|
371
|
+
useTabs: false,
|
|
372
|
+
}).formatted;
|
|
373
|
+
return tagger ? tagger.expand(formatted) : formatted;
|
|
374
|
+
}
|
|
375
|
+
// Shared test data
|
|
376
|
+
export const testData = {
|
|
377
|
+
primitives: { number: 42, string: "hello", bool: true, nil: null },
|
|
378
|
+
arrays: [1, 2, 3],
|
|
379
|
+
longArray: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
380
|
+
object: { a: 1, b: 2 },
|
|
381
|
+
typePrefix: { type: "person", first: "Albert", last: "Einstein" },
|
|
382
|
+
nested: {
|
|
383
|
+
users: [
|
|
384
|
+
{ id: 1, name: "Alice" },
|
|
385
|
+
{ id: 2, name: "Bob" },
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A more general string tagging system that supports arbitrary Unicode characters.
|
|
3
|
+
*
|
|
4
|
+
* Instead of encoding color information directly into characters, this system:
|
|
5
|
+
* 1. Assigns unique PUA (Private Use Area) codepoints to mark span starts
|
|
6
|
+
* 2. Uses a single universal PUA codepoint to mark span ends
|
|
7
|
+
* 3. Records the original characters and associated before/after strings separately
|
|
8
|
+
* 4. Can expand the tagged string back to the original with tags applied
|
|
9
|
+
*/
|
|
10
|
+
export declare class StringTagger {
|
|
11
|
+
private spans;
|
|
12
|
+
private nextMarkerIndex;
|
|
13
|
+
/**
|
|
14
|
+
* Tags a substring by replacing the first and last characters
|
|
15
|
+
* with unique PUA markers.
|
|
16
|
+
*
|
|
17
|
+
* @param text The text to tag (can be any Unicode)
|
|
18
|
+
* @param before String to insert before the span when expanded
|
|
19
|
+
* @param after String to insert after the span when expanded
|
|
20
|
+
* @returns A string with same length, but first and last chars are PUA markers
|
|
21
|
+
*/
|
|
22
|
+
tag(text: string, before: string, after: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Expands a tagged string back to the original text with before/after strings applied.
|
|
25
|
+
*
|
|
26
|
+
* @param encoded The string containing PUA markers
|
|
27
|
+
* @returns The fully expanded string with before/after strings wrapped around tagged spans
|
|
28
|
+
*/
|
|
29
|
+
expand(encoded: string): string;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=string-tagger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string-tagger.d.ts","sourceRoot":"","sources":["../src/string-tagger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgBH,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,eAAe,CAAK;IAE5B;;;;;;;;OAQG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAyCxD;;;;;OAKG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;CAsEhC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A more general string tagging system that supports arbitrary Unicode characters.
|
|
3
|
+
*
|
|
4
|
+
* Instead of encoding color information directly into characters, this system:
|
|
5
|
+
* 1. Assigns unique PUA (Private Use Area) codepoints to mark span starts
|
|
6
|
+
* 2. Uses a single universal PUA codepoint to mark span ends
|
|
7
|
+
* 3. Records the original characters and associated before/after strings separately
|
|
8
|
+
* 4. Can expand the tagged string back to the original with tags applied
|
|
9
|
+
*/
|
|
10
|
+
// Use Unicode private use area for markers
|
|
11
|
+
const PUA_START = 0xe000;
|
|
12
|
+
const PUA_END = 0xf8ff; // End of BMP private use area
|
|
13
|
+
const END_MARKER = PUA_END; // Universal end marker
|
|
14
|
+
const MAX_MARKERS = PUA_END - PUA_START; // One less because we reserve END_MARKER
|
|
15
|
+
export class StringTagger {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.spans = new Map();
|
|
18
|
+
this.nextMarkerIndex = 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Tags a substring by replacing the first and last characters
|
|
22
|
+
* with unique PUA markers.
|
|
23
|
+
*
|
|
24
|
+
* @param text The text to tag (can be any Unicode)
|
|
25
|
+
* @param before String to insert before the span when expanded
|
|
26
|
+
* @param after String to insert after the span when expanded
|
|
27
|
+
* @returns A string with same length, but first and last chars are PUA markers
|
|
28
|
+
*/
|
|
29
|
+
tag(text, before, after) {
|
|
30
|
+
if (text.length === 0) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
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
|
+
const originalStart = text[0];
|
|
41
|
+
const originalEnd = text.length === 1 ? null : text[text.length - 1];
|
|
42
|
+
this.spans.set(startMarker, {
|
|
43
|
+
startMarker,
|
|
44
|
+
originalStart,
|
|
45
|
+
originalEnd,
|
|
46
|
+
before,
|
|
47
|
+
after,
|
|
48
|
+
});
|
|
49
|
+
// Build the result: marker + middle + marker
|
|
50
|
+
if (originalEnd === null) {
|
|
51
|
+
// Single character - just the start marker
|
|
52
|
+
return String.fromCharCode(startMarker);
|
|
53
|
+
}
|
|
54
|
+
const middle = text.slice(1, -1);
|
|
55
|
+
return (String.fromCharCode(startMarker) +
|
|
56
|
+
middle +
|
|
57
|
+
String.fromCharCode(END_MARKER));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Expands a tagged string back to the original text with before/after strings applied.
|
|
61
|
+
*
|
|
62
|
+
* @param encoded The string containing PUA markers
|
|
63
|
+
* @returns The fully expanded string with before/after strings wrapped around tagged spans
|
|
64
|
+
*/
|
|
65
|
+
expand(encoded) {
|
|
66
|
+
if (encoded.length === 0) {
|
|
67
|
+
return encoded;
|
|
68
|
+
}
|
|
69
|
+
// Regex to find any PUA character (start markers or end marker)
|
|
70
|
+
const puaRegex = /[\ue000-\uf8ff]/g;
|
|
71
|
+
const parts = [];
|
|
72
|
+
let lastIndex = 0;
|
|
73
|
+
let match;
|
|
74
|
+
while ((match = puaRegex.exec(encoded)) !== null) {
|
|
75
|
+
const i = match.index;
|
|
76
|
+
const charCode = encoded.charCodeAt(i);
|
|
77
|
+
// Check if this is a start marker
|
|
78
|
+
const spanInfo = this.spans.get(charCode);
|
|
79
|
+
if (spanInfo) {
|
|
80
|
+
// Copy any regular text before this marker
|
|
81
|
+
if (i > lastIndex) {
|
|
82
|
+
parts.push(encoded.slice(lastIndex, i));
|
|
83
|
+
}
|
|
84
|
+
// Found a tagged span
|
|
85
|
+
if (spanInfo.originalEnd === null) {
|
|
86
|
+
// Single-character span - no end marker to look for
|
|
87
|
+
parts.push(spanInfo.before, spanInfo.originalStart, spanInfo.after);
|
|
88
|
+
lastIndex = i + 1;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Multi-character span - find the end marker
|
|
92
|
+
const endIndex = encoded.indexOf(String.fromCharCode(END_MARKER), i + 1);
|
|
93
|
+
if (endIndex === -1) {
|
|
94
|
+
throw new Error("Tagged span missing end marker");
|
|
95
|
+
}
|
|
96
|
+
// Extract the middle part (between start marker and end marker)
|
|
97
|
+
const middle = encoded.slice(i + 1, endIndex);
|
|
98
|
+
// Build the expanded span
|
|
99
|
+
parts.push(spanInfo.before, spanInfo.originalStart, middle, spanInfo.originalEnd, spanInfo.after);
|
|
100
|
+
lastIndex = endIndex + 1;
|
|
101
|
+
// Advance the regex past the end marker
|
|
102
|
+
puaRegex.lastIndex = lastIndex;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Unrecognized PUA character - this is an error
|
|
107
|
+
throw new Error(`Unrecognized PUA marker at index ${i}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Copy any remaining regular text
|
|
111
|
+
if (lastIndex < encoded.length) {
|
|
112
|
+
parts.push(encoded.slice(lastIndex));
|
|
113
|
+
}
|
|
114
|
+
return parts.join("");
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joshuahhh/pretty-print",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"prettier": "^2.8.8"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": ">=17"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/prettier": "^2.7.3",
|
|
29
|
+
"@types/react": "^19.2.4",
|
|
30
|
+
"react": "^19.2.0",
|
|
31
|
+
"typescript": "^5.2.2",
|
|
32
|
+
"vitest": "^4.0.6"
|
|
33
|
+
}
|
|
34
|
+
}
|