From ad574136e8a0e81d9aa2d6ba28d32ea023a8baae Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 2 Oct 2025 21:30:40 -0400 Subject: [PATCH 01/12] Add failing tests --- src/lib.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 52b24ae..7bd419e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -324,3 +324,26 @@ testcase! { return template_UUID(``, { eval() { return eval(arguments[0]) } }); }"# } + +testcase! { + extraneous_indentation_strip, + r#"let x = "#, + r#"import { template as template_UUID } from "@ember/template-compiler"; + let x = template_UUID(`hello`, { eval() { return eval(arguments[0])} });"# +} + +testcase! { + multiline_extraneous_indentation_strip, + r#"let x = "#, + r#"import { template as template_UUID } from "@ember/template-compiler"; + let x = template_UUID(`hello\n\nextra line break\n
\n content\n
`, { eval() { return eval(arguments[0])} });"# +} From 20234b48a527ae5fdd513c12324c2ea9f26a399c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:16:14 -0500 Subject: [PATCH 02/12] Implement RFC#1121 --- src/lib.rs | 7 +- src/transform.rs | 214 +++++++++++++++++++++++++++++++++++++- test/node/process.test.js | 184 ++++++++++++++++++++++++++++++++ 3 files changed, 403 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7bd419e..f0f9377 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -345,5 +345,10 @@ testcase! { "#, r#"import { template as template_UUID } from "@ember/template-compiler"; - let x = template_UUID(`hello\n\nextra line break\n
\n content\n
`, { eval() { return eval(arguments[0])} });"# + let x = template_UUID(`hello + +extra line break +
+ content +
`, { eval() { return eval(arguments[0])} });"# } diff --git a/src/transform.rs b/src/transform.rs index d171af8..e2ebe03 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -54,13 +54,14 @@ impl<'a> TransformVisitor<'a> { } fn content_literal(&self, contents: &Box) -> ExprOrSpread { + let stripped_content = strip_indent(&contents.value); Box::new(Expr::Tpl(Tpl { span: contents.span, exprs: vec![], quasis: vec![TplElement { span: contents.span, cooked: None, - raw: escape_template_literal(&contents.value), + raw: escape_template_literal(&stripped_content.into()), tail: false, }], })) @@ -76,6 +77,78 @@ fn escape_template_literal(input: &Atom) -> Atom { .into() } +// https://rfcs.emberjs.com/id/1121-extraneous-invisible-character-stripping/ +fn strip_indent(input: &str) -> String { + let lines: Vec<&str> = input.lines().collect(); + + if lines.is_empty() { + return String::new(); + } + + let first_non_empty = lines.iter().position(|line| !line.trim().is_empty()); + let last_non_empty = lines.iter().rposition(|line| !line.trim().is_empty()); + + let (first_idx, last_idx) = match (first_non_empty, last_non_empty) { + (Some(first), Some(last)) => (first, last), + _ => return String::new(), // All lines are empty + }; + + // Get the trimmed lines (removing leading/trailing empty lines) + let trimmed_lines = &lines[first_idx..=last_idx]; + + let mut min_indent: Option = None; + let mut uses_spaces = false; + let mut uses_tabs = false; + + for line in trimmed_lines { + if line.trim().is_empty() { + continue; + } + + let leading_whitespace: String = line.chars() + .take_while(|c| *c == ' ' || *c == '\t') + .collect(); + + let indent_count = leading_whitespace.len(); + + if leading_whitespace.contains(' ') { + uses_spaces = true; + } + if leading_whitespace.contains('\t') { + uses_tabs = true; + } + + min_indent = Some(match min_indent { + None => indent_count, + Some(current) => current.min(indent_count), + }); + } + + if uses_spaces && uses_tabs { + return trimmed_lines.join("\n"); + } + + let min_indent = min_indent.unwrap_or(0); + + if min_indent == 0 { + return trimmed_lines.join("\n"); + } + + let stripped_lines: Vec = trimmed_lines + .iter() + .map(|line| { + if line.trim().is_empty() { + String::new() + } else { + let chars: Vec = line.chars().collect(); + chars.iter().skip(min_indent).collect() + } + }) + .collect(); + + stripped_lines.join("\n") +} + impl<'a> VisitMut for TransformVisitor<'a> { fn visit_mut_expr(&mut self, n: &mut Expr) { n.visit_mut_children_with(self); @@ -301,3 +374,142 @@ test!( r#"let x = "#, r#"let x = template(`Hello\\nWorld\\u1234`, { eval() { return eval(arguments[0]) }})"# ); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + strips_leading_trailing_whitespace, + r#"let x = "#, + r#"let x = template(`Hello`, { eval() { return eval(arguments[0]) }})"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + strips_common_indentation, + r#"let x = "#, + r#"let x = template(`
+ Hello +
`, { eval() { return eval(arguments[0]) }})"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + strips_indentation_multiline, + r#""#, + r#"export default template(`Hello +there. +

