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    ConsoleArgument, ConsoleArgumentObject, ConsoleArgumentPropertyValue, ConsoleLogLevel,
11    ConsoleMessage, ConsoleMessageFields, ScriptToDevtoolsControlMsg, StackFrame, get_time_stamp,
12};
13use embedder_traits::EmbedderMsg;
14use js::conversions::jsstr_to_string;
15use js::jsapi::{self, ESClass, PropertyDescriptor};
16use js::jsval::{Int32Value, UndefinedValue};
17use js::rust::wrappers::{
18    GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
19    JS_IdToValue, JS_Stringify, JS_ValueToSource,
20};
21use js::rust::{
22    CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
23};
24use script_bindings::conversions::get_dom_class;
25
26use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::str::DOMString;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::workerglobalscope::WorkerGlobalScope;
31use crate::script_runtime::JSContext;
32
33/// The maximum object depth logged by console methods.
34const MAX_LOG_DEPTH: usize = 10;
35/// The maximum elements in an object logged by console methods.
36const MAX_LOG_CHILDREN: usize = 15;
37
38/// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
39#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
40pub(crate) struct Console;
41
42impl Console {
43    #[expect(unsafe_code)]
44    fn build_message(
45        level: ConsoleLogLevel,
46        arguments: Vec<ConsoleArgument>,
47        stacktrace: Option<Vec<StackFrame>>,
48    ) -> ConsoleMessage {
49        let cx = GlobalScope::get_cx();
50        let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
51
52        ConsoleMessage {
53            fields: ConsoleMessageFields {
54                level,
55                filename: caller.filename,
56                line_number: caller.line,
57                column_number: caller.col,
58                time_stamp: get_time_stamp(),
59            },
60            arguments,
61            stacktrace,
62        }
63    }
64
65    /// Helper to send a message that only consists of a single string
66    fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
67        let prefix = global.current_group_label().unwrap_or_default();
68        let formatted_message = format!("{prefix}{message}");
69
70        Self::send_to_embedder(global, level.clone(), formatted_message);
71
72        let console_message = Self::build_message(level, vec![message.into()], None);
73
74        Self::send_to_devtools(global, console_message);
75    }
76
77    fn method(
78        global: &GlobalScope,
79        level: ConsoleLogLevel,
80        messages: Vec<HandleValue>,
81        include_stacktrace: IncludeStackTrace,
82    ) {
83        let cx = GlobalScope::get_cx();
84
85        let arguments = messages
86            .iter()
87            .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
88            .collect();
89        let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
90            .then_some(get_js_stack(*GlobalScope::get_cx()));
91        let console_message = Self::build_message(level.clone(), arguments, stacktrace);
92
93        Console::send_to_devtools(global, console_message);
94
95        let prefix = global.current_group_label().unwrap_or_default();
96        let msgs = stringify_handle_values(&messages);
97        let formatted_message = format!("{prefix}{msgs}");
98
99        Self::send_to_embedder(global, level, formatted_message);
100    }
101
102    fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
103        if let Some(chan) = global.devtools_chan() {
104            let worker_id = global
105                .downcast::<WorkerGlobalScope>()
106                .map(|worker| worker.worker_id());
107            let devtools_message =
108                ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
109            chan.send(devtools_message).unwrap();
110        }
111    }
112
113    fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
114        global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
115            global.webview_id(),
116            level,
117            message,
118        ));
119    }
120
121    // Directly logs a DOMString, without processing the message
122    pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
123        Console::send_string_message(global, ConsoleLogLevel::Warn, String::from(message));
124    }
125}
126
127#[expect(unsafe_code)]
128unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
129    rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
130    match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
131        Some(js_str) => {
132            js_string.set(js_str.as_ptr());
133            unsafe { jsstr_to_string(cx, js_str) }.into()
134        },
135        None => "<error converting value to string>".into(),
136    }
137}
138
139#[expect(unsafe_code)]
140fn console_argument_from_handle_value(
141    cx: JSContext,
142    handle_value: HandleValue,
143    seen: &mut Vec<u64>,
144) -> ConsoleArgument {
145    if handle_value.is_string() {
146        let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
147        let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
148        return ConsoleArgument::String(dom_string);
149    }
150
151    if handle_value.is_int32() {
152        let integer = handle_value.to_int32();
153        return ConsoleArgument::Integer(integer);
154    }
155
156    if handle_value.is_number() {
157        let number = handle_value.to_number();
158        return ConsoleArgument::Number(number);
159    }
160
161    if handle_value.is_boolean() {
162        let boolean = handle_value.to_boolean();
163        return ConsoleArgument::Boolean(boolean);
164    }
165
166    if handle_value.is_object() {
167        // JS objects can create circular reference, and we want to avoid recursing infinitely
168        if seen.contains(&handle_value.asBits_) {
169            // FIXME: Handle this properly
170            return ConsoleArgument::String("[circular]".into());
171        }
172
173        seen.push(handle_value.asBits_);
174        let maybe_argument_object = console_object_from_handle_value(cx, handle_value, seen);
175        let js_value = seen.pop();
176        debug_assert_eq!(js_value, Some(handle_value.asBits_));
177
178        if let Some(console_argument_object) = maybe_argument_object {
179            return ConsoleArgument::Object(console_argument_object);
180        }
181    }
182
183    // FIXME: Handle more complex argument types here
184    let stringified_value = stringify_handle_value(handle_value);
185
186    ConsoleArgument::String(stringified_value.into())
187}
188
189#[expect(unsafe_code)]
190fn console_object_from_handle_value(
191    cx: JSContext,
192    handle_value: HandleValue,
193    seen: &mut Vec<u64>,
194) -> Option<ConsoleArgumentObject> {
195    rooted!(in(*cx) let object = handle_value.to_object());
196    let mut object_class = ESClass::Other;
197    if !unsafe { GetBuiltinClass(*cx, object.handle(), &mut object_class as *mut _) } {
198        return None;
199    }
200    if object_class != ESClass::Object {
201        return None;
202    }
203
204    let mut own_properties = Vec::new();
205    let mut ids = unsafe { IdVector::new(*cx) };
206    if !unsafe {
207        GetPropertyKeys(
208            *cx,
209            object.handle(),
210            jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
211            ids.handle_mut(),
212        )
213    } {
214        return None;
215    }
216
217    for id in ids.iter() {
218        rooted!(in(*cx) let id = *id);
219        rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
220
221        let mut is_none = false;
222        if !unsafe {
223            JS_GetOwnPropertyDescriptorById(
224                *cx,
225                object.handle(),
226                id.handle(),
227                descriptor.handle_mut(),
228                &mut is_none,
229            )
230        } {
231            return None;
232        }
233
234        rooted!(in(*cx) let mut property = UndefinedValue());
235        if !unsafe { JS_GetPropertyById(*cx, object.handle(), id.handle(), property.handle_mut()) }
236        {
237            return None;
238        }
239
240        let key = if id.is_string() {
241            rooted!(in(*cx) let mut key_value = UndefinedValue());
242            let raw_id: jsapi::HandleId = id.handle().into();
243            if !unsafe { JS_IdToValue(*cx, *raw_id.ptr, key_value.handle_mut()) } {
244                continue;
245            }
246            rooted!(in(*cx) let js_string = key_value.to_string());
247            let Some(js_string) = NonNull::new(js_string.get()) else {
248                continue;
249            };
250            unsafe { jsstr_to_string(*cx, js_string) }
251        } else {
252            continue;
253        };
254
255        own_properties.push(ConsoleArgumentPropertyValue {
256            key,
257            configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
258            enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
259            writable: descriptor.hasWritable_() && descriptor.writable_(),
260            value: console_argument_from_handle_value(cx, property.handle(), seen),
261        });
262    }
263
264    Some(ConsoleArgumentObject {
265        class: "Object".to_owned(),
266        own_properties,
267    })
268}
269
270#[expect(unsafe_code)]
271pub(crate) fn stringify_handle_value(message: HandleValue) -> DOMString {
272    let cx = GlobalScope::get_cx();
273    unsafe {
274        if message.is_string() {
275            let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
276            return jsstr_to_string(*cx, jsstr).into();
277        }
278        unsafe fn stringify_object_from_handle_value(
279            cx: *mut jsapi::JSContext,
280            value: HandleValue,
281            parents: Vec<u64>,
282        ) -> DOMString {
283            rooted!(in(cx) let mut obj = value.to_object());
284            let mut object_class = ESClass::Other;
285            if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
286                return DOMString::from("/* invalid */");
287            }
288            let mut ids = unsafe { IdVector::new(cx) };
289            if !unsafe {
290                GetPropertyKeys(
291                    cx,
292                    obj.handle(),
293                    jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
294                    ids.handle_mut(),
295                )
296            } {
297                return DOMString::from("/* invalid */");
298            }
299            let truncate = ids.len() > MAX_LOG_CHILDREN;
300            if object_class != ESClass::Array && object_class != ESClass::Object {
301                if truncate {
302                    return DOMString::from("…");
303                } else {
304                    return unsafe { handle_value_to_string(cx, value) };
305                }
306            }
307
308            let mut explicit_keys = object_class == ESClass::Object;
309            let mut props = Vec::with_capacity(ids.len());
310            for id in ids.iter().take(MAX_LOG_CHILDREN) {
311                rooted!(in(cx) let id = *id);
312                rooted!(in(cx) let mut desc = PropertyDescriptor::default());
313
314                let mut is_none = false;
315                if !unsafe {
316                    JS_GetOwnPropertyDescriptorById(
317                        cx,
318                        obj.handle(),
319                        id.handle(),
320                        desc.handle_mut(),
321                        &mut is_none,
322                    )
323                } {
324                    return DOMString::from("/* invalid */");
325                }
326
327                rooted!(in(cx) let mut property = UndefinedValue());
328                if !unsafe {
329                    JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
330                } {
331                    return DOMString::from("/* invalid */");
332                }
333
334                if !explicit_keys {
335                    if id.is_int() {
336                        if let Ok(id_int) = usize::try_from(id.to_int()) {
337                            explicit_keys = props.len() != id_int;
338                        } else {
339                            explicit_keys = false;
340                        }
341                    } else {
342                        explicit_keys = false;
343                    }
344                }
345                let value_string = stringify_inner(
346                    unsafe { JSContext::from_ptr(cx) },
347                    property.handle(),
348                    parents.clone(),
349                );
350                if explicit_keys {
351                    let key = if id.is_string() || id.is_symbol() || id.is_int() {
352                        rooted!(in(cx) let mut key_value = UndefinedValue());
353                        let raw_id: jsapi::HandleId = id.handle().into();
354                        if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
355                            return DOMString::from("/* invalid */");
356                        }
357                        unsafe { handle_value_to_string(cx, key_value.handle()) }
358                    } else {
359                        return DOMString::from("/* invalid */");
360                    };
361                    props.push(format!("{}: {}", key, value_string,));
362                } else {
363                    props.push(value_string.to_string());
364                }
365            }
366            if truncate {
367                props.push("…".to_string());
368            }
369            if object_class == ESClass::Array {
370                DOMString::from(format!("[{}]", itertools::join(props, ", ")))
371            } else {
372                DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
373            }
374        }
375        fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
376            if parents.len() >= MAX_LOG_DEPTH {
377                return DOMString::from("...");
378            }
379            let value_bits = value.asBits_;
380            if parents.contains(&value_bits) {
381                return DOMString::from("[circular]");
382            }
383            if value.is_undefined() {
384                // This produces a better value than "(void 0)" from JS_ValueToSource.
385                return DOMString::from("undefined");
386            } else if !value.is_object() {
387                return unsafe { handle_value_to_string(*cx, value) };
388            }
389            parents.push(value_bits);
390
391            if value.is_object() {
392                if let Some(repr) = maybe_stringify_dom_object(cx, value) {
393                    return repr;
394                }
395            }
396            unsafe { stringify_object_from_handle_value(*cx, value, parents) }
397        }
398        stringify_inner(cx, message, Vec::new())
399    }
400}
401
402#[expect(unsafe_code)]
403fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
404    // The standard object serialization is not effective for DOM objects,
405    // since their properties generally live on the prototype object.
406    // Instead, fall back to the output of JSON.stringify combined
407    // with the class name extracted from the output of toString().
408    rooted!(in(*cx) let obj = value.to_object());
409    let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
410    if !is_dom_class {
411        return None;
412    }
413    rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
414    let Some(class_name) = NonNull::new(class_name.get()) else {
415        return Some("<error converting DOM object to string>".into());
416    };
417    let class_name = unsafe {
418        jsstr_to_string(*cx, class_name)
419            .replace("[object ", "")
420            .replace("]", "")
421    };
422    let mut repr = format!("{} ", class_name);
423    rooted!(in(*cx) let mut value = value.get());
424
425    #[expect(unsafe_code)]
426    unsafe extern "C" fn stringified(
427        string: *const u16,
428        len: u32,
429        data: *mut std::ffi::c_void,
430    ) -> bool {
431        let s = data as *mut String;
432        let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
433        unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
434        true
435    }
436
437    rooted!(in(*cx) let space = Int32Value(2));
438    let stringify_result = unsafe {
439        JS_Stringify(
440            *cx,
441            value.handle_mut(),
442            HandleObject::null(),
443            space.handle(),
444            Some(stringified),
445            &mut repr as *mut String as *mut _,
446        )
447    };
448    if !stringify_result {
449        return Some("<error converting DOM object to string>".into());
450    }
451    Some(repr.into())
452}
453
454fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
455    DOMString::from(itertools::join(
456        messages.iter().copied().map(stringify_handle_value),
457        " ",
458    ))
459}
460
461#[derive(Debug, Eq, PartialEq)]
462enum IncludeStackTrace {
463    Yes,
464    No,
465}
466
467impl consoleMethods<crate::DomTypeHolder> for Console {
468    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/log>
469    fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
470        Console::method(
471            global,
472            ConsoleLogLevel::Log,
473            messages,
474            IncludeStackTrace::No,
475        );
476    }
477
478    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/clear>
479    fn Clear(global: &GlobalScope) {
480        if let Some(chan) = global.devtools_chan() {
481            let worker_id = global
482                .downcast::<WorkerGlobalScope>()
483                .map(|worker| worker.worker_id());
484            let devtools_message =
485                ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
486            if let Err(error) = chan.send(devtools_message) {
487                log::warn!("Error sending clear message to devtools: {error:?}");
488            }
489        }
490    }
491
492    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console>
493    fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
494        Console::method(
495            global,
496            ConsoleLogLevel::Debug,
497            messages,
498            IncludeStackTrace::No,
499        );
500    }
501
502    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/info>
503    fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
504        Console::method(
505            global,
506            ConsoleLogLevel::Info,
507            messages,
508            IncludeStackTrace::No,
509        );
510    }
511
512    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/warn>
513    fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
514        Console::method(
515            global,
516            ConsoleLogLevel::Warn,
517            messages,
518            IncludeStackTrace::No,
519        );
520    }
521
522    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/error>
523    fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
524        Console::method(
525            global,
526            ConsoleLogLevel::Error,
527            messages,
528            IncludeStackTrace::No,
529        );
530    }
531
532    /// <https://console.spec.whatwg.org/#trace>
533    fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
534        Console::method(
535            global,
536            ConsoleLogLevel::Trace,
537            messages,
538            IncludeStackTrace::Yes,
539        );
540    }
541
542    /// <https://developer.mozilla.org/en-US/docs/Web/API/Console/assert>
543    fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
544        if !condition {
545            let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
546
547            Console::send_string_message(global, ConsoleLogLevel::Log, message);
548        }
549    }
550
551    /// <https://console.spec.whatwg.org/#time>
552    fn Time(global: &GlobalScope, label: DOMString) {
553        if let Ok(()) = global.time(label.clone()) {
554            let message = format!("{label}: timer started");
555            Console::send_string_message(global, ConsoleLogLevel::Log, message);
556        }
557    }
558
559    /// <https://console.spec.whatwg.org/#timelog>
560    fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
561        if let Ok(delta) = global.time_log(&label) {
562            let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
563
564            Console::send_string_message(global, ConsoleLogLevel::Log, message);
565        }
566    }
567
568    /// <https://console.spec.whatwg.org/#timeend>
569    fn TimeEnd(global: &GlobalScope, label: DOMString) {
570        if let Ok(delta) = global.time_end(&label) {
571            let message = format!("{label}: {delta}ms");
572
573            Console::send_string_message(global, ConsoleLogLevel::Log, message);
574        }
575    }
576
577    /// <https://console.spec.whatwg.org/#group>
578    fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
579        global.push_console_group(stringify_handle_values(&messages));
580    }
581
582    /// <https://console.spec.whatwg.org/#groupcollapsed>
583    fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
584        global.push_console_group(stringify_handle_values(&messages));
585    }
586
587    /// <https://console.spec.whatwg.org/#groupend>
588    fn GroupEnd(global: &GlobalScope) {
589        global.pop_console_group();
590    }
591
592    /// <https://console.spec.whatwg.org/#count>
593    fn Count(global: &GlobalScope, label: DOMString) {
594        let count = global.increment_console_count(&label);
595        let message = format!("{label}: {count}");
596
597        Console::send_string_message(global, ConsoleLogLevel::Log, message);
598    }
599
600    /// <https://console.spec.whatwg.org/#countreset>
601    fn CountReset(global: &GlobalScope, label: DOMString) {
602        if global.reset_console_count(&label).is_err() {
603            Self::internal_warn(
604                global,
605                DOMString::from(format!("Counter “{label}” doesn’t exist.")),
606            )
607        }
608    }
609}
610
611#[expect(unsafe_code)]
612fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
613    const MAX_FRAME_COUNT: u32 = 128;
614
615    let mut frames = vec![];
616    rooted!(in(cx) let mut handle =  ptr::null_mut());
617    let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
618    let Some(captured_js_stack) = captured_js_stack else {
619        return frames;
620    };
621
622    captured_js_stack.for_each_stack_frame(|frame| {
623        rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
624
625        // Get function name
626        unsafe {
627            jsapi::GetSavedFrameFunctionDisplayName(
628                cx,
629                ptr::null_mut(),
630                frame.into(),
631                result.handle_mut().into(),
632                jsapi::SavedFrameSelfHosted::Include,
633            );
634        }
635        let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
636            unsafe { jsstr_to_string(cx, nonnull_result) }
637        } else {
638            "<anonymous>".into()
639        };
640
641        // Get source file name
642        result.set(ptr::null_mut());
643        unsafe {
644            jsapi::GetSavedFrameSource(
645                cx,
646                ptr::null_mut(),
647                frame.into(),
648                result.handle_mut().into(),
649                jsapi::SavedFrameSelfHosted::Include,
650            );
651        }
652        let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
653            unsafe { jsstr_to_string(cx, nonnull_result) }
654        } else {
655            "<anonymous>".into()
656        };
657
658        // get line/column number
659        let mut line_number = 0;
660        unsafe {
661            jsapi::GetSavedFrameLine(
662                cx,
663                ptr::null_mut(),
664                frame.into(),
665                &mut line_number,
666                jsapi::SavedFrameSelfHosted::Include,
667            );
668        }
669
670        let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
671        unsafe {
672            jsapi::GetSavedFrameColumn(
673                cx,
674                ptr::null_mut(),
675                frame.into(),
676                &mut column_number,
677                jsapi::SavedFrameSelfHosted::Include,
678            );
679        }
680        let frame = StackFrame {
681            filename,
682            function_name,
683            line_number,
684            column_number: column_number.value_,
685        };
686
687        frames.push(frame);
688    });
689
690    frames
691}