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);