boa/builtins/json/mod.rs
1//! This module implements the global `JSON` object.
2//!
3//! The `JSON` object contains methods for parsing [JavaScript Object Notation (JSON)][spec]
4//! and converting values to JSON. It can't be called or constructed, and aside from its
5//! two method properties, it has no interesting functionality of its own.
6//!
7//! More information:
8//! - [ECMAScript reference][spec]
9//! - [MDN documentation][mdn]
10//! - [JSON specification][json]
11//!
12//! [spec]: https://tc39.es/ecma262/#sec-json
13//! [json]: https://www.json.org/json-en.html
14//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON
15
16use crate::{
17 builtins::{
18 string::{is_leading_surrogate, is_trailing_surrogate},
19 BuiltIn,
20 },
21 object::{JsObject, ObjectInitializer, RecursionLimiter},
22 property::{Attribute, PropertyNameKind},
23 symbol::WellKnownSymbols,
24 value::IntegerOrInfinity,
25 BoaProfiler, Context, JsResult, JsString, JsValue,
26};
27use serde_json::{self, Value as JSONValue};
28
29use super::JsArgs;
30
31#[cfg(test)]
32mod tests;
33
34/// JavaScript `JSON` global object.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub(crate) struct Json;
37
38impl BuiltIn for Json {
39 const NAME: &'static str = "JSON";
40
41 fn attribute() -> Attribute {
42 Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
43 }
44
45 fn init(context: &mut Context) -> (&'static str, JsValue, Attribute) {
46 let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
47
48 let to_string_tag = WellKnownSymbols::to_string_tag();
49 let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE;
50
51 let json_object = ObjectInitializer::new(context)
52 .function(Self::parse, "parse", 2)
53 .function(Self::stringify, "stringify", 3)
54 .property(to_string_tag, Self::NAME, attribute)
55 .build();
56
57 (Self::NAME, json_object.into(), Self::attribute())
58 }
59}
60
61impl Json {
62 /// `JSON.parse( text[, reviver] )`
63 ///
64 /// This `JSON` method parses a JSON string, constructing the JavaScript value or object described by the string.
65 ///
66 /// An optional `reviver` function can be provided to perform a transformation on the resulting object before it is returned.
67 ///
68 /// More information:
69 /// - [ECMAScript reference][spec]
70 /// - [MDN documentation][mdn]
71 ///
72 /// [spec]: https://tc39.es/ecma262/#sec-json.parse
73 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
74 pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
75 // 1. Let jsonString be ? ToString(text).
76 let json_string = args
77 .get(0)
78 .cloned()
79 .unwrap_or_default()
80 .to_string(context)?;
81
82 // 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404.
83 // Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification.
84 if let Err(e) = serde_json::from_str::<JSONValue>(&json_string) {
85 return context.throw_syntax_error(e.to_string());
86 }
87
88 // 3. Let scriptString be the string-concatenation of "(", jsonString, and ");".
89 let script_string = JsString::concat_array(&["(", json_string.as_str(), ");"]);
90
91 // 4. Let script be ParseText(! StringToCodePoints(scriptString), Script).
92 // 5. NOTE: The early error rules defined in 13.2.5.1 have special handling for the above invocation of ParseText.
93 // 6. Assert: script is a Parse Node.
94 // 7. Let completion be the result of evaluating script.
95 // 8. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation.
96 // 9. Let unfiltered be completion.[[Value]].
97 // 10. Assert: unfiltered is either a String, Number, Boolean, Null, or an Object that is defined by either an ArrayLiteral or an ObjectLiteral.
98 let unfiltered = context.eval(script_string.as_bytes())?;
99
100 match args.get(1).cloned().unwrap_or_default().as_object() {
101 // 11. If IsCallable(reviver) is true, then
102 Some(obj) if obj.is_callable() => {
103 // a. Let root be ! OrdinaryObjectCreate(%Object.prototype%).
104 let root = context.construct_object();
105
106 // b. Let rootName be the empty String.
107 // c. Perform ! CreateDataPropertyOrThrow(root, rootName, unfiltered).
108 root.create_data_property_or_throw("", unfiltered, context)
109 .expect("CreateDataPropertyOrThrow should never throw here");
110
111 // d. Return ? InternalizeJSONProperty(root, rootName, reviver).
112 Self::internalize_json_property(root, "".into(), obj, context)
113 }
114 // 12. Else,
115 // a. Return unfiltered.
116 _ => Ok(unfiltered),
117 }
118 }
119
120 /// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )`
121 ///
122 /// More information:
123 /// - [ECMAScript reference][spec]
124 ///
125 /// [spec]: https://tc39.es/ecma262/#sec-internalizejsonproperty
126 fn internalize_json_property(
127 holder: JsObject,
128 name: JsString,
129 reviver: JsObject,
130 context: &mut Context,
131 ) -> JsResult<JsValue> {
132 // 1. Let val be ? Get(holder, name).
133 let val = holder.get(name.clone(), context)?;
134
135 // 2. If Type(val) is Object, then
136 if let Some(obj) = val.as_object() {
137 // a. Let isArray be ? IsArray(val).
138 // b. If isArray is true, then
139 if obj.is_array() {
140 // i. Let I be 0.
141 // ii. Let len be ? LengthOfArrayLike(val).
142 // iii. Repeat, while I < len,
143 let len = obj.length_of_array_like(context)? as i64;
144 for i in 0..len {
145 // 1. Let prop be ! ToString(𝔽(I)).
146 // 2. Let newElement be ? InternalizeJSONProperty(val, prop, reviver).
147 let new_element = Self::internalize_json_property(
148 obj.clone(),
149 i.to_string().into(),
150 reviver.clone(),
151 context,
152 )?;
153
154 // 3. If newElement is undefined, then
155 if new_element.is_undefined() {
156 // a. Perform ? val.[[Delete]](prop).
157 obj.__delete__(&i.into(), context)?;
158 }
159 // 4. Else,
160 else {
161 // a. Perform ? CreateDataProperty(val, prop, newElement).
162 obj.create_data_property(i, new_element, context)?;
163 }
164 }
165 }
166 // c. Else,
167 else {
168 // i. Let keys be ? EnumerableOwnPropertyNames(val, key).
169 let keys = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?;
170
171 // ii. For each String P of keys, do
172 for p in keys {
173 // This is safe, because EnumerableOwnPropertyNames with 'key' type only returns strings.
174 let p = p.as_string().unwrap();
175
176 // 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver).
177 let new_element = Self::internalize_json_property(
178 obj.clone(),
179 p.clone(),
180 reviver.clone(),
181 context,
182 )?;
183
184 // 2. If newElement is undefined, then
185 if new_element.is_undefined() {
186 // a. Perform ? val.[[Delete]](P).
187 obj.__delete__(&p.clone().into(), context)?;
188 }
189 // 3. Else,
190 else {
191 // a. Perform ? CreateDataProperty(val, P, newElement).
192 obj.create_data_property(p.as_str(), new_element, context)?;
193 }
194 }
195 }
196 }
197
198 // 3. Return ? Call(reviver, holder, « name, val »).
199 reviver.call(&holder.into(), &[name.into(), val], context)
200 }
201
202 /// `JSON.stringify( value[, replacer[, space]] )`
203 ///
204 /// This `JSON` method converts a JavaScript object or value to a JSON string.
205 ///
206 /// This method optionally replaces values if a `replacer` function is specified or
207 /// optionally including only the specified properties if a replacer array is specified.
208 ///
209 /// An optional `space` argument can be supplied of type `String` or `Number` that's used to insert
210 /// white space into the output JSON string for readability purposes.
211 ///
212 /// More information:
213 /// - [ECMAScript reference][spec]
214 /// - [MDN documentation][mdn]
215 ///
216 /// [spec]: https://tc39.es/ecma262/#sec-json.stringify
217 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
218 pub(crate) fn stringify(
219 _: &JsValue,
220 args: &[JsValue],
221 context: &mut Context,
222 ) -> JsResult<JsValue> {
223 // 1. Let stack be a new empty List.
224 let stack = Vec::new();
225
226 // 2. Let indent be the empty String.
227 let indent = JsString::new("");
228
229 // 3. Let PropertyList and ReplacerFunction be undefined.
230 let mut property_list = None;
231 let mut replacer_function = None;
232
233 let replacer = args.get_or_undefined(1);
234
235 // 4. If Type(replacer) is Object, then
236 if let Some(replacer_obj) = replacer.as_object() {
237 // a. If IsCallable(replacer) is true, then
238 if replacer_obj.is_callable() {
239 // i. Set ReplacerFunction to replacer.
240 replacer_function = Some(replacer_obj)
241 // b. Else,
242 } else {
243 // i. Let isArray be ? IsArray(replacer).
244 // ii. If isArray is true, then
245 if replacer_obj.is_array() {
246 // 1. Set PropertyList to a new empty List.
247 let mut property_set = indexmap::IndexSet::new();
248
249 // 2. Let len be ? LengthOfArrayLike(replacer).
250 let len = replacer_obj.length_of_array_like(context)?;
251
252 // 3. Let k be 0.
253 let mut k = 0;
254
255 // 4. Repeat, while k < len,
256 while k < len {
257 // a. Let prop be ! ToString(𝔽(k)).
258 // b. Let v be ? Get(replacer, prop).
259 let v = replacer_obj.get(k, context)?;
260
261 // c. Let item be undefined.
262 // d. If Type(v) is String, set item to v.
263 // e. Else if Type(v) is Number, set item to ! ToString(v).
264 // f. Else if Type(v) is Object, then
265 // g. If item is not undefined and item is not currently an element of PropertyList, then
266 // i. Append item to the end of PropertyList.
267 if let Some(s) = v.as_string() {
268 property_set.insert(s.to_owned());
269 } else if v.is_number() {
270 property_set.insert(
271 v.to_string(context)
272 .expect("ToString cannot fail on number value"),
273 );
274 } else if let Some(obj) = v.as_object() {
275 // i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v).
276 if obj.is_string() || obj.is_number() {
277 property_set.insert(v.to_string(context)?);
278 }
279 }
280
281 // h. Set k to k + 1.
282 k += 1;
283 }
284 property_list = Some(property_set.into_iter().collect());
285 }
286 }
287 }
288
289 let mut space = args.get_or_undefined(2).clone();
290
291 // 5. If Type(space) is Object, then
292 if let Some(space_obj) = space.as_object() {
293 // a. If space has a [[NumberData]] internal slot, then
294 if space_obj.is_number() {
295 // i. Set space to ? ToNumber(space).
296 space = space.to_number(context)?.into();
297 }
298 // b. Else if space has a [[StringData]] internal slot, then
299 else if space_obj.is_string() {
300 // i. Set space to ? ToString(space).
301 space = space.to_string(context)?.into();
302 }
303 }
304
305 // 6. If Type(space) is Number, then
306 let gap = if space.is_number() {
307 // a. Let spaceMV be ! ToIntegerOrInfinity(space).
308 // b. Set spaceMV to min(10, spaceMV).
309 // c. If spaceMV < 1, let gap be the empty String; otherwise let gap be the String value containing spaceMV occurrences of the code unit 0x0020 (SPACE).
310 match space
311 .to_integer_or_infinity(context)
312 .expect("ToIntegerOrInfinity cannot fail on number")
313 {
314 IntegerOrInfinity::PositiveInfinity => JsString::new(" "),
315 IntegerOrInfinity::NegativeInfinity => JsString::new(""),
316 IntegerOrInfinity::Integer(i) if i < 1 => JsString::new(""),
317 IntegerOrInfinity::Integer(i) => {
318 let mut s = String::new();
319 let i = std::cmp::min(10, i);
320 for _ in 0..i {
321 s.push(' ');
322 }
323 s.into()
324 }
325 }
326 // 7. Else if Type(space) is String, then
327 } else if let Some(s) = space.as_string() {
328 // a. If the length of space is 10 or less, let gap be space; otherwise let gap be the substring of space from 0 to 10.
329 String::from_utf16_lossy(&s.encode_utf16().take(10).collect::<Vec<u16>>()).into()
330 // 8. Else,
331 } else {
332 // a. Let gap be the empty String.
333 JsString::new("")
334 };
335
336 // 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%).
337 let wrapper = context.construct_object();
338
339 // 10. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value).
340 wrapper
341 .create_data_property_or_throw("", args.get_or_undefined(0).clone(), context)
342 .expect("CreateDataPropertyOrThrow should never fail here");
343
344 // 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }.
345 let mut state = StateRecord {
346 replacer_function,
347 stack,
348 indent,
349 gap,
350 property_list,
351 };
352
353 // 12. Return ? SerializeJSONProperty(state, the empty String, wrapper).
354 Ok(
355 Self::serialize_json_property(&mut state, JsString::new(""), wrapper, context)?
356 .map(|s| s.into())
357 .unwrap_or_default(),
358 )
359 }
360
361 /// `25.5.2.1 SerializeJSONProperty ( state, key, holder )`
362 ///
363 /// More information:
364 /// - [ECMAScript reference][spec]
365 ///
366 /// [spec]: https://tc39.es/ecma262/#sec-serializejsonproperty
367 fn serialize_json_property(
368 state: &mut StateRecord,
369 key: JsString,
370 holder: JsObject,
371 context: &mut Context,
372 ) -> JsResult<Option<JsString>> {
373 // 1. Let value be ? Get(holder, key).
374 let mut value = holder.get(key.clone(), context)?;
375
376 // 2. If Type(value) is Object or BigInt, then
377 if value.is_object() || value.is_bigint() {
378 // a. Let toJSON be ? GetV(value, "toJSON").
379 let to_json = value.get_field("toJSON", context)?;
380
381 // b. If IsCallable(toJSON) is true, then
382 if let Some(obj) = to_json.as_object() {
383 if obj.is_callable() {
384 // i. Set value to ? Call(toJSON, value, « key »).
385 value = obj.call(&value, &[key.clone().into()], context)?;
386 }
387 }
388 }
389
390 // 3. If state.[[ReplacerFunction]] is not undefined, then
391 if let Some(obj) = &state.replacer_function {
392 // a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »).
393 value = obj.call(&holder.into(), &[key.into(), value], context)?
394 }
395
396 // 4. If Type(value) is Object, then
397 if let Some(obj) = value.as_object() {
398 // a. If value has a [[NumberData]] internal slot, then
399 if obj.is_number() {
400 // i. Set value to ? ToNumber(value).
401 value = value.to_number(context)?.into();
402 }
403 // b. Else if value has a [[StringData]] internal slot, then
404 else if obj.is_string() {
405 // i. Set value to ? ToString(value).
406 value = value.to_string(context)?.into();
407 }
408 // c. Else if value has a [[BooleanData]] internal slot, then
409 else if let Some(boolean) = obj.borrow().as_boolean() {
410 // i. Set value to value.[[BooleanData]].
411 value = boolean.into()
412 }
413 // d. Else if value has a [[BigIntData]] internal slot, then
414 else if let Some(bigint) = obj.borrow().as_bigint() {
415 // i. Set value to value.[[BigIntData]].
416 value = bigint.clone().into()
417 }
418 }
419
420 // 5. If value is null, return "null".
421 if value.is_null() {
422 return Ok(Some(JsString::new("null")));
423 }
424
425 // 6. If value is true, return "true".
426 // 7. If value is false, return "false".
427 if value.is_boolean() {
428 return match value.to_boolean() {
429 true => Ok(Some(JsString::new("true"))),
430 false => Ok(Some(JsString::new("false"))),
431 };
432 }
433
434 // 8. If Type(value) is String, return QuoteJSONString(value).
435 if let Some(s) = value.as_string() {
436 return Ok(Some(Self::quote_json_string(s)));
437 }
438
439 // 9. If Type(value) is Number, then
440 if let Some(n) = value.as_number() {
441 // a. If value is finite, return ! ToString(value).
442 if n.is_finite() {
443 return Ok(Some(
444 value
445 .to_string(context)
446 .expect("ToString should never fail here"),
447 ));
448 }
449
450 // b. Return "null".
451 return Ok(Some(JsString::new("null")));
452 }
453
454 // 10. If Type(value) is BigInt, throw a TypeError exception.
455 if value.is_bigint() {
456 return Err(context.construct_type_error("cannot serialize bigint to JSON"));
457 }
458
459 // 11. If Type(value) is Object and IsCallable(value) is false, then
460 if let Some(obj) = value.as_object() {
461 if !obj.is_callable() {
462 // a. Let isArray be ? IsArray(value).
463 // b. If isArray is true, return ? SerializeJSONArray(state, value).
464 // c. Return ? SerializeJSONObject(state, value).
465 return if obj.is_array() {
466 Ok(Some(Self::serialize_json_array(state, obj, context)?))
467 } else {
468 Ok(Some(Self::serialize_json_object(state, obj, context)?))
469 };
470 }
471 }
472
473 // 12. Return undefined.
474 Ok(None)
475 }
476
477 /// `25.5.2.2 QuoteJSONString ( value )`
478 ///
479 /// More information:
480 /// - [ECMAScript reference][spec]
481 ///
482 /// [spec]: https://tc39.es/ecma262/#sec-quotejsonstring
483 fn quote_json_string(value: &JsString) -> JsString {
484 // 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK).
485 let mut product = String::from('"');
486
487 // 2. For each code point C of ! StringToCodePoints(value), do
488 for code_point in value.encode_utf16() {
489 match code_point {
490 // a. If C is listed in the “Code Point” column of Table 73, then
491 // i. Set product to the string-concatenation of product and the escape sequence for C as specified in the “Escape Sequence” column of the corresponding row.
492 0x8 => product.push_str("\\b"),
493 0x9 => product.push_str("\\t"),
494 0xA => product.push_str("\\n"),
495 0xC => product.push_str("\\f"),
496 0xD => product.push_str("\\r"),
497 0x22 => product.push_str("\\\""),
498 0x5C => product.push_str("\\\\"),
499 // b. Else if C has a numeric value less than 0x0020 (SPACE), or if C has the same numeric value as a leading surrogate or trailing surrogate, then
500 code_point
501 if is_leading_surrogate(code_point) || is_trailing_surrogate(code_point) =>
502 {
503 // i. Let unit be the code unit whose numeric value is that of C.
504 // ii. Set product to the string-concatenation of product and UnicodeEscape(unit).
505 product.push_str(&format!("\\\\uAA{:x}", code_point));
506 }
507 // c. Else,
508 code_point => {
509 // i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C).
510 product.push(
511 char::from_u32(code_point as u32)
512 .expect("char from code point cannot fail here"),
513 );
514 }
515 }
516 }
517
518 // 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK).
519 product.push('"');
520
521 // 4. Return product.
522 product.into()
523 }
524
525 /// `25.5.2.4 SerializeJSONObject ( state, value )`
526 ///
527 /// More information:
528 /// - [ECMAScript reference][spec]
529 ///
530 /// [spec]: https://tc39.es/ecma262/#sec-serializejsonobject
531 fn serialize_json_object(
532 state: &mut StateRecord,
533 value: JsObject,
534 context: &mut Context,
535 ) -> JsResult<JsString> {
536 // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
537 let limiter = RecursionLimiter::new(&value);
538 if limiter.live {
539 return Err(context.construct_type_error("cyclic object value"));
540 }
541
542 // 2. Append value to state.[[Stack]].
543 state.stack.push(value.clone().into());
544
545 // 3. Let stepback be state.[[Indent]].
546 let stepback = state.indent.clone();
547
548 // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
549 state.indent = JsString::concat(&state.indent, &state.gap);
550
551 // 5. If state.[[PropertyList]] is not undefined, then
552 let k = if let Some(p) = &state.property_list {
553 // a. Let K be state.[[PropertyList]].
554 p.clone()
555 // 6. Else,
556 } else {
557 // a. Let K be ? EnumerableOwnPropertyNames(value, key).
558 let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?;
559 // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values.
560 keys.iter().map(|v| v.to_string(context).unwrap()).collect()
561 };
562
563 // 7. Let partial be a new empty List.
564 let mut partial = Vec::new();
565
566 // 8. For each element P of K, do
567 for p in &k {
568 // a. Let strP be ? SerializeJSONProperty(state, P, value).
569 let str_p = Self::serialize_json_property(state, p.clone(), value.clone(), context)?;
570
571 // b. If strP is not undefined, then
572 if let Some(str_p) = str_p {
573 // i. Let member be QuoteJSONString(P).
574 // ii. Set member to the string-concatenation of member and ":".
575 // iii. If state.[[Gap]] is not the empty String, then
576 // 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE).
577 // iv. Set member to the string-concatenation of member and strP.
578 let member = if state.gap.is_empty() {
579 format!("{}:{}", Self::quote_json_string(p).as_str(), str_p.as_str())
580 } else {
581 format!(
582 "{}: {}",
583 Self::quote_json_string(p).as_str(),
584 str_p.as_str()
585 )
586 };
587
588 // v. Append member to partial.
589 partial.push(member);
590 }
591 }
592
593 // 9. If partial is empty, then
594 let r#final = if partial.is_empty() {
595 // a. Let final be "{}".
596 JsString::new("{}")
597 // 10. Else,
598 } else {
599 // a. If state.[[Gap]] is the empty String, then
600 if state.gap.is_empty() {
601 // i. Let properties be the String value formed by concatenating all the element Strings of partial
602 // with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
603 // A comma is not inserted either before the first String or after the last String.
604 // ii. Let final be the string-concatenation of "{", properties, and "}".
605 format!("{{{}}}", partial.join(",")).into()
606 // b. Else,
607 } else {
608 // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
609 // the code unit 0x000A (LINE FEED), and state.[[Indent]].
610 let separator = format!(",{}{}", '\u{A}', state.indent.as_str());
611 // ii. Let properties be the String value formed by concatenating all the element Strings of partial
612 // with each adjacent pair of Strings separated with separator.
613 // The separator String is not inserted either before the first String or after the last String.
614 let properties = partial.join(&separator);
615 // iii. Let final be the string-concatenation of "{", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "}".
616 format!(
617 "{{{}{}{}{}{}}}",
618 '\u{A}',
619 state.indent.as_str(),
620 &properties,
621 '\u{A}',
622 stepback.as_str()
623 )
624 .into()
625 }
626 };
627
628 // 11. Remove the last element of state.[[Stack]].
629 state.stack.pop();
630
631 // 12. Set state.[[Indent]] to stepback.
632 state.indent = stepback;
633
634 // 13. Return final.
635 Ok(r#final)
636 }
637
638 /// `25.5.2.5 SerializeJSONArray ( state, value )`
639 ///
640 /// More information:
641 /// - [ECMAScript reference][spec]
642 ///
643 /// [spec]: https://tc39.es/ecma262/#sec-serializejsonarray
644 fn serialize_json_array(
645 state: &mut StateRecord,
646 value: JsObject,
647 context: &mut Context,
648 ) -> JsResult<JsString> {
649 // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical.
650 let limiter = RecursionLimiter::new(&value);
651 if limiter.live {
652 return Err(context.construct_type_error("cyclic object value"));
653 }
654
655 // 2. Append value to state.[[Stack]].
656 state.stack.push(value.clone().into());
657
658 // 3. Let stepback be state.[[Indent]].
659 let stepback = state.indent.clone();
660
661 // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]].
662 state.indent = JsString::concat(&state.indent, &state.gap);
663
664 // 5. Let partial be a new empty List.
665 let mut partial = Vec::new();
666
667 // 6. Let len be ? LengthOfArrayLike(value).
668 let len = value.length_of_array_like(context)?;
669
670 // 7. Let index be 0.
671 let mut index = 0;
672
673 // 8. Repeat, while index < len,
674 while index < len {
675 // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value).
676 let str_p = Self::serialize_json_property(
677 state,
678 index.to_string().into(),
679 value.clone(),
680 context,
681 )?;
682
683 // b. If strP is undefined, then
684 if let Some(str_p) = str_p {
685 // i. Append strP to partial.
686 partial.push(str_p)
687 // c. Else,
688 } else {
689 // i. Append "null" to partial.
690 partial.push("null".into())
691 }
692
693 // d. Set index to index + 1.
694 index += 1;
695 }
696
697 // 9. If partial is empty, then
698 let r#final = if partial.is_empty() {
699 // a. Let final be "[]".
700 JsString::from("[]")
701 // 10. Else,
702 } else {
703 // a. If state.[[Gap]] is the empty String, then
704 if state.gap.is_empty() {
705 // i. Let properties be the String value formed by concatenating all the element Strings of partial
706 // with each adjacent pair of Strings separated with the code unit 0x002C (COMMA).
707 // A comma is not inserted either before the first String or after the last String.
708 // ii. Let final be the string-concatenation of "[", properties, and "]".
709 format!("[{}]", partial.join(",")).into()
710 // b. Else,
711 } else {
712 // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA),
713 // the code unit 0x000A (LINE FEED), and state.[[Indent]].
714 let separator = format!(",{}{}", '\u{A}', state.indent.as_str());
715 // ii. Let properties be the String value formed by concatenating all the element Strings of partial
716 // with each adjacent pair of Strings separated with separator.
717 // The separator String is not inserted either before the first String or after the last String.
718 let properties = partial.join(&separator);
719 // iii. Let final be the string-concatenation of "[", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "]".
720 format!(
721 "[{}{}{}{}{}]",
722 '\u{A}',
723 state.indent.as_str(),
724 &properties,
725 '\u{A}',
726 stepback.as_str()
727 )
728 .into()
729 }
730 };
731
732 // 11. Remove the last element of state.[[Stack]].
733 state.stack.pop();
734
735 // 12. Set state.[[Indent]] to stepback.
736 state.indent = stepback;
737
738 // 13. Return final.
739 Ok(r#final)
740 }
741}
742
743struct StateRecord {
744 replacer_function: Option<JsObject>,
745 stack: Vec<JsValue>,
746 indent: JsString,
747 gap: JsString,
748 property_list: Option<Vec<JsString>>,
749}