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

hexojs / hexo-util / 16568769083

28 Jul 2025 12:15PM UTC coverage: 96.878% (+0.003%) from 96.875%
16568769083

Pull #430

github

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

490 of 510 branches covered (96.08%)

157 of 162 new or added lines in 1 file covered. (96.91%)

1955 of 2018 relevant lines covered (96.88%)

75.24 hits per line

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

96.91
/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
// No longer needed, as we only care about objects for circular refs
1✔
17

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

1✔
49
/**
1✔
50
 * Adds a value to a set of known values and returns its index.
1✔
51
 */
1✔
52
const set = (known: Map<unknown, string>, input: unknown[], value: unknown): string => {
1✔
53
  const index = String(input.push(value) - 1);
2,008✔
54
  known.set(value, index);
2,008✔
55
  return index;
2,008✔
56
};
2,008✔
57

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

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

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

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

1✔
129
/**
1✔
130
 * Parses a circular object from JSON.
1✔
131
 * @param anyData - The JSON string to parse.
1✔
132
 * @returns The parsed object.
1✔
133
 */
1✔
134
const fromJSONBrowser = <T = unknown>(anyData: string): T => parse<T>($stringify(anyData));
1✔
135
export { fromJSONBrowser, parse as parseBrowser, stringify as stringifyBrowser };
1✔
136

1✔
137
/**
1✔
138
 * transform any object to json. Suppress `TypeError: Converting circular structure to JSON`
1✔
139
 * @param data
1✔
140
 * @returns
1✔
141
 */
1✔
142
/**
1✔
143
 * Transforms any object to JSON, suppressing `TypeError: Converting circular structure to JSON` (Browser version)
1✔
144
 * @param data - The object to stringify
1✔
145
 * @returns The JSON string representation
1✔
146
 */
1✔
147
export function jsonStringifyWithCircular(data: unknown): string {
6✔
148
  return stringify(data);
6✔
149
}
6✔
150

1✔
151
export { jsonStringifyWithCircular as jsonStringify };
1✔
152

1✔
153
/**
1✔
154
 * Parses JSON stringified with circular refs (Browser version)
1✔
155
 * @param data - The JSON string to parse
1✔
156
 * @returns The parsed object
1✔
157
 */
1✔
158
export function jsonParseWithCircular<T>(data: string): T {
6✔
159
  return parse(data) as T;
6✔
160
}
6✔
161

1✔
162
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