feat: add dependency
This commit is contained in:
36
javascript-engine/external/boa/boa_cli/Cargo.toml
vendored
Normal file
36
javascript-engine/external/boa/boa_cli/Cargo.toml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "boa_cli"
|
||||
keywords = ["javascript", "compiler", "js", "cli"]
|
||||
categories = ["command-line-utilities"]
|
||||
default-run = "boa"
|
||||
description.workspace = true
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
boa_engine = { workspace = true, features = ["deser", "console", "flowgraph"] }
|
||||
boa_ast = { workspace = true, features = ["serde"]}
|
||||
boa_parser.workspace = true
|
||||
rustyline = "10.1.1"
|
||||
rustyline-derive = "0.7.0"
|
||||
clap = { version = "4.1.1", features = ["derive"] }
|
||||
serde_json = "1.0.91"
|
||||
colored = "2.0.0"
|
||||
regex = "1.7.1"
|
||||
phf = { version = "0.11.1", features = ["macros"] }
|
||||
|
||||
[features]
|
||||
default = ["intl"]
|
||||
intl = ["boa_engine/intl"]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu.dependencies]
|
||||
jemallocator = "0.5.0"
|
||||
|
||||
[[bin]]
|
||||
name = "boa"
|
||||
doc = false
|
||||
path = "src/main.rs"
|
||||
172
javascript-engine/external/boa/boa_cli/src/helper.rs
vendored
Normal file
172
javascript-engine/external/boa/boa_cli/src/helper.rs
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
use colored::{Color, Colorize};
|
||||
use phf::{phf_set, Set};
|
||||
use regex::{Captures, Regex};
|
||||
use rustyline::{
|
||||
error::ReadlineError,
|
||||
highlight::Highlighter,
|
||||
validate::{MatchingBracketValidator, ValidationContext, ValidationResult, Validator},
|
||||
};
|
||||
use rustyline_derive::{Completer, Helper, Hinter};
|
||||
use std::borrow::Cow;
|
||||
|
||||
const STRING_COLOR: Color = Color::Green;
|
||||
const KEYWORD_COLOR: Color = Color::Yellow;
|
||||
const PROPERTY_COLOR: Color = Color::Magenta;
|
||||
const OPERATOR_COLOR: Color = Color::TrueColor {
|
||||
r: 214,
|
||||
g: 95,
|
||||
b: 26,
|
||||
};
|
||||
const UNDEFINED_COLOR: Color = Color::TrueColor {
|
||||
r: 100,
|
||||
g: 100,
|
||||
b: 100,
|
||||
};
|
||||
const NUMBER_COLOR: Color = Color::TrueColor {
|
||||
r: 26,
|
||||
g: 214,
|
||||
b: 175,
|
||||
};
|
||||
const IDENTIFIER_COLOR: Color = Color::TrueColor {
|
||||
r: 26,
|
||||
g: 160,
|
||||
b: 214,
|
||||
};
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Completer, Helper, Hinter)]
|
||||
pub(crate) struct RLHelper {
|
||||
highlighter: LineHighlighter,
|
||||
validator: MatchingBracketValidator,
|
||||
}
|
||||
|
||||
impl RLHelper {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
highlighter: LineHighlighter,
|
||||
validator: MatchingBracketValidator::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for RLHelper {
|
||||
fn validate(
|
||||
&self,
|
||||
context: &mut ValidationContext<'_>,
|
||||
) -> Result<ValidationResult, ReadlineError> {
|
||||
self.validator.validate(context)
|
||||
}
|
||||
|
||||
fn validate_while_typing(&self) -> bool {
|
||||
self.validator.validate_while_typing()
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for RLHelper {
|
||||
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
|
||||
hint.into()
|
||||
}
|
||||
|
||||
fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
|
||||
self.highlighter.highlight(line, pos)
|
||||
}
|
||||
|
||||
fn highlight_candidate<'c>(
|
||||
&self,
|
||||
candidate: &'c str,
|
||||
_completion: rustyline::CompletionType,
|
||||
) -> Cow<'c, str> {
|
||||
self.highlighter.highlight(candidate, 0)
|
||||
}
|
||||
|
||||
fn highlight_char(&self, line: &str, _: usize) -> bool {
|
||||
!line.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
static KEYWORDS: Set<&'static str> = phf_set! {
|
||||
"break",
|
||||
"case",
|
||||
"catch",
|
||||
"class",
|
||||
"const",
|
||||
"continue",
|
||||
"default",
|
||||
"delete",
|
||||
"do",
|
||||
"else",
|
||||
"export",
|
||||
"extends",
|
||||
"finally",
|
||||
"for",
|
||||
"function",
|
||||
"if",
|
||||
"import",
|
||||
"instanceof",
|
||||
"new",
|
||||
"return",
|
||||
"super",
|
||||
"switch",
|
||||
"this",
|
||||
"throw",
|
||||
"try",
|
||||
"typeof",
|
||||
"var",
|
||||
"void",
|
||||
"while",
|
||||
"with",
|
||||
"yield",
|
||||
"await",
|
||||
"enum",
|
||||
"let",
|
||||
};
|
||||
|
||||
struct LineHighlighter;
|
||||
|
||||
impl Highlighter for LineHighlighter {
|
||||
fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> {
|
||||
let mut coloured = line.to_string();
|
||||
|
||||
let reg = Regex::new(
|
||||
r#"(?x)
|
||||
(?P<identifier>\b[$_\p{ID_Start}][$_\p{ID_Continue}\u{200C}\u{200D}]*\b) |
|
||||
(?P<string_double_quote>"([^"\\]|\\.)*") |
|
||||
(?P<string_single_quote>'([^'\\]|\\.)*') |
|
||||
(?P<template_literal>`([^`\\]|\\.)*`) |
|
||||
(?P<op>[+\-/*%~^!&|=<>;:]) |
|
||||
(?P<number>0[bB][01](_?[01])*n?|0[oO][0-7](_?[0-7])*n?|0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?|(([0-9](_?[0-9])*\.([0-9](_?[0-9])*)?)|(([0-9](_?[0-9])*)?\.[0-9](_?[0-9])*)|([0-9](_?[0-9])*))([eE][+-]?[0-9](_?[0-9])*)?n?)"#,
|
||||
)
|
||||
.expect("could not compile regular expression");
|
||||
|
||||
coloured = reg
|
||||
.replace_all(&coloured, |caps: &Captures<'_>| {
|
||||
if let Some(cap) = caps.name("identifier") {
|
||||
match cap.as_str() {
|
||||
"true" | "false" | "null" | "Infinity" | "globalThis" => {
|
||||
cap.as_str().color(PROPERTY_COLOR).to_string()
|
||||
}
|
||||
"undefined" => cap.as_str().color(UNDEFINED_COLOR).to_string(),
|
||||
identifier if KEYWORDS.contains(identifier) => {
|
||||
cap.as_str().color(KEYWORD_COLOR).bold().to_string()
|
||||
}
|
||||
_ => cap.as_str().color(IDENTIFIER_COLOR).to_string(),
|
||||
}
|
||||
} else if let Some(cap) = caps.name("string_double_quote") {
|
||||
cap.as_str().color(STRING_COLOR).to_string()
|
||||
} else if let Some(cap) = caps.name("string_single_quote") {
|
||||
cap.as_str().color(STRING_COLOR).to_string()
|
||||
} else if let Some(cap) = caps.name("template_literal") {
|
||||
cap.as_str().color(STRING_COLOR).to_string()
|
||||
} else if let Some(cap) = caps.name("op") {
|
||||
cap.as_str().color(OPERATOR_COLOR).to_string()
|
||||
} else if let Some(cap) = caps.name("number") {
|
||||
cap.as_str().color(NUMBER_COLOR).to_string()
|
||||
} else {
|
||||
caps[0].to_string()
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
|
||||
coloured.into()
|
||||
}
|
||||
}
|
||||
387
javascript-engine/external/boa/boa_cli/src/main.rs
vendored
Normal file
387
javascript-engine/external/boa/boa_cli/src/main.rs
vendored
Normal file
@@ -0,0 +1,387 @@
|
||||
//! A ECMAScript REPL implementation based on boa_engine.
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg"
|
||||
)]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
|
||||
#![warn(missing_docs, clippy::dbg_macro)]
|
||||
#![deny(
|
||||
// rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html
|
||||
warnings,
|
||||
future_incompatible,
|
||||
let_underscore,
|
||||
nonstandard_style,
|
||||
rust_2018_compatibility,
|
||||
rust_2018_idioms,
|
||||
rust_2021_compatibility,
|
||||
unused,
|
||||
|
||||
// rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
|
||||
macro_use_extern_crate,
|
||||
meta_variable_misuse,
|
||||
missing_abi,
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
non_ascii_idents,
|
||||
noop_method_call,
|
||||
single_use_lifetimes,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unreachable_pub,
|
||||
unsafe_op_in_unsafe_fn,
|
||||
unused_crate_dependencies,
|
||||
unused_import_braces,
|
||||
unused_lifetimes,
|
||||
unused_qualifications,
|
||||
unused_tuple_struct_fields,
|
||||
variant_size_differences,
|
||||
|
||||
// rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html
|
||||
rustdoc::broken_intra_doc_links,
|
||||
rustdoc::private_intra_doc_links,
|
||||
rustdoc::missing_crate_level_docs,
|
||||
rustdoc::private_doc_tests,
|
||||
rustdoc::invalid_codeblock_attributes,
|
||||
rustdoc::invalid_rust_codeblocks,
|
||||
rustdoc::bare_urls,
|
||||
|
||||
// clippy categories https://doc.rust-lang.org/clippy/
|
||||
clippy::all,
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::style,
|
||||
clippy::complexity,
|
||||
clippy::perf,
|
||||
clippy::pedantic,
|
||||
clippy::nursery,
|
||||
)]
|
||||
#![allow(clippy::option_if_let_else, clippy::redundant_pub_crate)]
|
||||
|
||||
mod helper;
|
||||
|
||||
use boa_ast::StatementList;
|
||||
use boa_engine::{
|
||||
context::ContextBuilder,
|
||||
job::{JobQueue, NativeJob},
|
||||
vm::flowgraph::{Direction, Graph},
|
||||
Context, JsResult,
|
||||
};
|
||||
use clap::{Parser, ValueEnum, ValueHint};
|
||||
use colored::{Color, Colorize};
|
||||
use rustyline::{config::Config, error::ReadlineError, EditMode, Editor};
|
||||
use std::{cell::RefCell, collections::VecDeque, fs::read, fs::OpenOptions, io, path::PathBuf};
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
|
||||
#[cfg_attr(
|
||||
all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"),
|
||||
global_allocator
|
||||
)]
|
||||
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
||||
|
||||
/// CLI configuration for Boa.
|
||||
static CLI_HISTORY: &str = ".boa_history";
|
||||
|
||||
const READLINE_COLOR: Color = Color::Cyan;
|
||||
|
||||
// Added #[allow(clippy::option_option)] because to StructOpt an Option<Option<T>>
|
||||
// is an optional argument that optionally takes a value ([--opt=[val]]).
|
||||
// https://docs.rs/structopt/0.3.11/structopt/#type-magic
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, version, about, name = "boa")]
|
||||
struct Opt {
|
||||
/// The JavaScript file(s) to be evaluated.
|
||||
#[arg(name = "FILE", value_hint = ValueHint::FilePath)]
|
||||
files: Vec<PathBuf>,
|
||||
|
||||
/// Dump the AST to stdout with the given format.
|
||||
#[arg(
|
||||
long,
|
||||
short = 'a',
|
||||
value_name = "FORMAT",
|
||||
ignore_case = true,
|
||||
value_enum,
|
||||
conflicts_with = "graph"
|
||||
)]
|
||||
#[allow(clippy::option_option)]
|
||||
dump_ast: Option<Option<DumpFormat>>,
|
||||
|
||||
/// Dump the AST to stdout with the given format.
|
||||
#[arg(long, short, conflicts_with = "graph")]
|
||||
trace: bool,
|
||||
|
||||
/// Use vi mode in the REPL
|
||||
#[arg(long = "vi")]
|
||||
vi_mode: bool,
|
||||
|
||||
/// Generate instruction flowgraph. Default is Graphviz.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FORMAT",
|
||||
ignore_case = true,
|
||||
value_enum,
|
||||
group = "graph"
|
||||
)]
|
||||
#[allow(clippy::option_option)]
|
||||
flowgraph: Option<Option<FlowgraphFormat>>,
|
||||
|
||||
/// Specifies the direction of the flowgraph. Default is TopToBottom.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FORMAT",
|
||||
ignore_case = true,
|
||||
value_enum,
|
||||
requires = "graph"
|
||||
)]
|
||||
flowgraph_direction: Option<FlowgraphDirection>,
|
||||
}
|
||||
|
||||
impl Opt {
|
||||
/// Returns whether a dump flag has been used.
|
||||
const fn has_dump_flag(&self) -> bool {
|
||||
self.dump_ast.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
enum DumpFormat {
|
||||
/// The different types of format available for dumping.
|
||||
// NOTE: This can easily support other formats just by
|
||||
// adding a field to this enum and adding the necessary
|
||||
// implementation. Example: Toml, Html, etc.
|
||||
//
|
||||
// NOTE: The fields of this enum are not doc comments because
|
||||
// arg_enum! macro does not support it.
|
||||
|
||||
// This is the default format that you get from std::fmt::Debug.
|
||||
Debug,
|
||||
|
||||
// This is a minified json format.
|
||||
Json,
|
||||
|
||||
// This is a pretty printed json format.
|
||||
JsonPretty,
|
||||
}
|
||||
|
||||
/// Represents the format of the instruction flowgraph.
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum FlowgraphFormat {
|
||||
/// Generates in [graphviz][graphviz] format.
|
||||
///
|
||||
/// [graphviz]: https://graphviz.org/
|
||||
Graphviz,
|
||||
/// Generates in [mermaid][mermaid] format.
|
||||
///
|
||||
/// [mermaid]: https://mermaid-js.github.io/mermaid/#/
|
||||
Mermaid,
|
||||
}
|
||||
|
||||
/// Represents the direction of the instruction flowgraph.
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
enum FlowgraphDirection {
|
||||
TopToBottom,
|
||||
BottomToTop,
|
||||
LeftToRight,
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
/// Parses the the token stream into an AST and returns it.
|
||||
///
|
||||
/// Returns a error of type String with a message,
|
||||
/// if the token stream has a parsing error.
|
||||
fn parse_tokens<S>(src: S, context: &mut Context<'_>) -> Result<StatementList, String>
|
||||
where
|
||||
S: AsRef<[u8]>,
|
||||
{
|
||||
let src_bytes = src.as_ref();
|
||||
boa_parser::Parser::new(src_bytes)
|
||||
.parse_all(context.interner_mut())
|
||||
.map_err(|e| format!("ParsingError: {e}"))
|
||||
}
|
||||
|
||||
/// Dumps the AST to stdout with format controlled by the given arguments.
|
||||
///
|
||||
/// Returns a error of type String with a error message,
|
||||
/// if the source has a syntax or parsing error.
|
||||
fn dump<S>(src: S, args: &Opt, context: &mut Context<'_>) -> Result<(), String>
|
||||
where
|
||||
S: AsRef<[u8]>,
|
||||
{
|
||||
if let Some(ref arg) = args.dump_ast {
|
||||
let ast = parse_tokens(src, context)?;
|
||||
|
||||
match arg {
|
||||
Some(DumpFormat::Json) => println!(
|
||||
"{}",
|
||||
serde_json::to_string(&ast).expect("could not convert AST to a JSON string")
|
||||
),
|
||||
Some(DumpFormat::JsonPretty) => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&ast)
|
||||
.expect("could not convert AST to a pretty JSON string")
|
||||
),
|
||||
Some(DumpFormat::Debug) | None => println!("{ast:#?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_flowgraph(
|
||||
context: &mut Context<'_>,
|
||||
src: &[u8],
|
||||
format: FlowgraphFormat,
|
||||
direction: Option<FlowgraphDirection>,
|
||||
) -> JsResult<String> {
|
||||
let ast = context.parse(src)?;
|
||||
let code = context.compile(&ast)?;
|
||||
|
||||
let direction = match direction {
|
||||
Some(FlowgraphDirection::TopToBottom) | None => Direction::TopToBottom,
|
||||
Some(FlowgraphDirection::BottomToTop) => Direction::BottomToTop,
|
||||
Some(FlowgraphDirection::LeftToRight) => Direction::LeftToRight,
|
||||
Some(FlowgraphDirection::RightToLeft) => Direction::RightToLeft,
|
||||
};
|
||||
|
||||
let mut graph = Graph::new(direction);
|
||||
code.to_graph(context.interner(), graph.subgraph(String::default()));
|
||||
let result = match format {
|
||||
FlowgraphFormat::Graphviz => graph.to_graphviz_format(),
|
||||
FlowgraphFormat::Mermaid => graph.to_mermaid_format(),
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), io::Error> {
|
||||
let args = Opt::parse();
|
||||
|
||||
let queue = Jobs::default();
|
||||
let mut context = ContextBuilder::new().job_queue(&queue).build();
|
||||
|
||||
// Trace Output
|
||||
context.set_trace(args.trace);
|
||||
|
||||
for file in &args.files {
|
||||
let buffer = read(file)?;
|
||||
|
||||
if args.has_dump_flag() {
|
||||
if let Err(e) = dump(&buffer, &args, &mut context) {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
} else if let Some(flowgraph) = args.flowgraph {
|
||||
match generate_flowgraph(
|
||||
&mut context,
|
||||
&buffer,
|
||||
flowgraph.unwrap_or(FlowgraphFormat::Graphviz),
|
||||
args.flowgraph_direction,
|
||||
) {
|
||||
Ok(v) => println!("{v}"),
|
||||
Err(v) => eprintln!("Uncaught {v}"),
|
||||
}
|
||||
} else {
|
||||
match context.eval(&buffer) {
|
||||
Ok(v) => println!("{}", v.display()),
|
||||
Err(v) => eprintln!("Uncaught {v}"),
|
||||
}
|
||||
context.run_jobs();
|
||||
}
|
||||
}
|
||||
|
||||
if args.files.is_empty() {
|
||||
let config = Config::builder()
|
||||
.keyseq_timeout(1)
|
||||
.edit_mode(if args.vi_mode {
|
||||
EditMode::Vi
|
||||
} else {
|
||||
EditMode::Emacs
|
||||
})
|
||||
.build();
|
||||
|
||||
let mut editor =
|
||||
Editor::with_config(config).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
// Check if the history file exists. If it does, create it.
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(CLI_HISTORY)?;
|
||||
editor.load_history(CLI_HISTORY).map_err(|err| match err {
|
||||
ReadlineError::Io(e) => e,
|
||||
e => io::Error::new(io::ErrorKind::Other, e),
|
||||
})?;
|
||||
editor.set_helper(Some(helper::RLHelper::new()));
|
||||
|
||||
let readline = ">> ".color(READLINE_COLOR).bold().to_string();
|
||||
|
||||
loop {
|
||||
match editor.readline(&readline) {
|
||||
Ok(line) if line == ".exit" => break,
|
||||
Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
|
||||
|
||||
Ok(line) => {
|
||||
editor.add_history_entry(&line);
|
||||
|
||||
if args.has_dump_flag() {
|
||||
if let Err(e) = dump(&line, &args, &mut context) {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
} else if let Some(flowgraph) = args.flowgraph {
|
||||
match generate_flowgraph(
|
||||
&mut context,
|
||||
line.trim_end().as_bytes(),
|
||||
flowgraph.unwrap_or(FlowgraphFormat::Graphviz),
|
||||
args.flowgraph_direction,
|
||||
) {
|
||||
Ok(v) => println!("{v}"),
|
||||
Err(v) => eprintln!("Uncaught {v}"),
|
||||
}
|
||||
} else {
|
||||
match context.eval(line.trim_end()) {
|
||||
Ok(v) => {
|
||||
println!("{}", v.display());
|
||||
}
|
||||
Err(v) => {
|
||||
eprintln!("{}: {}", "Uncaught".red(), v.to_string().red());
|
||||
}
|
||||
}
|
||||
context.run_jobs();
|
||||
}
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
eprintln!("Unknown error: {err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor
|
||||
.save_history(CLI_HISTORY)
|
||||
.expect("could not save CLI history");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Jobs(RefCell<VecDeque<NativeJob>>);
|
||||
|
||||
impl JobQueue for Jobs {
|
||||
fn enqueue_promise_job(&self, job: NativeJob, _: &mut Context<'_>) {
|
||||
self.0.borrow_mut().push_front(job);
|
||||
}
|
||||
|
||||
fn run_jobs(&self, context: &mut Context<'_>) {
|
||||
loop {
|
||||
let jobs = std::mem::take(&mut *self.0.borrow_mut());
|
||||
if jobs.is_empty() {
|
||||
return;
|
||||
}
|
||||
for job in jobs {
|
||||
if let Err(e) = job.call(context) {
|
||||
eprintln!("Uncaught {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user