+ how are you +

`, { eval() { return eval(arguments[0]) }},);"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + class_member_strips_indentation, + r#"class X { + +}"#, + r#"class X { + static { + template(`Hello`, { component: this, eval() { return eval(arguments[0]) }},); + } +}"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + deeply_nested_indentation, + r#"class Component { + method() { + return ; + } +}"#, + r#"class Component { + method() { + return template(`
+ Nested +
`, { eval() { return eval(arguments[0]) }}); + } +}"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + preserves_internal_indentation, + r#"let x = "#, + r#"let x = template(`
+
+    some code
+      with indentation
+  
+
`, { eval() { return eval(arguments[0]) }})"# +); + +test!( + Default::default(), + |_| as_folder(TransformVisitor::new( + &Ident::new("template".into(), Default::default()), + None, + )), + opt_out_with_comment, + r#"let x = "#, + r#"let x = template(` +{{!-- prevent automatic de-indent --}} +
+      content here
+    
+ `, { eval() { return eval(arguments[0]) }})"# +); diff --git a/test/node/process.test.js b/test/node/process.test.js index 6e8a005..2dd1b85 100644 --- a/test/node/process.test.js +++ b/test/node/process.test.js @@ -108,4 +108,188 @@ describe(`process`, function () { let output = p.process(`class X { declare a: string; }`); expect(output.code).to.match(/declare a: string/); }); + + describe("indentation stripping (RFC #1121)", function () { + it("strips leading and trailing whitespace from simple template", function () { + let output = p.process(``); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + export default template_UUID(\`Hello\`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); + + it("strips common indentation from nested templates", function () { + let input = ` + class Foo extends Component { + + } + `; + + let output = p.process(input); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + class Foo extends Component { + static{ + template_UUID(\`
+ {{this.greeting}} +
\`, { + component: this, + eval () { + return eval(arguments[0]); + } + }); + } + };`, + ); + }); + + it("strips indentation from multiline content", function () { + let input = ``; + + let output = p.process(input); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + export default template_UUID(\`Hello +there. +

+ how are you +

\`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); + + it("preserves relative indentation within template", function () { + let input = ` + let x = + `; + + let output = p.process(input); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + let x = template_UUID(\`
+
+    some code
+      with indentation
+  
+
\`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); + + it("allows opt-out with comment", function () { + let input = `let x = `; + + let output = p.process(input); + + // When opted out, original whitespace is preserved + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + let x = template_UUID(\` +{{!-- prevent automatic de-indent --}} +
+      content here
+    
+ \`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); + + it("handles deeply nested indentation", function () { + let input = ` + class Component { + method() { + return ; + } + } + `; + + let output = p.process(input); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + class Component { + method() { + return template_UUID(\`
+ Nested +
\`, { + eval () { + return eval(arguments[0]); + } + }); + } + }`, + ); + }); + + it("works with template expressions", function () { + let input = `const MyTemplate = + +`; + + let output = p.process(input); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + const MyTemplate = template_UUID(\`x\`, { + eval () { + return eval(arguments[0]); + } + }); + export default template_UUID(\`\`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); + }); }); From cfd9770edba892d178ee01b841a751d1c0e92c47 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:23:32 -0500 Subject: [PATCH 03/12] Update src/transform.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transform.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index e2ebe03..38d26eb 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -140,8 +140,7 @@ fn strip_indent(input: &str) -> String { if line.trim().is_empty() { String::new() } else { - let chars: Vec = line.chars().collect(); - chars.iter().skip(min_indent).collect() + line.chars().skip(min_indent).collect() } }) .collect(); From 3cdc1158914744efb53819059bd0e01a9f4b29d1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:23:50 -0500 Subject: [PATCH 04/12] Update src/transform.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transform.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 38d26eb..a384e25 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -105,17 +105,17 @@ fn strip_indent(input: &str) -> String { continue; } - let leading_whitespace: String = line.chars() - .take_while(|c| *c == ' ' || *c == '\t') - .collect(); - - let indent_count = leading_whitespace.len(); - - if leading_whitespace.contains(' ') { - uses_spaces = true; - } - if leading_whitespace.contains('\t') { - uses_tabs = true; + let mut indent_count = 0; + for c in line.chars() { + if c == ' ' { + uses_spaces = true; + indent_count += 1; + } else if c == '\t' { + uses_tabs = true; + indent_count += 1; + } else { + break; + } } min_indent = Some(match min_indent { From 1644b961ae03473cfd6db4766b0377f339584130 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:28:45 -0500 Subject: [PATCH 05/12] Fix test that forgot that indentation stripping is separate from leading/trailing newline stripping --- src/transform.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index a384e25..9861e95 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -505,10 +505,8 @@ test!( content here "#, - r#"let x = template(` -{{!-- prevent automatic de-indent --}} + r#"let x = template(`{{!-- prevent automatic de-indent --}}
       content here
-    
- `, { eval() { return eval(arguments[0]) }})"# + `, { eval() { return eval(arguments[0]) }})"# ); From 43b7dc76cab4875ed84d02aacd35fae98d84fb4f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:33:57 -0500 Subject: [PATCH 06/12] Don't lose significant empty lines --- src/transform.rs | 2 +- test/node/process.test.js | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 9861e95..cb0e1d2 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -138,7 +138,7 @@ fn strip_indent(input: &str) -> String { .iter() .map(|line| { if line.trim().is_empty() { - String::new() + line.to_string() } else { line.chars().skip(min_indent).collect() } diff --git a/test/node/process.test.js b/test/node/process.test.js index 2dd1b85..8b46900 100644 --- a/test/node/process.test.js +++ b/test/node/process.test.js @@ -222,12 +222,10 @@ describe(`process`, function () { // When opted out, original whitespace is preserved expect(normalizeOutput(output.code)).to.equalCode( `import { template as template_UUID } from "@ember/template-compiler"; - let x = template_UUID(\` -{{!-- prevent automatic de-indent --}} + let x = template_UUID(\`{{!-- prevent automatic de-indent --}}
       content here
-    
- \`, { + \`, { eval () { return eval(arguments[0]); } From e79ff804b6f2cc021378b052f2ccc373400c7b28 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:39:31 -0500 Subject: [PATCH 07/12] Update src/transform.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/transform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transform.rs b/src/transform.rs index cb0e1d2..5866eb0 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -77,7 +77,7 @@ fn escape_template_literal(input: &Atom) -> Atom { .into() } -// https://rfcs.emberjs.com/id/1121-extraneous-invisible-character-stripping/ +// RFC: https://github.com/emberjs/rfcs/pull/1121 fn strip_indent(input: &str) -> String { let lines: Vec<&str> = input.lines().collect(); From ed6d660f0552b9b1ba89dfe84be83a0507f6c2ea Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:42:41 -0500 Subject: [PATCH 08/12] cleanup --- src/transform.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 5866eb0..2f23dfc 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -90,10 +90,9 @@ fn strip_indent(input: &str) -> String { let (first_idx, last_idx) = match (first_non_empty, last_non_empty) { (Some(first), Some(last)) => (first, last), - _ => return String::new(), // All lines are empty + _ => return String::new(), }; - // Get the trimmed lines (removing leading/trailing empty lines) let trimmed_lines = &lines[first_idx..=last_idx]; let mut min_indent: Option = None; From 6d852c976021e535b27ca5d7256a8c84454bcea5 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:14:13 -0500 Subject: [PATCH 09/12] =?UTF-8?q?Cleanup=20--=20account=20for=20case=20not?= =?UTF-8?q?=20covered=20by=20the=20RFC=20=F0=9F=99=88=20(sorta,=20it's=20a?= =?UTF-8?q?=20little=20ambiguous,=20oops)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/transform.rs | 72 +++++++++++++++++---------------------- test/node/process.test.js | 13 +++++++ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 2f23dfc..98f9ee2 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -77,74 +77,64 @@ fn escape_template_literal(input: &Atom) -> Atom { .into() } -// RFC: https://github.com/emberjs/rfcs/pull/1121 fn strip_indent(input: &str) -> String { - let lines: Vec<&str> = input.lines().collect(); + let mut lines: Vec<&str> = input.lines().collect(); if lines.is_empty() { return String::new(); } - let first_non_empty = lines.iter().position(|line| !line.trim().is_empty()); - let last_non_empty = lines.iter().rposition(|line| !line.trim().is_empty()); + if lines.len() == 1 { + return lines[0].to_string(); + } - let (first_idx, last_idx) = match (first_non_empty, last_non_empty) { - (Some(first), Some(last)) => (first, last), - _ => return String::new(), - }; + while lines.first().is_some_and(|l| l.trim().is_empty()) { + lines.remove(0); + } + while lines.last().is_some_and(|l| l.trim().is_empty()) { + lines.pop(); + } - let trimmed_lines = &lines[first_idx..=last_idx]; + if lines.is_empty() { + return String::new(); + } let mut min_indent: Option = None; - let mut uses_spaces = false; - let mut uses_tabs = false; + let mut has_spaces = false; + let mut has_tabs = false; - for line in trimmed_lines { - if line.trim().is_empty() { + for line in &lines { + let content = line.trim_start(); + if content.is_empty() { continue; } - let mut indent_count = 0; - for c in line.chars() { - if c == ' ' { - uses_spaces = true; - indent_count += 1; - } else if c == '\t' { - uses_tabs = true; - indent_count += 1; - } else { - break; - } - } + let indent_size = line.len() - content.len(); + let indent_chars = &line[..indent_size]; - min_indent = Some(match min_indent { - None => indent_count, - Some(current) => current.min(indent_count), - }); - } + has_spaces |= indent_chars.contains(' '); + has_tabs |= indent_chars.contains('\t'); - if uses_spaces && uses_tabs { - return trimmed_lines.join("\n"); + min_indent = Some(min_indent.map_or(indent_size, |current| current.min(indent_size))); } let min_indent = min_indent.unwrap_or(0); - if min_indent == 0 { - return trimmed_lines.join("\n"); + if (has_spaces && has_tabs) || min_indent == 0 { + return lines.join("\n"); } - let stripped_lines: Vec = trimmed_lines + lines .iter() .map(|line| { - if line.trim().is_empty() { - line.to_string() + if line.len() >= min_indent { + &line[min_indent..] } else { - line.chars().skip(min_indent).collect() + line } }) - .collect(); - - stripped_lines.join("\n") + .collect::>() + .join("\n") } impl<'a> VisitMut for TransformVisitor<'a> { diff --git a/test/node/process.test.js b/test/node/process.test.js index 8b46900..6e875fa 100644 --- a/test/node/process.test.js +++ b/test/node/process.test.js @@ -289,5 +289,18 @@ describe(`process`, function () { });`, ); }); + + it("prerves whitespace when component is one line", function () { + let output = p.process(``); + + expect(normalizeOutput(output.code)).to.equalCode( + `import { template as template_UUID } from "@ember/template-compiler"; + export default template_UUID(\` Hello \`, { + eval () { + return eval(arguments[0]); + } + });`, + ); + }); }); }); From f7cd15d361510df2935a3d64ddc1a44952783844 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:18:33 -0500 Subject: [PATCH 10/12] Cleanup --- src/transform.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 98f9ee2..4d12b43 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -80,12 +80,8 @@ fn escape_template_literal(input: &Atom) -> Atom { fn strip_indent(input: &str) -> String { let mut lines: Vec<&str> = input.lines().collect(); - if lines.is_empty() { - return String::new(); - } - - if lines.len() == 1 { - return lines[0].to_string(); + if lines.len() <= 1 { + return input.to_string(); } while lines.first().is_some_and(|l| l.trim().is_empty()) { From 0c08fa9f0e6e79e63f114c65f0c48df1d4f9f2d2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:22:55 -0500 Subject: [PATCH 11/12] How about this --- src/transform.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/transform.rs b/src/transform.rs index 4d12b43..d7debd8 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -78,28 +78,26 @@ fn escape_template_literal(input: &Atom) -> Atom { } fn strip_indent(input: &str) -> String { - let mut lines: Vec<&str> = input.lines().collect(); + let lines: Vec<&str> = input.lines().collect(); if lines.len() <= 1 { return input.to_string(); } - while lines.first().is_some_and(|l| l.trim().is_empty()) { - lines.remove(0); - } - while lines.last().is_some_and(|l| l.trim().is_empty()) { - lines.pop(); - } + let start = lines.iter().position(|l| !l.trim().is_empty()).unwrap_or(lines.len()); + let end = lines.iter().rposition(|l| !l.trim().is_empty()).map(|i| i + 1).unwrap_or(0); - if lines.is_empty() { + if start >= end { return String::new(); } + let lines = &lines[start..end]; + let mut min_indent: Option = None; let mut has_spaces = false; let mut has_tabs = false; - for line in &lines { + for line in lines { let content = line.trim_start(); if content.is_empty() { continue; From a087571862bb07c762f9c88bc4afcbd753a91959 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 24 Nov 2025 02:27:59 -0500 Subject: [PATCH 12/12] Early return --- src/transform.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/transform.rs b/src/transform.rs index d7debd8..e392824 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -109,12 +109,16 @@ fn strip_indent(input: &str) -> String { has_spaces |= indent_chars.contains(' '); has_tabs |= indent_chars.contains('\t'); + if has_spaces && has_tabs { + return lines.join("\n"); + } + min_indent = Some(min_indent.map_or(indent_size, |current| current.min(indent_size))); } let min_indent = min_indent.unwrap_or(0); - if (has_spaces && has_tabs) || min_indent == 0 { + if min_indent == 0 { return lines.join("\n"); }