Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IBufferWriter support back #62

Closed
Closed
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
285 changes: 285 additions & 0 deletions src/RazorSlices/BufferWriterHtmlExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
using System.Buffers.Text;
using System.Diagnostics;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.Razor.TagHelpers;
using RazorSlices;

namespace System.Buffers;

internal static class BufferWriterHtmlExtensions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How similar is this to the PipeWriter extensions? If it's basically the same then I'd want to figure out a way to avoid all the duplication, e.g. use T4.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recovered the code that was before the move to PipeWriter. Is seems 95% identical. And maybe some differences are just small refactoring that can also be applied in IBuffer.

There is also Utf8BufferTextWriter that will be almost a copy-paste.

{
public static void HtmlEncodeAndWriteUtf8(this IBufferWriter<byte> bufferWriter, ReadOnlySpan<byte> utf8Text, HtmlEncoder htmlEncoder)
{
if (utf8Text.Length == 0)
{
return;
}

if (htmlEncoder == NullHtmlEncoder.Default)
{
// No HTML encoding required
bufferWriter.Write(utf8Text);
return;
}

Span<byte> writerSpan = default;
var encodeStatus = OperationStatus.Done;
var waitingToAdvance = 0;

while (utf8Text.Length > 0)
{
if (writerSpan.Length == 0)
{
if (waitingToAdvance > 0)
{
bufferWriter.Advance(waitingToAdvance);
waitingToAdvance = 0;
}
// Allow space for HTML encoding the string
var spanSizeHint = BufferSizes.GetHtmlEncodedSizeHint(utf8Text.Length);
writerSpan = bufferWriter.GetSpan(spanSizeHint);
}

// Encode to buffer
encodeStatus = htmlEncoder.EncodeUtf8(utf8Text, writerSpan, out var bytesConsumed, out var bytesWritten);
waitingToAdvance += bytesWritten;

if (utf8Text.Length - bytesConsumed == 0)
{
break;
}

utf8Text = utf8Text[bytesConsumed..];
writerSpan = writerSpan[bytesWritten..];
}

if (waitingToAdvance > 0)
{
bufferWriter.Advance(waitingToAdvance);
}

Debug.Assert(encodeStatus == OperationStatus.Done, "Bad math in IBufferWriter HTML writing extensions");
}

public static void HtmlEncodeAndWriteSpanFormattable<T>(this IBufferWriter<byte> bufferWriter, T? formattable, HtmlEncoder htmlEncoder, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = null)
where T : ISpanFormattable
{
if (formattable is null)
{
return;
}

if (TryHtmlEncodeAndWriteSpanFormattableSmall(bufferWriter, formattable, htmlEncoder, format, formatProvider))
{
return;
}

var bufferSize = BufferSizes.SmallFormattableWriteCharSize * 2;
var rentedBuffer = ArrayPool<char>.Shared.Rent(bufferSize);
int charsWritten;

while (!formattable.TryFormat(rentedBuffer, out charsWritten, format, formatProvider))
{
// Buffer was too small, return the current buffer and rent a new buffer twice the size
bufferSize = rentedBuffer.Length * 2;
ArrayPool<char>.Shared.Return(rentedBuffer);
rentedBuffer = ArrayPool<char>.Shared.Rent(bufferSize);
}

HtmlEncodeAndWrite(bufferWriter, rentedBuffer.AsSpan()[..charsWritten], htmlEncoder);
ArrayPool<char>.Shared.Return(rentedBuffer);
}

private static bool TryHtmlEncodeAndWriteSpanFormattableSmall<T>(IBufferWriter<byte> bufferWriter, T formattable, HtmlEncoder htmlEncoder, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = null)
where T : ISpanFormattable
{
Span<char> buffer = stackalloc char[BufferSizes.SmallFormattableWriteCharSize];
if (formattable.TryFormat(buffer, out var charsWritten, format, formatProvider))
{
HtmlEncodeAndWrite(bufferWriter, buffer[..charsWritten], htmlEncoder);
return true;
}
return false;
}

#if NET8_0_OR_GREATER
public static void HtmlEncodeAndWriteUtf8SpanFormattable<T>(this IBufferWriter<byte> bufferWriter, T? formattable, HtmlEncoder htmlEncoder, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = null)
where T : IUtf8SpanFormattable
{
if (formattable is null)
{
return;
}

if (TryHtmlEncodeAndWriteUtf8SpanFormattableSmall(bufferWriter, formattable, htmlEncoder, format, formatProvider))
{
return;
}

var bufferSize = BufferSizes.SmallFormattableWriteByteSize * 2;
var rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
int bytesWritten;

while (!formattable.TryFormat(rentedBuffer, out bytesWritten, format, formatProvider))
{
// Buffer was too small, return the current buffer and rent a new buffer twice the size
bufferSize = rentedBuffer.Length * 2;
ArrayPool<byte>.Shared.Return(rentedBuffer);
rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
}

HtmlEncodeAndWriteUtf8(bufferWriter, rentedBuffer.AsSpan()[..bytesWritten], htmlEncoder);
ArrayPool<byte>.Shared.Return(rentedBuffer);
}

private static bool TryHtmlEncodeAndWriteUtf8SpanFormattableSmall<T>(IBufferWriter<byte> bufferWriter, T formattable, HtmlEncoder htmlEncoder, ReadOnlySpan<char> format = default, IFormatProvider? formatProvider = null)
where T : IUtf8SpanFormattable
{
Span<byte> buffer = stackalloc byte[BufferSizes.SmallFormattableWriteByteSize];
if (formattable.TryFormat(buffer, out var bytesWritten, format, formatProvider))
{
HtmlEncodeAndWriteUtf8(bufferWriter, buffer[..bytesWritten], htmlEncoder);
return true;
}
return false;
}
#endif

public static void HtmlEncodeAndWrite(this IBufferWriter<byte> bufferWriter, ReadOnlySpan<char> textSpan, HtmlEncoder htmlEncoder)
{
if (textSpan.Length == 0)
{
return;
}

if (htmlEncoder == NullHtmlEncoder.Default)
{
// No HTML encoding required
WriteHtml(bufferWriter, textSpan);
return;
}

if (textSpan.Length <= BufferSizes.SmallTextWriteCharSize)
{
HtmlEncodeAndWriteSmall(bufferWriter, textSpan, htmlEncoder);
return;
}

var sizeHint = BufferSizes.GetHtmlEncodedSizeHint(textSpan.Length);
var rentedBuffer = ArrayPool<char>.Shared.Rent(sizeHint);
Span<char> bufferSpan = rentedBuffer;
var waitingToWrite = 0;
var encodeStatus = OperationStatus.Done;

while (textSpan.Length > 0)
{
if (bufferSpan.Length == 0)
{
if (waitingToWrite > 0)
{
WriteHtml(bufferWriter, rentedBuffer.AsSpan()[..waitingToWrite]);
waitingToWrite = 0;
bufferSpan = rentedBuffer;
}
}

// Encode to rented buffer
encodeStatus = htmlEncoder.Encode(textSpan, bufferSpan, out var charsConsumed, out var charsWritten);
waitingToWrite += charsWritten;

if (textSpan.Length - charsConsumed == 0)
{
break;
}

textSpan = textSpan[charsConsumed..];
bufferSpan = bufferSpan[charsWritten..];
}

if (waitingToWrite > 0)
{
WriteHtml(bufferWriter, rentedBuffer.AsSpan()[..waitingToWrite]);
}

ArrayPool<char>.Shared.Return(rentedBuffer);

Debug.Assert(encodeStatus == OperationStatus.Done, "Bad math in IBufferWriter HTML writing extensions");
}

private static void HtmlEncodeAndWriteSmall(IBufferWriter<byte> bufferWriter, ReadOnlySpan<char> textSpan, HtmlEncoder htmlEncoder)
{
Span<char> encodedBuffer = stackalloc char[BufferSizes.SmallTextWriteCharSize];
var encodeStatus = OperationStatus.Done;

// It's possible for encoding to take multiple cycles if an unusually high number of chars need HTML encoding and/or the
// the total number of chars is close to the SmallWriteCharSize limit.
while (textSpan.Length > 0)
{
// Encode to buffer
encodeStatus = htmlEncoder.Encode(textSpan, encodedBuffer, out var charsConsumed, out var charsWritten);

Debug.Assert(encodeStatus != OperationStatus.NeedMoreData, "Not expecting to hit this case");

// Write encoded chars to the writer
var encoded = encodedBuffer[..charsWritten];
WriteHtml(bufferWriter, encoded);

textSpan = textSpan[charsConsumed..];
}

Debug.Assert(encodeStatus == OperationStatus.Done, "Bad math in IBufferWriter HTML writing extensions");
}

public static void WriteHtml(this IBufferWriter<byte> bufferWriter, ReadOnlySpan<char> html)
{
Span<byte> writerSpan = default;

var status = OperationStatus.Done;
int waitingToAdvance = 0;

while (html.Length > 0)
{
if (writerSpan.Length == 0)
{
if (waitingToAdvance > 0)
{
bufferWriter.Advance(waitingToAdvance);
waitingToAdvance = 0;
}
var spanLengthHint = Math.Min(html.Length, BufferSizes.MaxBufferSize);
writerSpan = bufferWriter.GetSpan(spanLengthHint);
}

status = Utf8.FromUtf16(html, writerSpan, out var charsRead, out var bytesWritten);
waitingToAdvance += bytesWritten;

if (html.Length - charsRead == 0)
{
break;
}

html = html[charsRead..];
writerSpan = writerSpan[bytesWritten..];
}

if (waitingToAdvance > 0)
{
bufferWriter.Advance(waitingToAdvance);
}

Debug.Assert(status == OperationStatus.Done, "Bad math in IBufferWriter HTML writing extensions");
}

private static readonly int TrueStringLength = bool.TrueString.Length;
private static readonly int FalseStringLength = bool.FalseString.Length;

public static void Write(this IBufferWriter<byte> bufferWriter, bool value)
{
var buffer = bufferWriter.GetSpan(value ? TrueStringLength : FalseStringLength);
if (!Utf8Formatter.TryFormat(value, buffer, out var _))
{
throw new FormatException("Unexpectedly insufficient space in buffer to format bool value.");
}
}
}
4 changes: 4 additions & 0 deletions src/RazorSlices/RazorSlice.Partials.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ internal ValueTask<HtmlString> RenderChildSliceAsync(RazorSlice child)
{
renderPartialTask = child.RenderToTextWriterAsync(_textWriter, _htmlEncoder, CancellationToken, renderLayout: false);
}
else if (_bufferWriter is not null)
{
renderPartialTask = child.RenderToBufferWriterAsync(_bufferWriter, _htmlEncoder, CancellationToken, renderLayout: false);
}
#pragma warning restore CA2012
else
{
Expand Down
20 changes: 17 additions & 3 deletions src/RazorSlices/RazorSlice.Write.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ protected void WriteLiteral(string? value)

_pipeWriter?.WriteHtml(value.AsSpan());
_textWriter?.Write(value);
_bufferWriter?.WriteHtml(value.AsSpan());
}

