• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hexojs / hexo-util / 16568845561

28 Jul 2025 12:19PM UTC coverage: 96.85% (-0.03%) from 96.875%
16568845561

Pull #430

github

web-flow
Merge 02f3f3c47 into d497bc760
Pull Request #430: feat(json): add support for circular JSON stringify/parse

490 of 510 branches covered (96.08%)

170 of 176 new or added lines in 2 files covered. (96.59%)

1968 of 2032 relevant lines covered (96.85%)

74.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.14
/lib/json_stringify_circular.ts
1
/* eslint no-fallthrough: ["error", { "commentPattern": "break[\\s\\w]*omitted" }] */
1✔
2

1✔
3

1✔
4
/* ! (c) 2020 Andrea Giammarchi */
1✔
5
/* source https://github.com/WebReflection/flatted/blob/main/cjs/index.js */
1✔
6

1✔
7
const { parse: $parse, stringify: $stringify } = JSON;
1✔
8
const { keys } = Object;
1✔
9

1✔
10
const isObject = (value: unknown) => typeof value === 'object' && value !== null;
1✔
11

1✔
12
const ignore = {};
1✔
13

1✔
14
const noop = (_: unknown, value: unknown) => value;
1✔
15

1✔
16
/**
1✔
17
 * Recursively revives circular references in a parsed object.
1✔
18
 *
1✔
19
 * @param input - The array of parsed objects.
1✔
20
 * @param parsed - A set of already parsed objects to avoid infinite recursion.
1✔
21
 * @param output - The current output object being revived.
1✔
22
 * @param $ - The reviver function to apply to each key/value pair.
1✔
23
 * @returns The revived object with circular references restored.
1✔
24
 */
1✔
25
const revive = (
1✔
26
  input: unknown[],
2,012✔
27
  parsed: Set<unknown>,
2,012✔
28
  output: Record<string, unknown>,
2,012✔
29
  $: (key: string, value: unknown) => unknown
2,012✔
30
): unknown => {
2,012✔
31
  const lazy: Array<{ k: string; a: [unknown[], Set<unknown>, Record<string, unknown>, (key: string, value: unknown) => unknown] }> = [];
2,012✔
32
  for (let ke = keys(output), { length } = ke, y = 0; y < length; y++) {
2,012✔
33
    const k = ke[y];
5,026✔
34
    const value = output[k];
5,026✔
35
    if (typeof value === 'string' && /^\d+$/.test(value)) {
5,026✔
36
      // Only treat as reference if value is a string that is a number (index)
3,007✔
37
      const tmp = input[Number(value)];
3,007✔
38
      if (isObject(tmp) && !parsed.has(tmp)) {
3,007✔
39
        parsed.add(tmp);
2,006✔
40
        output[k] = ignore;
2,006✔
41
        lazy.push({ k, a: [input, parsed, tmp as Record<string, unknown>, $] });
2,006✔
42
      } else output[k] = $.call(output, k, tmp);
3,007✔
43
    } else if (output[k] !== ignore) output[k] = $.call(output, k, value);
5,026✔
44
  }
5,026✔
45
  for (let { length } = lazy, i = 0; i < length; i++) {
2,012✔
46
    const { k, a } = lazy[i];
2,006✔
47
    // eslint-disable-next-line prefer-spread
2,006✔
48
    output[k] = $.call(output, k, revive.apply(null, a));
2,006✔
49
  }
2,006✔
50
  return output;
2,012✔
51
};
2,012✔
52

1✔
53
/**
1✔
54
 * Adds a value to a set of known values and returns its index as a string.
1✔
55
 *
1✔
56
 * @param known - A map of known objects to their indices.
1✔
57
 * @param input - The array of input objects.
1✔
58
 * @param value - The value to add.
1✔
59
 * @returns The index of the value as a string.
1✔
60
 */
1✔
61
const set = (known: Map<unknown, string>, input: unknown[], value: unknown): string => {
1✔
62
  const index = String(input.push(value) - 1);
2,008✔
63
  known.set(value, index);
2,008✔
64
  return index;
2,008✔
65
};
2,008✔
66

1✔
67
/**
1✔
68
 * Parses a JSON string with support for circular references.
1✔
69
 *
1✔
70
 * @template T
1✔
71
 * @param text - The JSON string to parse.
1✔
72
 * @param reviver - Optional function to transform the parsed values.
1✔
73
 * @returns The parsed object of type T.
1✔
74
 */
