diff --git a/docs/converter.md b/docs/converter.md
index 2fd1ea136..024c13ef1 100644
--- a/docs/converter.md
+++ b/docs/converter.md
@@ -184,6 +184,26 @@ Verify("the source text", "texttoconvert");
snippet source | anchor
+A target for a text extension can also be built from a stream. The stream is read as text, so a converter can re-emit its source without converting it back to a string:
+
+
+
+```cs
+// A target for a text extension can be built from a stream, eg to re-emit the
+// source of the conversion. The stream is read as text.
+VerifierSettings.RegisterStreamConverter(
+ "streamtoconvert",
+ (_, stream, _) =>
+ new(
+ null,
+ [
+ new("streamtoconvert", stream),
+ new("txt", "derived from text")
+ ]));
+```
+snippet source | anchor
+
+
### Cleanup
diff --git a/docs/mdsource/converter.source.md b/docs/mdsource/converter.source.md
index 0cce35eee..011057741 100644
--- a/docs/mdsource/converter.source.md
+++ b/docs/mdsource/converter.source.md
@@ -62,6 +62,10 @@ snippet: RegisterStreamConverterTextExtension
snippet: TextExtensionConverterVerify
+A target for a text extension can also be built from a stream. The stream is read as text, so a converter can re-emit its source without converting it back to a string:
+
+snippet: RegisterStreamConverterTextExtensionStream
+
### Cleanup
diff --git a/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.streamtoconvert b/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.streamtoconvert
new file mode 100644
index 000000000..925839fe6
--- /dev/null
+++ b/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.streamtoconvert
@@ -0,0 +1 @@
+the source text
\ No newline at end of file
diff --git a/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.txt b/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.txt
new file mode 100644
index 000000000..f925c4ac5
--- /dev/null
+++ b/src/Verify.Tests/Converters/ExtensionConverterTests.TextSplitterStream.verified.txt
@@ -0,0 +1 @@
+derived from text
\ No newline at end of file
diff --git a/src/Verify.Tests/Converters/ExtensionConverterTests.cs b/src/Verify.Tests/Converters/ExtensionConverterTests.cs
index f0c992b7b..c716093e5 100644
--- a/src/Verify.Tests/Converters/ExtensionConverterTests.cs
+++ b/src/Verify.Tests/Converters/ExtensionConverterTests.cs
@@ -31,6 +31,33 @@ public Task TextSplitter() =>
Verify("the source text", "texttoconvert");
#endregion
+ [ModuleInitializer]
+ public static void TextSplitterStreamInit()
+ {
+ FileExtensions.AddTextExtension("streamtoconvert");
+
+ #region RegisterStreamConverterTextExtensionStream
+
+ // A target for a text extension can be built from a stream, eg to re-emit the
+ // source of the conversion. The stream is read as text.
+ VerifierSettings.RegisterStreamConverter(
+ "streamtoconvert",
+ (_, stream, _) =>
+ new(
+ null,
+ [
+ new("streamtoconvert", stream),
+ new("txt", "derived from text")
+ ]));
+
+ #endregion
+ }
+
+ // a conversion splitter for a text extension that re-emits its source stream
+ [Fact]
+ public Task TextSplitterStream() =>
+ Verify("the source text", "streamtoconvert");
+
[ModuleInitializer]
public static void RecursiveInit() =>
VerifierSettings.RegisterStreamConverter(
diff --git a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GetExtensions.verified.txt b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GetExtensions.verified.txt
index 3956841f0..a35b4bf63 100644
--- a/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GetExtensions.verified.txt
+++ b/src/Verify.Tests/InnerVerifyChecksTests/VerifyChecksTests.GetExtensions.verified.txt
@@ -6,6 +6,7 @@
nuspec,
props,
staticComparerExt,
+ streamtoconvert,
texttoconvert,
txt,
xml
diff --git a/src/Verify/IoHelpers.cs b/src/Verify/IoHelpers.cs
index f7f9be26b..cccf73770 100644
--- a/src/Verify/IoHelpers.cs
+++ b/src/Verify/IoHelpers.cs
@@ -160,6 +160,11 @@ public static async Task ReadStringBuilderWithFixedLines(this Str
internal static async Task ReadStringBuilderWithFixedLines(TextReader reader)
{
var contents = await reader.ReadToEndAsync();
+ return contents.ToStringBuilderWithFixedLines();
+ }
+
+ internal static StringBuilder ToStringBuilderWithFixedLines(this string contents)
+ {
var builder = new StringBuilder(contents);
if (contents.Contains('\r'))
{
diff --git a/src/Verify/Target.cs b/src/Verify/Target.cs
index 6ef58f27a..4fe7664f9 100644
--- a/src/Verify/Target.cs
+++ b/src/Verify/Target.cs
@@ -57,23 +57,31 @@ public Target(string extension, Stream data, string? name = null, bool performCo
{
Guards.AgainstBadExtension(extension);
+ Extension = extension;
+ Name = FileNameCleaner.SanitizeFilePath(name);
+ PerformConversion = performConversion;
+
+ // text is always stored as text, so for a text extension the stream is read here.
+ // eg a converter registered against a text extension re-emitting its source stream.
if (FileExtensions.IsTextExtension(extension))
{
- throw new(
- $"""
- Don't pass a stream for text.
- If {extension} is not a text extension then use `FileExtensions.RemoveTextExtensions(\"{extension}\")` at initialization;
- Otherwise use `Target(string extension, string data)` or `Target(string extension, StringBuilder data, string? name)`.
- """);
+ streamData = null;
+ stringBuilderData = ReadText(data);
+ return;
}
- Extension = extension;
- Name = FileNameCleaner.SanitizeFilePath(name);
- PerformConversion = performConversion;
streamData = data;
stringBuilderData = null;
}
+ // the stream is owned by the target, so it is consumed here
+ static StringBuilder ReadText(Stream stream)
+ {
+ stream.MoveToStart();
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd().ToStringBuilderWithFixedLines();
+ }
+
public Target(string extension, StringBuilder data, string? name = null)
{
ValidateExtension(extension);