Skip to main content

script/dom/
console.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10    ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields, DebuggerValue, FunctionPreview,
11    ObjectPreview, PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg,
12    StackFrame, get_time_stamp,
13};
14use embedder_traits::EmbedderMsg;
15use js::context::JSContext;
16use js::conversions::jsstr_to_string;
17use js::jsapi::{self, ESClass, JS_GetFunctionArity, PropertyDescriptor, SavedFrameSelfHosted};
18use js::jsval::{Int32Value, UndefinedValue};
19use js::realm::CurrentRealm;
20use js::rust::wrappers2::{
21    GetArrayLength, GetBuiltinClass, GetPropertyKeys, GetSavedFrameColumn,
22    GetSavedFrameFunctionDisplayName, GetSavedFrameLine, GetSavedFrameSource,
23    JS_ClearPendingException, JS_GetElement, JS_GetFunctionDisplayId, JS_GetFunctionId,
24    JS_GetOwnPropertyDescriptorById, JS_GetPropertyById, JS_IdToValue, JS_Stringify,
25    JS_ValueToFunction, JS_ValueToSource, MapEntries, MapSize,
26};
27use js::rust::{
28    CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
29    describe_scripted_caller, for_of,
30};
31use script_bindings::conversions::get_dom_class;
32
33use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
34use crate::dom::bindings::error::report_pending_exception;
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::str::DOMString;
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::workerglobalscope::WorkerGlobalScope;
39
40/// The maximum object depth logged by console methods.
41const MAX_LOG_DEPTH: usize = 10;
42/// The maximum elements in an object logged by console methods.
43const MAX_LOG_CHILDREN: usize = 15;
44
45/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
46#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
47pub(crate) struct Console;
48
49impl Console {
50    #[expect(unsafe_code)]
51    fn build_message(
52        cx: &mut JSContext,
53        level: ConsoleLogLevel,
54        arguments: Vec<DebuggerValue>,
55        stacktrace: Option<Vec<StackFrame>>,
56    ) -> ConsoleMessage {
57        let caller = unsafe { describe_scripted_caller(cx.raw_cx()) }.unwrap_or_default();
58
59        ConsoleMessage {
60            fields: ConsoleMessageFields {
61                level,
62                filename: caller.filename,
63                line_number: caller.line,
64                column_number: caller.col,
65                time_stamp: get_time_stamp(),
66            },
67            arguments,
68            stacktrace,
69        }
70    }
71
72    /// Helper to send a message that only consists of a single string
73    fn send_string_message(
74        cx: &mut JSContext,
75        global: &GlobalScope,
76        level: ConsoleLogLevel,
77        message: String,
78    ) {
79        let prefix = global.current_group_label().unwrap_or_default();
80        let formatted_message = format!("{prefix}{message}");
81
82        Self::send_to_embedder(global, level.clone(), formatted_message);
83
84        let console_message =
85            Self::build_message(cx, level, vec![DebuggerValue::StringValue(message)], None);
86
87        Self::send_to_devtools(global, console_message);
88    }
89
90    fn method(
91        cx: &mut JSContext,
92        global: &GlobalScope,
93        level: ConsoleLogLevel,
94        messages: Vec<HandleValue>,
95        include_stacktrace: IncludeStackTrace,
96    ) {
97        // If the first argument is a string, apply sprintf-style substitutions per the
98        // WHATWG Console spec formatter. The result is a single formatted string followed
99        // by any arguments that were not consumed by a substitution specifier.
100        let (arguments, embedder_msg) = if !messages.is_empty() && messages[0].is_string() {
101            let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
102            let remaining = &messages[consumed..];
103
104            let mut arguments: Vec<DebuggerValue> =
105                vec![DebuggerValue::StringValue(formatted.clone())];
106            for msg in remaining {
107                arguments.push(console_argument_from_handle_value(
108                    cx,
109                    *msg,
110                    &mut Vec::new(),
111                ));
112            }
113
114            let embedder_msg = if remaining.is_empty() {
115                formatted
116            } else {
117                format!("{formatted} {}", stringify_handle_values(cx, remaining))
118            };
119
120            (arguments, embedder_msg.into())
121        } else {
122            let arguments = messages
123                .iter()
124                .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
125                .collect();
126            (arguments, stringify_handle_values(cx, &messages))
127        };
128
129        let stacktrace = (include_stacktrace == IncludeStackTrace::Yes).then_some(get_js_stack(cx));
130        let console_message = Self::build_message(cx, level.clone(), arguments, stacktrace);
131
132        Console::send_to_devtools(global, console_message);
133
134        let prefix = global.current_group_label().unwrap_or_default();
135        let formatted_message = format!("{prefix}{embedder_msg}");
136
137        Self::send_to_embedder(global, level, formatted_message);
138    }
139
140    fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
141        if let Some(chan) = global.devtools_chan() {
142            let worker_id = global
143                .downcast::<WorkerGlobalScope>()
144                .map(|worker| worker.worker_id());
145            let devtools_message =
146                ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
147            chan.send(devtools_message).unwrap();
148        }
149    }
150
151    fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
152        global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
153            global.webview_id(),
154            level,
155            message,
156        ));
157    }
158
159    // Directly logs a string message, without processing the message
160    pub(crate) fn internal_warn(cx: &mut JSContext, global: &GlobalScope, message: String) {
161        Console::send_string_message(cx, global, ConsoleLogLevel::Warn, message);
162    }
163}
164
165#[expect(unsafe_code)]
166fn handle_value_to_string(cx: &mut JSContext, value: HandleValue) -> DOMString {
167    match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
168        Some(js_str) => unsafe { jsstr_to_string(cx, js_str) }.into(),
169        None => "<error converting value to string>".into(),
170    }
171}
172
173fn console_argument_from_handle_value(
174    cx: &mut JSContext,
175    handle_value: HandleValue,
176    seen: &mut Vec<u64>,
177) -> DebuggerValue {
178    #[expect(unsafe_code)]
179    fn inner(
180        cx: &mut JSContext,
181        handle_value: HandleValue,
182        seen: &mut Vec<u64>,
183    ) -> Result<DebuggerValue, ()> {
184        if handle_value.is_string() {
185            let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
186            let dom_string = unsafe { jsstr_to_string(cx, js_string) };
187            return Ok(DebuggerValue::StringValue(dom_string));
188        }
189
190        if handle_value.is_number() {
191            let number = handle_value.to_number();
192            return Ok(DebuggerValue::NumberValue(number));
193        }
194
195        if handle_value.is_boolean() {
196            let boolean = handle_value.to_boolean();
197            return Ok(DebuggerValue::BooleanValue(boolean));
198        }
199
200        if handle_value.is_object() {
201            // JS objects can create circular reference, and we want to avoid recursing infinitely
202            if seen.contains(&handle_value.asBits_) {
203                // FIXME: Handle this properly
204                return Ok(DebuggerValue::StringValue("[circular]".into()));
205            }
206
207            seen.push(handle_value.asBits_);
208            let console_object = console_object_from_handle_value(cx, handle_value, seen);
209            let js_value = seen.pop();
210            debug_assert_eq!(js_value, Some(handle_value.asBits_));
211
212            if let Some((class, preview)) = console_object {
213                return Ok(DebuggerValue::ObjectValue {
214                    actor: None,
215                    class,
216                    own_property_length: preview.own_properties_length,
217                    preview: Some(Box::new(preview)),
218                });
219            }
220
221            return Err(());
222        }
223
224        // FIXME: Handle more complex argument types here
225        let stringified_value = stringify_handle_value(cx, handle_value);
226
227        Ok(DebuggerValue::StringValue(stringified_value.into()))
228    }
229
230    match inner(cx, handle_value, seen) {
231        Ok(arg) => arg,
232        Err(()) => {
233            report_pending_exception(&mut CurrentRealm::assert(cx));
234            DebuggerValue::StringValue("<error>".into())
235        },
236    }
237}
238
239fn accessor_value_from_property_descriptor(descriptor: &PropertyDescriptor) -> DebuggerValue {
240    // https://console.spec.whatwg.org/#printer
241    // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
242    let value = match (
243        descriptor.hasGetter_() && !descriptor.getter_.is_null(),
244        descriptor.hasSetter_() && !descriptor.setter_.is_null(),
245    ) {
246        (true, true) => "Getter/Setter",
247        (true, false) => "Getter",
248        (false, true) => "Setter",
249        (false, false) => "undefined",
250    };
251    DebuggerValue::StringValue(value.into())
252}
253
254#[expect(unsafe_code)]
255fn console_map_object_from_handle_value(
256    cx: &mut JSContext,
257    handle_object: HandleObject,
258    seen: &mut Vec<u64>,
259) -> Option<(String, ObjectPreview)> {
260    rooted!(&in(cx) let mut iterator = UndefinedValue());
261    if !unsafe { MapEntries(cx, handle_object, iterator.handle_mut()) } {
262        return None;
263    }
264
265    let mut entries = Vec::new();
266    for_of(unsafe { cx.raw_cx() }, iterator.handle(), |entry| {
267        if !entry.is_object() {
268            return Err(().into());
269        }
270
271        rooted!(&in(cx) let entry_object = entry.to_object());
272        rooted!(&in(cx) let mut key = UndefinedValue());
273        rooted!(&in(cx) let mut value = UndefinedValue());
274
275        // Each map entry is a [key, value] pair.
276        if !unsafe { JS_GetElement(cx, entry_object.handle(), 0, key.handle_mut()) } ||
277            !unsafe { JS_GetElement(cx, entry_object.handle(), 1, value.handle_mut()) }
278        {
279            return Err(().into());
280        }
281
282        entries.push((
283            console_argument_from_handle_value(cx, key.handle(), seen),
284            console_argument_from_handle_value(cx, value.handle(), seen),
285        ));
286
287        Ok(std::ops::ControlFlow::Continue(()))
288    })
289    .ok()?;
290
291    Some((
292        "Map".into(),
293        ObjectPreview {
294            kind: "MapLike".into(),
295            size: Some(unsafe { MapSize(cx, handle_object) }),
296            entries: Some(entries),
297            own_properties_length: Some(0),
298            own_properties: None,
299            function: None,
300            array_length: None,
301            items: None,
302        },
303    ))
304}
305
306#[expect(unsafe_code)]
307fn console_object_from_handle_value(
308    cx: &mut JSContext,
309    handle_value: HandleValue,
310    seen: &mut Vec<u64>,
311) -> Option<(String, ObjectPreview)> {
312    rooted!(&in(cx) let object = handle_value.to_object());
313    let mut object_class = ESClass::Other;
314    if !unsafe { GetBuiltinClass(cx, object.handle(), &mut object_class as *mut _) } {
315        return None;
316    }
317    if object_class != ESClass::Object &&
318        object_class != ESClass::Array &&
319        object_class != ESClass::Map &&
320        object_class != ESClass::Function
321    {
322        return None;
323    }
324
325    if object_class == ESClass::Map {
326        return console_map_object_from_handle_value(cx, object.handle(), seen);
327    }
328
329    let mut own_properties = Vec::new();
330    let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
331    let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
332    // https://console.spec.whatwg.org/#printer
333    // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
334    if !unsafe {
335        GetPropertyKeys(
336            cx,
337            object.handle(),
338            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
339            ids.handle_mut(),
340        )
341    } {
342        return None;
343    }
344
345    for id in ids.iter() {
346        rooted!(&in(cx) let id = *id);
347        rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
348
349        let mut is_none = false;
350        if !unsafe {
351            JS_GetOwnPropertyDescriptorById(
352                cx,
353                object.handle(),
354                id.handle(),
355                descriptor.handle_mut(),
356                &mut is_none,
357            )
358        } {
359            return None;
360        }
361        if is_none {
362            continue;
363        }
364
365        // https://console.spec.whatwg.org/#printer
366        // Objects with either generic JavaScript object formatting or optimally useful formatting applied.
367        let is_accessor = (descriptor.hasGetter_() && !descriptor.getter_.is_null()) ||
368            (descriptor.hasSetter_() && !descriptor.setter_.is_null());
369        let value = if is_accessor {
370            accessor_value_from_property_descriptor(&descriptor)
371        } else {
372            rooted!(&in(cx) let property = descriptor.value_);
373            console_argument_from_handle_value(cx, property.handle(), seen)
374        };
375
376        if object_class == ESClass::Array && id.is_int() {
377            let index = id.to_int();
378            items.push((index, value));
379            continue;
380        }
381
382        let key = if id.is_string() {
383            rooted!(&in(cx) let mut key_value = UndefinedValue());
384            if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
385                continue;
386            }
387            rooted!(&in(cx) let js_string = key_value.to_string());
388            let Some(js_string) = NonNull::new(js_string.get()) else {
389                continue;
390            };
391            unsafe { jsstr_to_string(cx, js_string) }
392        } else if id.is_symbol() || id.is_int() {
393            rooted!(&in(cx) let mut key_value = UndefinedValue());
394            if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
395                continue;
396            }
397            handle_value_to_string(cx, key_value.handle()).to_string()
398        } else {
399            continue;
400        };
401
402        own_properties.push(DevtoolsPropertyDescriptor {
403            name: key,
404            value,
405            configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
406            enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
407            writable: !is_accessor && descriptor.hasWritable_() && descriptor.writable_(),
408            is_accessor,
409        });
410    }
411
412    let (class, kind, function, array_length, items) = match object_class {
413        ESClass::Array => {
414            let mut len = 0u32;
415            if !unsafe { GetArrayLength(cx, object.handle(), &mut len) } {
416                return None;
417            }
418            items.sort_by_key(|(index, _)| *index);
419            let ordered: Vec<DebuggerValue> = items.into_iter().map(|(_, value)| value).collect();
420            (
421                "Array".into(),
422                "ArrayLike".into(),
423                None,
424                Some(len),
425                Some(ordered),
426            )
427        },
428        ESClass::Function => {
429            rooted!(&in(cx) let fun = unsafe { JS_ValueToFunction(cx, handle_value) });
430            rooted!(&in(cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
431            rooted!(&in(cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
432            let arity;
433            unsafe {
434                JS_GetFunctionId(cx, fun.handle(), name.handle_mut());
435                JS_GetFunctionDisplayId(cx, fun.handle(), display_name.handle_mut());
436                arity = JS_GetFunctionArity(fun.get());
437            }
438            let name = ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(cx, name) });
439            let display_name = ptr::NonNull::new(*display_name)
440                .map(|display_name| unsafe { jsstr_to_string(cx, display_name) });
441
442            // TODO: We should get the actual argument names from the function
443            // It's not trivial since we can't access the debugger API here
444            let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
445
446            let function = FunctionPreview {
447                name,
448                display_name,
449                parameter_names,
450                is_async: None,
451                is_generator: None,
452            };
453            (
454                "Function".into(),
455                "Object".into(),
456                Some(function),
457                None,
458                None,
459            )
460        },
461        // TODO: Investigate if this class should be the object class
462        _ => ("Object".into(), "Object".into(), None, None, None),
463    };
464
465    Some((
466        class,
467        ObjectPreview {
468            kind,
469            size: None,
470            entries: None,
471            own_properties_length: Some(own_properties.len() as u32),
472            own_properties: Some(own_properties),
473            function,
474            array_length,
475            items,
476        },
477    ))
478}
479
480#[expect(unsafe_code)]
481pub(crate) fn stringify_handle_value(cx: &mut JSContext, message: HandleValue) -> DOMString {
482    if message.is_string() {
483        let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
484        return unsafe { jsstr_to_string(cx, jsstr) }.into();
485    }
486    fn stringify_object_from_handle_value(
487        cx: &mut JSContext,
488        value: HandleValue,
489        parents: Vec<u64>,
490    ) -> DOMString {
491        rooted!(&in(cx) let mut obj = value.to_object());
492        let mut object_class = ESClass::Other;
493        if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
494            return DOMString::from("/* invalid */");
495        }
496        let mut ids = unsafe { IdVector::new(cx.raw_cx()) };
497        if !unsafe {
498            GetPropertyKeys(
499                cx,
500                obj.handle(),
501                jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
502                ids.handle_mut(),
503            )
504        } {
505            return DOMString::from("/* invalid */");
506        }
507        let truncate = ids.len() > MAX_LOG_CHILDREN;
508        if object_class != ESClass::Array && object_class != ESClass::Object {
509            if truncate {
510                return DOMString::from("…");
511            } else {
512                return handle_value_to_string(cx, value);
513            }
514        }
515
516        let mut explicit_keys = object_class == ESClass::Object;
517        let mut props = Vec::with_capacity(ids.len());
518        for id in ids.iter().take(MAX_LOG_CHILDREN) {
519            rooted!(&in(cx) let id = *id);
520            rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
521
522            let mut is_none = false;
523            if !unsafe {
524                JS_GetOwnPropertyDescriptorById(
525                    cx,
526                    obj.handle(),
527                    id.handle(),
528                    desc.handle_mut(),
529                    &mut is_none,
530                )
531            } {
532                return DOMString::from("/* invalid */");
533            }
534
535            rooted!(&in(cx) let mut property = UndefinedValue());
536            if !unsafe { JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) }
537            {
538                return DOMString::from("/* invalid */");
539            }
540
541            if !explicit_keys {
542                if id.is_int() {
543                    if let Ok(id_int) = usize::try_from(id.to_int()) {
544                        explicit_keys = props.len() != id_int;
545                    } else {
546                        explicit_keys = false;
547                    }
548                } else {
549                    explicit_keys = false;
550                }
551            }
552            let value_string = stringify_inner(cx, property.handle(), parents.clone());
553            if explicit_keys {
554                let key = if id.is_string() || id.is_symbol() || id.is_int() {
555                    rooted!(&in(cx) let mut key_value = UndefinedValue());
556                    if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
557                        return DOMString::from("/* invalid */");
558                    }
559                    handle_value_to_string(cx, key_value.handle())
560                } else {
561                    return DOMString::from("/* invalid */");
562                };
563                props.push(format!("{}: {}", key, value_string,));
564            } else {
565                props.push(String::from(value_string));
566            }
567        }
568        if truncate {
569            props.push("…".to_string());
570        }
571        if object_class == ESClass::Array {
572            DOMString::from(format!("[{}]", itertools::join(props, ", ")))
573        } else {
574            DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
575        }
576    }
577    fn stringify_inner(cx: &mut JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
578        if parents.len() >= MAX_LOG_DEPTH {
579            return DOMString::from("...");
580        }
581        let value_bits = value.asBits_;
582        if parents.contains(&value_bits) {
583            return DOMString::from("[circular]");
584        }
585        if value.is_undefined() {
586            // This produces a better value than "(void 0)" from JS_ValueToSource.
587            return DOMString::from("undefined");
588        } else if !value.is_object() {
589            return handle_value_to_string(cx, value);
590        }
591        parents.push(value_bits);
592
593        if value.is_object() &&
594            let Some(repr) = maybe_stringify_dom_object(cx, value)
595        {
596            return repr;
597        }
598        stringify_object_from_handle_value(cx, value, parents)
599    }
600    stringify_inner(cx, message, Vec::new())
601}
602
603#[expect(unsafe_code)]
604fn maybe_stringify_dom_object(cx: &mut JSContext, value: HandleValue) -> Option<DOMString> {
605    // The standard object serialization is not effective for DOM objects,
606    // since their properties generally live on the prototype object.
607    // Instead, fall back to the output of JSON.stringify combined
608    // with the class name extracted from the output of toString().
609    rooted!(&in(cx) let obj = value.to_object());
610    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
611    if !is_dom_class {
612        return None;
613    }
614    rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
615    let Some(class_name) = NonNull::new(class_name.get()) else {
616        return Some("<error converting DOM object to string>".into());
617    };
618    let class_name = unsafe { jsstr_to_string(cx, class_name) }
619        .replace("[object ", "")
620        .replace("]", "");
621    let mut repr = format!("{} ", class_name);
622    rooted!(&in(cx) let mut value = value.get());
623
624    #[expect(unsafe_code)]
625    unsafe extern "C" fn stringified(
626        string: *const u16,
627        len: u32,
628        data: *mut std::ffi::c_void,
629    ) -> bool {
630        let s = data as *mut String;
631        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
632        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
633        true
634    }
635
636    rooted!(&in(cx) let space = Int32Value(2));
637    let stringify_result = unsafe {
638        JS_Stringify(
639            cx,
640            value.handle_mut(),
641            HandleObject::null(),
642            space.handle(),
643            Some(stringified),
644            &mut repr as *mut String as *mut _,
645        )
646    };
647    if !stringify_result {
648        return Some("<error converting DOM object to string>".into());
649    }
650    Some(repr.into())
651}
652
653/// Apply sprintf-style substitutions to console format arguments per the WHATWG Console spec.
654///
655/// If the first argument is a string, it is treated as a format string where `%s`, `%d`, `%i`,
656/// `%f`, `%o`, `%O`, and `%c` are replaced by subsequent arguments. Returns the formatted string
657/// and the index of the first argument that was not consumed by a substitution.
658///
659/// <https://console.spec.whatwg.org/#formatter>
660#[expect(unsafe_code)]
661fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
662    debug_assert!(!messages.is_empty() && messages[0].is_string());
663
664    let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
665    let format_string = unsafe { jsstr_to_string(cx, js_string) };
666
667    let mut result = String::new();
668    let mut arg_index = 1usize;
669    let mut chars = format_string.chars().peekable();
670
671    while let Some(c) = chars.next() {
672        if c != '%' {
673            result.push(c);
674            continue;
675        }
676
677        match chars.peek().copied() {
678            Some('s') => {
679                chars.next();
680                if arg_index < messages.len() {
681                    result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
682                    arg_index += 1;
683                } else {
684                    result.push_str("%s");
685                }
686            },
687            Some('d') | Some('i') => {
688                let spec = chars.next().unwrap();
689                if arg_index < messages.len() {
690                    let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
691                    if num.is_err() {
692                        unsafe { JS_ClearPendingException(cx) };
693                    }
694                    arg_index += 1;
695                    format_integer_substitution(&mut result, num);
696                } else {
697                    result.push('%');
698                    result.push(spec);
699                }
700            },
701            Some('f') => {
702                chars.next();
703                if arg_index < messages.len() {
704                    let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
705                    if num.is_err() {
706                        unsafe { JS_ClearPendingException(cx) };
707                    }
708                    arg_index += 1;
709                    format_float_substitution(&mut result, num);
710                } else {
711                    result.push_str("%f");
712                }
713            },
714            Some('o') | Some('O') => {
715                let spec = chars.next().unwrap();
716                if arg_index < messages.len() {
717                    result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
718                    arg_index += 1;
719                } else {
720                    result.push('%');
721                    result.push(spec);
722                }
723            },
724            Some('c') => {
725                chars.next();
726                if arg_index < messages.len() {
727                    arg_index += 1; // consume but ignore CSS styling
728                }
729            },
730            Some('%') => {
731                chars.next();
732                result.push('%');
733            },
734            _ => {
735                result.push('%');
736            },
737        }
738    }
739
740    (result, arg_index)
741}
742
743fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
744    match num {
745        Ok(n) if n.is_nan() => result.push_str("NaN"),
746        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
747        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
748        Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
749        Err(_) => result.push_str("NaN"),
750    }
751}
752
753fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
754    match num {
755        Ok(n) if n.is_nan() => result.push_str("NaN"),
756        Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
757        Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
758        Ok(n) => result.push_str(&n.to_string()),
759        Err(_) => result.push_str("NaN"),
760    }
761}
762
763fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
764    DOMString::from(itertools::join(
765        messages
766            .iter()
767            .copied()
768            .map(|msg| stringify_handle_value(cx, msg)),
769        " ",
770    ))
771}
772
773/// An implementation of <https://console.spec.whatwg.org/#printer>.
774/// This produces a string version of the argument that is printed to the console.
775fn stringify_debugger_value(value: &DebuggerValue) -> String {
776    match value {
777        DebuggerValue::VoidValue => "undefined".into(),
778        DebuggerValue::NullValue(_) => "null".into(),
779        DebuggerValue::BooleanValue(value) => value.to_string(),
780        DebuggerValue::NumberValue(value) => value.to_string(),
781        DebuggerValue::StringValue(value) => value.clone(),
782        DebuggerValue::ObjectValue { class, preview, .. } => {
783            let Some(preview) = preview else {
784                return class.clone();
785            };
786
787            if preview.kind == "ArrayLike" {
788                let mut items = preview
789                    .items
790                    .as_ref()
791                    .map(|items| {
792                        items
793                            .iter()
794                            .take(MAX_LOG_CHILDREN)
795                            .map(stringify_debugger_value)
796                            .collect::<Vec<_>>()
797                    })
798                    .unwrap_or_default();
799                if preview
800                    .array_length
801                    .is_some_and(|length| length as usize > items.len())
802                {
803                    items.push("...".into());
804                }
805                return format!("[{}]", itertools::join(items, ", "));
806            }
807
808            let mut properties = preview
809                .own_properties
810                .as_ref()
811                .map(|properties| {
812                    properties
813                        .iter()
814                        .take(MAX_LOG_CHILDREN)
815                        .map(|property| {
816                            format!(
817                                "{}: {}",
818                                property.name,
819                                stringify_debugger_value(&property.value)
820                            )
821                        })
822                        .collect::<Vec<_>>()
823                })
824                .unwrap_or_default();
825            if preview
826                .own_properties_length
827                .is_some_and(|length| length as usize > properties.len())
828            {
829                properties.push("...".into());
830            }
831            format!("{class} {{{}}}", itertools::join(properties, ", "))
832        },
833    }
834}
835
836#[derive(Debug, Eq, PartialEq)]
837enum IncludeStackTrace {
838    Yes,
839    No,
840}
841
842impl consoleMethods<crate::DomTypeHolder> for Console {
843    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
844    fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
845        Console::method(
846            cx,
847            global,
848            ConsoleLogLevel::Log,
849            messages,
850            IncludeStackTrace::No,
851        );
852    }
853
854    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
855    fn Clear(global: &GlobalScope) {
856        if let Some(chan) = global.devtools_chan() {
857            let worker_id = global
858                .downcast::<WorkerGlobalScope>()
859                .map(|worker| worker.worker_id());
860            let devtools_message =
861                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
862            if let Err(error) = chan.send(devtools_message) {
863                log::warn!("Error sending clear message to devtools: {error:?}");
864            }
865        }
866    }
867
868    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
869    fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
870        Console::method(
871            cx,
872            global,
873            ConsoleLogLevel::Debug,
874            messages,
875            IncludeStackTrace::No,
876        );
877    }
878
879    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
880    fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
881        Console::method(
882            cx,
883            global,
884            ConsoleLogLevel::Info,
885            messages,
886            IncludeStackTrace::No,
887        );
888    }
889
890    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
891    fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
892        Console::method(
893            cx,
894            global,
895            ConsoleLogLevel::Warn,
896            messages,
897            IncludeStackTrace::No,
898        );
899    }
900
901    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
902    fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
903        Console::method(
904            cx,
905            global,
906            ConsoleLogLevel::Error,
907            messages,
908            IncludeStackTrace::No,
909        );
910    }
911
912    /// <https://console.spec.whatwg.org/#trace>
913    fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
914        Console::method(
915            cx,
916            global,
917            ConsoleLogLevel::Trace,
918            messages,
919            IncludeStackTrace::Yes,
920        );
921    }
922
923    /// <https://console.spec.whatwg.org/#dir>
924    fn Dir(
925        cx: &mut js::context::JSContext,
926        global: &GlobalScope,
927        item: HandleValue,
928        _options: Option<*mut jsapi::JSObject>,
929    ) {
930        // Step 1. Let object be item with generic JavaScript object formatting applied.
931        let argument = console_argument_from_handle_value(cx, item, &mut Vec::new());
932        let prefix = global.current_group_label().unwrap_or_default();
933        // Step 2. Perform Printer("dir", « object », options).
934        Console::send_to_devtools(
935            global,
936            Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
937        );
938        Self::send_to_embedder(
939            global,
940            ConsoleLogLevel::Dir,
941            format!("{prefix}{}", stringify_debugger_value(&argument)),
942        );
943    }
944
945    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
946    fn Assert(
947        cx: &mut JSContext,
948        global: &GlobalScope,
949        condition: bool,
950        messages: Vec<HandleValue>,
951    ) {
952        if !condition {
953            let message = format!(
954                "Assertion failed: {}",
955                stringify_handle_values(cx, &messages)
956            );
957
958            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
959        }
960    }
961
962    /// <https://console.spec.whatwg.org/#time>
963    fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
964        if let Ok(()) = global.time(label.clone()) {
965            let message = format!("{label}: timer started");
966            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
967        }
968    }
969
970    /// <https://console.spec.whatwg.org/#timelog>
971    fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
972        if let Ok(delta) = global.time_log(&label) {
973            let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
974
975            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
976        }
977    }
978
979    /// <https://console.spec.whatwg.org/#timeend>
980    fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
981        if let Ok(delta) = global.time_end(&label) {
982            let message = format!("{label}: {delta}ms");
983
984            Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
985        }
986    }
987
988    /// <https://console.spec.whatwg.org/#group>
989    fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
990        global.push_console_group(stringify_handle_values(cx, &messages));
991    }
992
993    /// <https://console.spec.whatwg.org/#groupcollapsed>
994    fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
995        global.push_console_group(stringify_handle_values(cx, &messages));
996    }
997
998    /// <https://console.spec.whatwg.org/#groupend>
999    fn GroupEnd(global: &GlobalScope) {
1000        global.pop_console_group();
1001    }
1002
1003    /// <https://console.spec.whatwg.org/#count>
1004    fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1005        let count = global.increment_console_count(&label);
1006        let message = format!("{label}: {count}");
1007
1008        Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1009    }
1010
1011    /// <https://console.spec.whatwg.org/#countreset>
1012    fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1013        if global.reset_console_count(&label).is_err() {
1014            Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
1015        }
1016    }
1017}
1018
1019#[expect(unsafe_code)]
1020fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
1021    const MAX_FRAME_COUNT: u32 = 128;
1022
1023    let mut frames = vec![];
1024    rooted!(&in(cx) let mut handle =  ptr::null_mut());
1025    let captured_js_stack =
1026        unsafe { CapturedJSStack::new(cx.raw_cx(), handle, Some(MAX_FRAME_COUNT)) };
1027    let Some(captured_js_stack) = captured_js_stack else {
1028        return frames;
1029    };
1030
1031    captured_js_stack.for_each_stack_frame(|frame| {
1032        rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
1033
1034        // Get function name
1035        unsafe {
1036            GetSavedFrameFunctionDisplayName(
1037                cx,
1038                ptr::null_mut(),
1039                frame,
1040                result.handle_mut(),
1041                SavedFrameSelfHosted::Include,
1042            );
1043        }
1044        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1045            unsafe { jsstr_to_string(cx, nonnull_result) }
1046        } else {
1047            "<anonymous>".into()
1048        };
1049
1050        // Get source file name
1051        result.set(ptr::null_mut());
1052        unsafe {
1053            GetSavedFrameSource(
1054                cx,
1055                ptr::null_mut(),
1056                frame,
1057                result.handle_mut(),
1058                SavedFrameSelfHosted::Include,
1059            );
1060        }
1061        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1062            unsafe { jsstr_to_string(cx, nonnull_result) }
1063        } else {
1064            "<anonymous>".into()
1065        };
1066
1067        // get line/column number
1068        let mut line_number = 0;
1069        unsafe {
1070            GetSavedFrameLine(
1071                cx,
1072                ptr::null_mut(),
1073                frame,
1074                &mut line_number,
1075                SavedFrameSelfHosted::Include,
1076            );
1077        }
1078
1079        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1080        unsafe {
1081            GetSavedFrameColumn(
1082                cx,
1083                ptr::null_mut(),
1084                frame,
1085                &mut column_number,
1086                SavedFrameSelfHosted::Include,
1087            );
1088        }
1089        let frame = StackFrame {
1090            filename,
1091            function_name,
1092            line_number,
1093            column_number: column_number.value_,
1094        };
1095
1096        frames.push(frame);
1097    });
1098
1099    frames
1100}