Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CodeBlocker.Test/TemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand Down
12 changes: 6 additions & 6 deletions CodeBlocker/Templates/TemplateBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public abstract class TemplateBase
public string DefaultValue { get; set; } = string.Empty;

/// <summary>
/// Gets or sets a value indicating whether <see cref="DefaultValue"/> 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 <see cref="DefaultValue"/> 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.
/// </summary>
public bool DefaultValueIsQuoted { get; set; }

Expand Down Expand Up @@ -79,8 +79,8 @@ public virtual void WriteTo(CodeBlocker codeBlocker)
}

/// <summary>
/// Writes <c> = value</c> when <see cref="DefaultValue"/> is set, quoting it when
/// <see cref="DefaultValueIsQuoted"/> is set.
/// Writes <c> = value</c> when <see cref="DefaultValue"/> is set, as an escaped string literal
/// when <see cref="DefaultValueIsQuoted"/> is set.
/// </summary>
/// <param name="codeBlocker">The <see cref="CodeBlocker"/> to write to.</param>
protected void WriteDefaultValueTo(CodeBlocker codeBlocker)
Expand All @@ -93,7 +93,7 @@ protected void WriteDefaultValueTo(CodeBlocker codeBlocker)
}

codeBlocker.Write(" = ");
codeBlocker.Write(DefaultValueIsQuoted ? $"\"{DefaultValue}\"" : DefaultValue);
codeBlocker.Write(DefaultValueIsQuoted ? TemplateRendering.QuoteStringLiteral(DefaultValue) : DefaultValue);
}
}

Expand Down
51 changes: 51 additions & 0 deletions CodeBlocker/Templates/TemplateRendering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace ktsu.CodeBlocker.Templates;

using System.Globalization;
using System.Text;

/// <summary>
/// The rendering steps shared by more than one template.
/// </summary>
Expand Down Expand Up @@ -82,6 +85,54 @@ internal static void SpliceFragment(CodeBlocker parent, string fragment)
}
}

/// <summary>
/// Formats raw text as a C# regular string literal, quoted and escaped so that it compiles and
/// evaluates back to exactly <paramref name="value"/>.
/// </summary>
/// <param name="value">The raw text of the string.</param>
/// <returns>The quoted literal.</returns>
/// <remarks>
/// 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 <c>\uXXXX</c> escape.
/// </remarks>
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();
}

/// <summary>
/// Writes a parenthesised, comma-separated parameter list.
/// </summary>
Expand Down
Loading