@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.
@@ -0,0 +1,3 @@
1
+ export { prettyPrintForBrowser, prettyLog, PrettyPrint, prettyPrintToString, testData, } from "./pretty-print.js";
2
+ export { StringTagger } from "./string-tagger.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export { prettyPrintForBrowser, prettyLog, PrettyPrint, prettyPrintToString, testData, } from "./pretty-print.js";
2
+ export { StringTagger } from "./string-tagger.js";
@@ -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
+ }