/// <summary>
Expand Down Expand Up @@ -75,6 +76,7 @@ protected void WriteLiteral(ReadOnlySpan<byte> value)

_pipeWriter?.Write(value);
_textWriter?.WriteUtf8(value);
_bufferWriter?.Write(value);
}

/// <summary>
Expand Down Expand Up @@ -119,6 +121,7 @@ protected void Write(ReadOnlySpan<byte> value)

_pipeWriter?.HtmlEncodeAndWriteUtf8(value, _htmlEncoder);
_textWriter?.HtmlEncodeAndWriteUtf8(value, _htmlEncoder);
_bufferWriter?.HtmlEncodeAndWriteUtf8(value, _htmlEncoder);
}

/// <summary>
Expand Down Expand Up @@ -177,6 +180,7 @@ protected void Write(ReadOnlySpan<char> value)
{
_pipeWriter?.HtmlEncodeAndWrite(value, _htmlEncoder);
_textWriter?.HtmlEncodeAndWrite(value, _htmlEncoder);
_bufferWriter?.HtmlEncodeAndWrite(value, _htmlEncoder);
}
}

Expand Down Expand Up @@ -204,6 +208,7 @@ protected HtmlString WriteBool(bool? value)
{
_pipeWriter?.Write(value.Value);
_textWriter?.Write(value.Value);
_bufferWriter?.Write(value.Value);
}
return HtmlString.Empty;
}
Expand All @@ -224,6 +229,7 @@ protected HtmlString WriteSpanFormattable<T>(T? formattable, ReadOnlySpan<char>
var htmlEncoder = htmlEncode ? _htmlEncoder : NullHtmlEncoder.Default;
_pipeWriter?.HtmlEncodeAndWriteSpanFormattable(formattable, htmlEncoder, format, formatProvider);
_textWriter?.HtmlEncodeAndWriteSpanFormattable(formattable, htmlEncoder, format, formatProvider);
_bufferWriter?.HtmlEncodeAndWriteSpanFormattable(formattable, htmlEncoder, format, formatProvider);
}

