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

hexojs / hexo-util / 16650823172

31 Jul 2025 01:47PM UTC coverage: 96.989% (+0.1%) from 96.875%
16650823172

Pull #430

github

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

490 of 511 branches covered (95.89%)

167 of 170 new or added lines in 2 files covered. (98.24%)

1965 of 2026 relevant lines covered (96.99%)

70.0 hits per line

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

98.82
/lib/json_stringify_circular.ts
1
// (c) 2020 Andrea Giammarchi
1✔
2
// Source: https://github.com/WebReflection/flatted/blob/main/cjs/index.js
1✔
3

1✔
4
const { parse: $parse, stringify: $stringify } = JSON;
1✔
5
const { keys } = Object;
1✔
6
const isObject = (v: unknown) => typeof v === 'object' && v !== null;
1✔
7
const ignore = {};
1✔
8
const noop = (_: unknown, v: unknown) => v;
1✔
9

1✔
10
/**
1✔
11
 * Recursively revives circular references in a parsed object.
1✔
12
 *
1✔
13
 * @param input - The array of parsed objects.
1✔
14
 * @param parsed - A set of already parsed objects to avoid infinite recursion.
1✔
15
 * @param output - The current output object being revived.
1✔
16
 * @param $ - The reviver function to apply to each key/value pair.
1✔
17
 * @returns The revived object with circular references restored.
1✔
18
 */
1✔
19
const revive = (
1✔
20
  input: unknown[],
2,012✔
21
  parsed: Set<unknown>,
2,012✔
22
  output: Record<string, unknown>,
2,012✔
23
  $: (key: string, value: unknown) => unknown
2,012✔
24
): unknown => {
2,012✔
25
  const lazy: Array<{
2,012✔
26
    k: string;
2,012✔
27
    a: [unknown[], Set<unknown>, Record<string, unknown>, (key: string, value: unknown) => unknown];
2,012✔
28
  }> = [];
2,012✔
29
  for (const k of keys(output)) {
2,012✔
30
    const value = output[k];
5,026✔
31
    if (typeof value === 'string' && /^\d+$/.test(value)) {
5,026✔
32
      const tmp = input[Number(value)];
3,007✔
33
      if (isObject(tmp) && !parsed.has(tmp)) {
3,007✔
34
        parsed.add(tmp);
2,006✔
35
        output[k] = ignore;
2,006✔
36
        lazy.push({ k, a: [input, parsed, tmp as Record<string, unknown>, $] });
2,006✔
37
      } else output[k] = $.call(output, k, tmp);
3,007✔
38
    } else if (output[k] !== ignore) output[k] = $.call(output, k, value);
5,026✔
39
  }
5,026✔
40
  for (const { k, a } of lazy) output[k] = $.call(output, k, revive(...a));
2,012✔
41
  return output;
2,012✔
42
};
2,012✔
43

1✔
44
/**
1✔
45
 * Adds a value to a set of known values and returns its index as a string.
1✔
46
 *
1✔
47
 * @param known - A map of known objects to their indices.
1✔
48
 * @param input - The array of input objects.
1✔
49
 * @param value - The value to add.
1✔
50
 * @returns The index of the value as a string.
1✔
51
 */
1✔
52
const set = (known: Map<unknown, string>, input: unknown[], value: unknown): string => {
1✔
53
  const idx = String(input.push(value) - 1);
2,008✔
54
  known.set(value, idx);
2,008✔
55
  return idx;
2,008✔
56
};
2,008✔
57

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

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

6✔
106
  function replace(this: unknown, key: string, value: unknown): unknown {
6✔
107
    if (firstRun) {
7,025✔
108
      firstRun = false;
2,008✔
109
      return value;
2,008✔
110
    }
2,008✔
111
    const after = $.call(this, key, value);
5,017✔
112
    if (isObject(after)) {
7,025✔
113
      if (after === null) return after;
3,007!
114
      return known.get(after) || set(known, input, after);
3,007✔
115
    }
3,007✔
116
    return after;
2,010✔
117
  }
2,010✔
118
};
6✔
119

1✔
120
/**
1✔
121
 * Converts an object with circular references to a JSON-compatible object.
1✔
122
 *
1✔
123
 * @param anyData - The object to convert.
1✔
124
 * @returns The JSON-compatible representation of the object.
1✔
125
 */
1✔
126
const toJSON = (anyData: unknown): unknown => $parse(stringify(anyData));
1✔
127

1✔
128
/**
1✔
129
 * Parses a circular object from a JSON string.
1✔
130
 *
1✔
131
 * @template T
1✔
132
 * @param anyData - The JSON string to parse.
1✔
133
 * @returns The parsed object of type T.
1✔
134
 */
1✔
135
const fromJSON = <T = unknown>(anyData: string): T => parse<T>($stringify(anyData));
1✔
136

1✔
137
/**
1✔
138
 * Transforms any object to a JSON string, suppressing `TypeError: Converting circular structure to JSON`.
1✔
139
 *
1✔
140
 * @param data - The object to stringify.
1✔
141
 * @returns The JSON string representation.
1✔
142
 */
1✔
143
function jsonStringify(data: unknown): string {
6✔
144
  return stringify(data);
6✔
145
}
6✔
146

1✔
147
/**
1✔
148
 * Parses a JSON string that was stringified with circular references.
1✔
149
 *
1✔
150
 * @template T
1✔
151
 * @param data - The JSON string to parse.
1✔
152
 * @returns The parsed object of type T.
1✔
153
 */
1✔
154
function jsonParse<T>(data: string): T {
6✔
155
  return parse(data) as T;
6✔
156
}
6✔
157

1✔
158
export { parse, stringify, toJSON, fromJSON, jsonStringify, jsonParse };
1✔
159

1✔
160
if (typeof module !== 'undefined' && typeof module.exports === 'object' && module.exports !== null) {
1✔
161
  module.exports = {
1✔
162
    parse,
1✔
163
    stringify,
1✔
164
    jsonStringify,
1✔
165
    jsonParse,
1✔
166
    toJSON,
1✔
167
    fromJSON
1✔
168
  };
1✔
169
}
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