1✔
75
const parse = <T = unknown>(text: string, reviver?: (this: unknown, key: string, value: unknown) => unknown): T => {
1✔
76
  const input = $parse(text);
6✔
77
  const value = input[0];
6✔
78
  const $ = reviver || noop;
6✔
79
  const tmp = isObject(value) ? revive(input, new Set(), value, $) : value;
6!
80
  return $.call({ '': tmp }, '', tmp) as T;
6✔
81
};
6✔
82

1✔
83
/**
1✔
84
 * Stringifies an object into JSON with support for circular references.
1✔
85
 *
1✔
86
 * @param value - The object to stringify.
1✔
87
 * @param replacer - Optional function or array of strings to transform the values before stringifying.
1✔
88
 * @param space - Optional number or string to use as white space in the output.
1✔
89
 * @returns The JSON string representation of the object.
1✔
90
 */
1✔
91
const stringify = (
1✔
92
  value: unknown,
6✔
93
  replacer?: ((this: unknown, key: string, value: unknown) => unknown) | string[],
6✔
94
  space?: string | number
6✔
95
): string => {
6✔
96
  const isCallable = typeof replacer === 'function';
6✔
97
  let $: (k: string, v: unknown) => unknown;
6✔
98
  if (isCallable) {
6!
NEW
99
    $ = replacer as (k: string, v: unknown) => unknown;
×
100
  } else if (typeof replacer === 'object') {
6!
NEW
101
    $ = (k: string, v: unknown) => {
×
NEW
102
      if (k === '' || (replacer as string[]).indexOf(k) !== -1) return v;
×
NEW
103
      return undefined;
×
NEW
104
    };
×
105
  } else {
6✔
106
    $ = noop;
6✔
107
  }
6✔
108
  const known = new Map<unknown, string>();
6✔
109
  const input: unknown[] = [];
6✔
110
  const output: string[] = [];
6✔
111
  let i = +set(known, input, $.call({ '': value }, '', value));
6✔
112
  let firstRun = !i;
6✔
113
  while (i < input.length) {
6✔
114
    firstRun = true;
2,008✔
115
    output[i] = $stringify(input[i++], replace, space);
2,008✔
116
  }
2,008✔
117
  return '[' + output.join(',') + ']';
6✔
118

6✔
119
  function replace(this: unknown, key: string, value: unknown): unknown {
6✔
120
    if (firstRun) {
7,025✔
121
      firstRun = false;
2,008✔
122
      return value;
2,008✔
123
    }
2,008✔
124
    const after = $.call(this, key, value);
5,017✔
125
    if (isObject(after)) {
7,025✔
126
      if (after === null) return after;
3,007!
127
      return known.get(after) || set(known, input, after);
3,007✔
128
    }
3,007✔
129
    return after;
2,010✔
130
  }
2,010✔
131
};
6✔
132

1✔
133
/**
1✔
134
 * Converts an object with circular references to a JSON-compatible object.
1✔
135
 *
1✔
136
 * @param anyData - The object to convert.
1✔
137
 * @returns The JSON-compatible representation of the object.
1✔
138
 */
1✔
139
const toJSON = (anyData: unknown): unknown => $parse(stringify(anyData));
1✔
140
export { toJSON };
1✔
141

1✔
142
/**
1✔
143
 * Parses a circular object from a JSON string.
1✔
144
 *
1✔
145
 * @template T
1✔
146
 * @param anyData - The JSON string to parse.
1✔
147
 * @returns The parsed object of type T.
1✔
148
 */
1✔
149
const fromJSON = <T = unknown>(anyData: string): T => parse<T>($stringify(anyData));
1✔
150
export { fromJSON, parse, stringify };
1✔
151

1✔
152
/**
1✔
153
 * Transforms any object to a JSON string, suppressing `TypeError: Converting circular structure to JSON`.
1✔
154
 *
1✔
155
 * @param data - The object to stringify.
1✔
156
 * @returns The JSON string representation.
1✔
157
 */
1✔
158
export function jsonStringifyWithCircular(data: unknown): string {
6✔
159
  return stringify(data);
6✔
160
}
6✔
161

1✔
162
export { jsonStringifyWithCircular as jsonStringify };
1✔
163

1✔
164
/**
1✔
165
 * Parses a JSON string that was stringified with circular references (browser version).
1✔
166
 *
1✔
167
 * @template T
1✔
168
 * @param data - The JSON string to parse.
1✔
169
 * @returns The parsed object of type T.
1✔
170
 */
1✔
171
export function jsonParseWithCircular<T>(data: string): T {
6✔
172
  return parse(data) as T;
6✔
173
}
6✔
174

1✔
175
export { jsonParseWithCircular as jsonParse };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc