use crate::import::{ImportEntry, ImportGroup};
use crate::lang::CodeLang;
use crate::lang::config::{
BlockSyntaxConfig, EnumAndAnnotationConfig, FunctionSyntaxConfig, GenericSyntaxConfig,
QuoteStyle, TypeDeclSyntaxConfig, TypePresentationConfig,
};
use crate::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
use crate::type_name::TypePresentation;
#[derive(Debug, Clone)]
pub struct JavaScript {
pub quote_style: QuoteStyle,
pub indent: String,
pub extension: String,
pub semicolons: bool,
}
impl Default for JavaScript {
fn default() -> Self {
Self {
quote_style: QuoteStyle::Single,
indent: " ".to_string(),
extension: "js".to_string(),
semicolons: true,
}
}
}
impl JavaScript {
pub fn new() -> Self {
Self::default()
}
pub fn esm() -> Self {
Self {
extension: "mjs".to_string(),
..Self::default()
}
}
pub fn cjs() -> Self {
Self {
extension: "cjs".to_string(),
..Self::default()
}
}
pub fn with_quote_style(mut self, qs: QuoteStyle) -> Self {
self.quote_style = qs;
self
}
pub fn with_indent(mut self, s: &str) -> Self {
self.indent = s.to_string();
self
}
pub fn with_semicolons(mut self, b: bool) -> Self {
self.semicolons = b;
self
}
pub fn with_extension(mut self, s: &str) -> Self {
self.extension = s.to_string();
self
}
}
#[rustfmt::skip]
const JS_RESERVED: &[&str] = &[
"break", "case", "catch", "class", "const", "continue", "debugger",
"default", "delete", "do", "else", "enum", "export", "extends", "false",
"finally", "for", "function", "if", "import", "in", "instanceof", "new",
"null", "return", "super", "switch", "this", "throw", "true", "try",
"typeof", "var", "void", "while", "with",
"implements", "interface", "let", "package", "private", "protected",
"public", "static", "yield",
"async", "await",
];
impl CodeLang for JavaScript {
fn file_extension(&self) -> &str {
&self.extension
}
fn reserved_words(&self) -> &[&str] {
JS_RESERVED
}
fn render_imports(&self, imports: &ImportGroup) -> String {
let mut lines = Vec::new();
let quote = self.quote_style.char();
let semi = if self.semicolons { ";" } else { "" };
let mut by_module: std::collections::BTreeMap<&str, Vec<&ImportEntry>> =
std::collections::BTreeMap::new();
for entry in &imports.entries {
if entry.is_side_effect {
lines.push(format!("import {quote}{}{quote}{semi}", entry.module));
continue;
}
if entry.is_wildcard {
let alias = super::module_to_alias(&entry.module);
lines.push(format!(
"import * as {} from {quote}{}{quote}{semi}",
alias, entry.module,
));
continue;
}
by_module.entry(&entry.module).or_default().push(entry);
}
for (module, entries) in &by_module {
let mut names: Vec<String> = Vec::new();
for entry in entries {
let spec = if let Some(alias) = &entry.alias {
format!("{} as {}", entry.name, alias)
} else {
entry.name.clone()
};
names.push(spec);
}
names.sort();
if !names.is_empty() {
lines.push(format!(
"import {{ {} }} from {quote}{}{quote}{semi}",
names.join(", "),
module,
));
}
}
lines.join("\n")
}
fn render_string_literal(&self, s: &str) -> String {
match self.quote_style {
QuoteStyle::Single => {
format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'"))
}
QuoteStyle::Double => {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
}
}
fn render_doc_comment(&self, lines: &[&str]) -> String {
if lines.is_empty() {
return String::new();
}
let mut out = String::from("/**\n");
for line in lines {
if line.is_empty() {
out.push_str(" *\n");
} else {
out.push_str(&format!(" * {line}\n"));
}
}
out.push_str(" */");
out
}
fn line_comment_prefix(&self) -> &str {
"//"
}
fn render_visibility(&self, vis: Visibility, ctx: DeclarationContext) -> &str {
match ctx {
DeclarationContext::TopLevel => match vis {
Visibility::Public => "export ",
_ => "",
},
DeclarationContext::Member => "",
}
}
fn function_keyword(&self, ctx: DeclarationContext) -> &str {
match ctx {
DeclarationContext::TopLevel => "function",
DeclarationContext::Member => "",
}
}
fn type_keyword(&self, kind: TypeKind) -> &str {
match kind {
TypeKind::Class | TypeKind::Struct => "class",
TypeKind::Interface | TypeKind::Trait => "class",
TypeKind::Enum => "class",
TypeKind::TypeAlias | TypeKind::Newtype => "class",
}
}
fn methods_inside_type_body(&self, _kind: TypeKind) -> bool {
true
}
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
tuple: TypePresentation::Delimited {
open: "[",
sep: ", ",
close: "]",
},
..Default::default()
}
}
fn generic_syntax(&self) -> GenericSyntaxConfig<'_> {
GenericSyntaxConfig {
constraint_keyword: "",
constraint_separator: "",
context_bound_keyword: "",
..Default::default()
}
}
fn block_syntax(&self) -> BlockSyntaxConfig<'_> {
BlockSyntaxConfig {
indent_unit: &self.indent,
uses_semicolons: self.semicolons,
field_terminator: ";",
..Default::default()
}
}
fn function_syntax(&self) -> FunctionSyntaxConfig<'_> {
FunctionSyntaxConfig {
abstract_keyword: "",
..Default::default()
}
}
fn type_decl_syntax(&self) -> TypeDeclSyntaxConfig<'_> {
TypeDeclSyntaxConfig {
super_type_keyword: " extends ",
..Default::default()
}
}
fn enum_and_annotation(&self) -> EnumAndAnnotationConfig<'_> {
EnumAndAnnotationConfig {
variant_separator: "",
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_extension() {
let js = JavaScript::new();
assert_eq!(js.file_extension(), "js");
}
#[test]
fn test_esm_extension() {
let js = JavaScript::esm();
assert_eq!(js.file_extension(), "mjs");
}
#[test]
fn test_cjs_extension() {
let js = JavaScript::cjs();
assert_eq!(js.file_extension(), "cjs");
}
#[test]
fn test_escape_reserved() {
let js = JavaScript::new();
assert_eq!(js.escape_reserved("class"), "class_");
assert_eq!(js.escape_reserved("async"), "async_");
assert_eq!(js.escape_reserved("yield"), "yield_");
assert_eq!(js.escape_reserved("myVar"), "myVar");
assert_eq!(js.escape_reserved("type"), "type");
assert_eq!(js.escape_reserved("interface"), "interface_");
assert_eq!(js.escape_reserved("any"), "any");
assert_eq!(js.escape_reserved("string"), "string");
}
#[test]
fn test_render_imports_basic() {
let js = JavaScript::new();
let imports = ImportGroup {
entries: vec![ImportEntry {
module: "./utils".into(),
name: "formatDate".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
}],
};
assert_eq!(
js.render_imports(&imports),
"import { formatDate } from './utils';"
);
}
#[test]
fn test_render_imports_no_import_type() {
let js = JavaScript::new();
let imports = ImportGroup {
entries: vec![
ImportEntry {
module: "./models".into(),
name: "User".into(),
alias: None,
is_type_only: true, is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "./models".into(),
name: "createUser".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
],
};
let output = js.render_imports(&imports);
assert_eq!(output, "import { User, createUser } from './models';");
assert!(!output.contains("import type"));
}
#[test]
fn test_render_imports_with_alias() {
let js = JavaScript::new();
let imports = ImportGroup {
entries: vec![ImportEntry {
module: "./other".into(),
name: "User".into(),
alias: Some("OtherUser".into()),
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
}],
};
assert_eq!(
js.render_imports(&imports),
"import { User as OtherUser } from './other';"
);
}
#[test]
fn test_render_imports_multiple_modules() {
let js = JavaScript::new();
let imports = ImportGroup {
entries: vec![
ImportEntry {
module: "./models".into(),
name: "User".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "./utils".into(),
name: "format".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
],
};
let output = js.render_imports(&imports);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "import { User } from './models';");
assert_eq!(lines[1], "import { format } from './utils';");
}
#[test]
fn test_render_imports_no_semicolons() {
let js = JavaScript {
semicolons: false,
..Default::default()
};
let imports = ImportGroup {
entries: vec![ImportEntry {
module: "./utils".into(),
name: "format".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
}],
};
assert_eq!(
js.render_imports(&imports),
"import { format } from './utils'"
);
}
#[test]
fn test_doc_comment_single() {
let js = JavaScript::new();
let doc = js.render_doc_comment(&["A brief description."]);
assert!(doc.starts_with("/**\n"));
assert!(doc.contains(" * A brief description.\n"));
assert!(doc.ends_with(" */"));
}
#[test]
fn test_doc_comment_multi() {
let js = JavaScript::new();
let doc = js.render_doc_comment(&["Get user.", "", "Returns null if not found."]);
assert!(doc.contains(" * Get user.\n"));
assert!(doc.contains(" *\n"));
assert!(doc.contains(" * Returns null if not found.\n"));
}
#[test]
fn test_string_literal_single_quotes() {
let js = JavaScript::new();
assert_eq!(js.render_string_literal("hello"), "'hello'");
assert_eq!(js.render_string_literal("it's"), "'it\\'s'");
}
#[test]
fn test_string_literal_double_quotes() {
let js = JavaScript::new().with_quote_style(QuoteStyle::Double);
assert_eq!(js.render_string_literal("hello"), "\"hello\"");
}
#[test]
fn test_javascript_builder_fluent() {
let js = JavaScript::new()
.with_semicolons(false)
.with_quote_style(QuoteStyle::Double)
.with_extension("mjs")
.with_indent(" ");
assert!(!js.block_syntax().uses_semicolons);
assert_eq!(js.file_extension(), "mjs");
assert_eq!(js.block_syntax().indent_unit, " ");
assert_eq!(js.render_string_literal("hi"), "\"hi\"");
}
#[test]
fn test_visibility_top_level() {
let js = JavaScript::new();
assert_eq!(
js.render_visibility(Visibility::Public, DeclarationContext::TopLevel),
"export "
);
assert_eq!(
js.render_visibility(Visibility::Private, DeclarationContext::TopLevel),
""
);
}
#[test]
fn test_visibility_member() {
let js = JavaScript::new();
assert_eq!(
js.render_visibility(Visibility::Public, DeclarationContext::Member),
""
);
assert_eq!(
js.render_visibility(Visibility::Private, DeclarationContext::Member),
""
);
assert_eq!(
js.render_visibility(Visibility::Protected, DeclarationContext::Member),
""
);
}
#[test]
fn test_no_abstract() {
let js = JavaScript::new();
assert_eq!(js.function_syntax().abstract_keyword, "");
}
#[test]
fn test_no_implements() {
let js = JavaScript::new();
assert_eq!(js.type_decl_syntax().implements_keyword, "");
}
#[test]
fn test_type_keyword() {
let js = JavaScript::new();
assert_eq!(js.type_keyword(TypeKind::Class), "class");
assert_eq!(js.type_keyword(TypeKind::Struct), "class");
assert_eq!(js.type_keyword(TypeKind::Interface), "class");
assert_eq!(js.type_keyword(TypeKind::Enum), "class");
}
}