diff --git a/CodeBlocker.Test/TemplateTests.cs b/CodeBlocker.Test/TemplateTests.cs
index 4b809ef..b517a72 100644
--- a/CodeBlocker.Test/TemplateTests.cs
+++ b/CodeBlocker.Test/TemplateTests.cs
@@ -37,6 +37,17 @@ public void ParameterQuotesItsDefaultValueWhenAsked() =>
"string name = \"none\"",
Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = "none", DefaultValueIsQuoted = true }));
+ [TestMethod]
+ [DataRow(@"C:\temp", @"""C:\\temp""", DisplayName = "Backslash")]
+ [DataRow("say \"hi\"", @"""say \""hi\""""", DisplayName = "Quote")]
+ [DataRow("one\ntwo\r\n", @"""one\ntwo\r\n""", DisplayName = "Newline")]
+ [DataRow("tab\there\0", @"""tab\there\0""", DisplayName = "Tab and null")]
+ [DataRow("bell\u0007 sep\u2028", @"""bell\a sep\u2028""", DisplayName = "Other control and separator")]
+ public void ParameterEscapesAQuotedDefaultValue(string value, string expectedLiteral) =>
+ Assert.AreEqual(
+ $"string name = {expectedLiteral}",
+ Render(new ParameterTemplate { Type = "string", Name = "name", DefaultValue = value, DefaultValueIsQuoted = true }));
+
[TestMethod]
public void ParameterKeepsItsAttributesOnTheDeclarationLine()
{
@@ -59,6 +70,12 @@ public void FieldWritesItsInitialiser() =>
"private const int Max = 10;\n",
Render(new FieldTemplate { Type = "int", Name = "Max", Keywords = { "private", "const" }, DefaultValue = "10" }));
+ [TestMethod]
+ public void FieldEscapesAQuotedInitialiser() =>
+ Assert.AreEqual(
+ "private string p = \"C:\\\\temp\\\\\\\"x\\\"\";\n",
+ Render(new FieldTemplate { Type = "string", Name = "p", Keywords = { "private" }, DefaultValue = "C:\\temp\\\"x\"", DefaultValueIsQuoted = true }));
+
[TestMethod]
public void FieldCommentsAndAttributesGoOnTheirOwnLines()
{
diff --git a/CodeBlocker/Templates/TemplateBase.cs b/CodeBlocker/Templates/TemplateBase.cs
index 42349ac..ee31222 100644
--- a/CodeBlocker/Templates/TemplateBase.cs
+++ b/CodeBlocker/Templates/TemplateBase.cs
@@ -27,9 +27,9 @@ public abstract class TemplateBase
public string DefaultValue { get; set; } = string.Empty;
///
- /// Gets or sets a value indicating whether is written inside double
- /// quotes. Set this for string literals; leave it clear for anything already written as an
- /// expression.
+ /// Gets or sets a value indicating whether is written as a string
+ /// literal. Set this to pass the raw text of a string, which is escaped and quoted; leave it clear
+ /// for anything already written as an expression.
///
public bool DefaultValueIsQuoted { get; set; }
@@ -79,8 +79,8 @@ public virtual void WriteTo(CodeBlocker codeBlocker)
}
///
- /// Writes = value when is set, quoting it when
- /// is set.
+ /// Writes = value when is set, as an escaped string literal
+ /// when is set.
///
/// The to write to.
protected void WriteDefaultValueTo(CodeBlocker codeBlocker)
@@ -93,7 +93,7 @@ protected void WriteDefaultValueTo(CodeBlocker codeBlocker)
}
codeBlocker.Write(" = ");
- codeBlocker.Write(DefaultValueIsQuoted ? $"\"{DefaultValue}\"" : DefaultValue);
+ codeBlocker.Write(DefaultValueIsQuoted ? TemplateRendering.QuoteStringLiteral(DefaultValue) : DefaultValue);
}
}
diff --git a/CodeBlocker/Templates/TemplateRendering.cs b/CodeBlocker/Templates/TemplateRendering.cs
index c54a2c4..3fe9451 100644
--- a/CodeBlocker/Templates/TemplateRendering.cs
+++ b/CodeBlocker/Templates/TemplateRendering.cs
@@ -2,6 +2,9 @@
namespace ktsu.CodeBlocker.Templates;
+using System.Globalization;
+using System.Text;
+
///
/// The rendering steps shared by more than one template.
///
@@ -82,6 +85,54 @@ internal static void SpliceFragment(CodeBlocker parent, string fragment)
}
}
+ ///
+ /// Formats raw text as a C# regular string literal, quoted and escaped so that it compiles and
+ /// evaluates back to exactly .
+ ///
+ /// The raw text of the string.
+ /// The quoted literal.
+ ///
+ /// Backslashes, quotes and the common control characters take their short escapes; any other
+ /// control character, and the line and paragraph separators the compiler also treats as line
+ /// breaks, take a \uXXXX escape.
+ ///
+ internal static string QuoteStringLiteral(string value)
+ {
+ StringBuilder literal = new(value.Length + 2);
+ literal.Append('"');
+ foreach (char c in value)
+ {
+ string? escape = c switch
+ {
+ '\\' => @"\\",
+ '"' => "\\\"",
+ '\0' => @"\0",
+ '\a' => @"\a",
+ '\b' => @"\b",
+ '\f' => @"\f",
+ '\n' => @"\n",
+ '\r' => @"\r",
+ '\t' => @"\t",
+ '\v' => @"\v",
+ _ when char.IsControl(c) || c is '\u2028' or '\u2029' =>
+ @"\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture),
+ _ => null,
+ };
+
+ if (escape is null)
+ {
+ literal.Append(c);
+ }
+ else
+ {
+ literal.Append(escape);
+ }
+ }
+
+ literal.Append('"');
+ return literal.ToString();
+ }
+
///
/// Writes a parenthesised, comma-separated parameter list.
///