return HtmlString.Empty;
Expand All @@ -245,6 +251,7 @@ protected HtmlString WriteUtf8SpanFormattable<T>(T? formattable, ReadOnlySpan<ch
var htmlEncoder = htmlEncode ? _htmlEncoder : NullHtmlEncoder.Default;
_pipeWriter?.HtmlEncodeAndWriteUtf8SpanFormattable(formattable, htmlEncoder, format, formatProvider);
_textWriter?.HtmlEncodeAndWriteUtf8SpanFormattable(formattable, htmlEncoder, format, formatProvider);
_bufferWriter?.HtmlEncodeAndWriteUtf8SpanFormattable(formattable, htmlEncoder, format, formatProvider);
}

return HtmlString.Empty;
Expand All @@ -262,13 +269,18 @@ protected HtmlString WriteHtml<T>(T htmlContent)
{
if (_pipeWriter is not null)
{
_utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter);
htmlContent.WriteTo(_utf8BufferTextWriter, _htmlEncoder);
_utf8PipeTextWriter ??= Utf8PipeWriterTextWriter.Get(_pipeWriter);
htmlContent.WriteTo(_utf8PipeTextWriter, _htmlEncoder);
}
if (_textWriter is not null)
else if (_textWriter is not null)
{
htmlContent.WriteTo(_textWriter, _htmlEncoder);
}
else if (_bufferWriter is not null)
{
_utf8BufferTextWriter ??= Utf8BufferTextWriter.Get(_bufferWriter);
htmlContent.WriteTo(_utf8BufferTextWriter, _htmlEncoder);
}
}

return HtmlString.Empty;
Expand All @@ -285,6 +297,7 @@ protected HtmlString WriteHtml(HtmlString htmlString)
{
_pipeWriter?.WriteHtml(htmlString.Value);
_textWriter?.Write(htmlString.Value);
_bufferWriter?.WriteHtml(htmlString.Value);
}

return HtmlString.Empty;
Expand All @@ -305,6 +318,7 @@ protected HtmlString WriteHtml(string? htmlString)
{
_pipeWriter?.WriteHtml(htmlString.AsSpan());
_textWriter?.Write(htmlString);
_bufferWriter?.WriteHtml(htmlString.AsSpan());
}

return HtmlString.Empty;
Expand Down
Loading