@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 CHANGED
@@ -1,3 +1,3 @@
1
1
  export { prettyPrintForBrowser, prettyLog, PrettyPrint, prettyPrintToString, testData, } from "./pretty-print.js";
2
- export { StringTagger } from "./string-tagger.js";
2
+ export type { PrettyPrintOptions } from "./pretty-print.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAE3B,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,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
@@ -1,2 +1 @@
1
1
  export { prettyPrintForBrowser, prettyLog, PrettyPrint, prettyPrintToString, testData, } from "./pretty-print.js";
2
- export { StringTagger } from "./string-tagger.js";
@@ -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, printWidth?: number): [string, ...string[]];
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, width }?: {
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;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"}
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"}
@@ -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, printWidth = 80) {
25
- const textWithAnsi = prettyPrintToString(value, printWidth, true);
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, width = 120 } = {}) {
53
+ export function prettyLog(value, { label, ...options } = {}) {
54
54
  if (label) {
55
55
  console.group(label);
56
56
  }
57
- console.log(...prettyPrintForBrowser(value, width));
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 [printWidth, setPrintWidth] = React.useState(80);
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
- setPrintWidth(charsPerLine); // minimum 40 chars
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, printWidth, true);
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
- // Helper to colorize text if tagger is available
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
- : ["{", prettyPrintToDoc(val, tagger, visited), "}"];
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 = entries.find(([key]) => key === "type");
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 [coloredKey, ": ", prettyPrintToDoc(val, tagger, visited)];
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
- * 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) {
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pretty-print.test.d.ts.map
@@ -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
+ });
@@ -1,28 +1,36 @@
1
1
  /**
2
- * A more general string tagging system that supports arbitrary Unicode characters.
2
+ * A string tagging system that supports arbitrary Unicode characters.
3
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
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;;;;;;;;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"}
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"}
@@ -1,64 +1,61 @@
1
1
  /**
2
- * A more general string tagging system that supports arbitrary Unicode characters.
2
+ * A string tagging system that supports arbitrary Unicode characters.
3
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
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
- // 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
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 = new Map();
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.set(startMarker, {
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
- // Single character - just the start marker
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(startMarker) +
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
- // Regex to find any PUA character (start markers or end marker)
70
- const puaRegex = /[\ue000-\uf8ff]/g;
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 match;
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
- // Check if this is a start marker
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
- // Found a tagged span
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 - no end marker to look for
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 - find the end marker
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
- // Advance the regex past the end marker
102
- puaRegex.lastIndex = lastIndex;
96
+ i = endIndex; // skip past end marker
103
97
  }
104
98
  }
105
- else {
106
- // Unrecognized PUA character - this is an error
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=string-tagger.test.d.ts.map
@@ -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.1",
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": "^2.8.8"
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"