Skip to content

Commit

Permalink
Added IHtmlDocument to simplify the standardized parts of HTML docume…
Browse files Browse the repository at this point in the history
…nt generation.
  • Loading branch information
RyanLamansky committed Jul 27, 2024
1 parent 8d98564 commit bfd57eb
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 23 deletions.
53 changes: 31 additions & 22 deletions Examples/AspNetPageEmitter/Program.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
using HtmlUtilities;
using HtmlUtilities.Validated;

var app = WebApplication.CreateBuilder(args).Build();
var builder = WebApplication.CreateBuilder(args);

app.MapFallback(async (HttpResponse response, CancellationToken cancellationToken) =>
{
response.ContentType = "text/html; charset=utf-8";
builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);

await HtmlWriter.WriteDocumentAsync(response.BodyWriter, attributes => attributes.Write("lang", "en-us"), async (children, cancellationToken) =>
{
children.WriteElement("head", null, children =>
{
children.WriteElementSelfClosing("meta", attributes => attributes.Write("charset", "utf-8"));
children.WriteElement("title", null, children => children.WriteText("Hello World!"));
}); //head
var app = builder.Build();

await children.WriteElementAsync("body", null, async (children, cancellationToken) =>
{
children.WriteElement("p", null, children => children.WriteText("First bytes."));
app.MapFallback((HttpContext context) =>
context.WriteDocumentAsync(new TestDocument()));

await response.BodyWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
await app.RunAsync().ConfigureAwait(false);

children.WriteElement("p", null, children => children.WriteText("Second bytes after a delay."));
}, cancellationToken).ConfigureAwait(false); // body
}, cancellationToken).ConfigureAwait(false);
class TestDocument : IHtmlDocument
{
ValidatedAttributeValue IHtmlDocument.Language => "en-us";
ValidatedText IHtmlDocument.Title => "Hello World!";
ValidatedAttributeValue IHtmlDocument.Description => "Test page for HTML Utilities";

await response.BodyWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
});
Task IHtmlDocument.WriteBodyContentsAsync(HtmlWriter writer, CancellationToken cancellationToken)
{
writer.WriteElement("p", null, children => children.WriteText("Test bytes."));
return Task.CompletedTask;
}
}

app.Run();
static class Extensions
{
public static Task WriteDocumentAsync(this HttpContext context, IHtmlDocument document)
{
var request = context.Request;
var response = context.Response;
response.ContentType = "text/html; charset=utf-8";
var baseUri = $"{request.Scheme}://{request.Host}/";
response.Headers.ContentSecurityPolicy = $"default-src {baseUri}; base-uri {baseUri}";

return document.WriteAsync(response.BodyWriter, context.RequestAborted);
}
}
57 changes: 57 additions & 0 deletions HtmlUtilities/HtmlDocumentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Buffers;

namespace HtmlUtilities;

/// <summary>
/// Provides extended functionality for <see cref="IHtmlDocument"/> implementations.
/// </summary>
public static class HtmlDocumentExtensions
{
/// <summary>
/// Writes the document to <paramref name="writer"/>.
/// </summary>
/// <param name="document">The document to write.</param>
/// <param name="writer">Receives the written document.</param>
/// <param name="cancellationToken">Cancels emission of document data.</param>
/// <returns></returns>
public static async Task WriteAsync(
this IHtmlDocument document,
IBufferWriter<byte> writer,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

writer.Write("<!DOCTYPE html><html"u8);

byte[]? validated;

if ((validated = document.Language.value) is not null)
{
writer.Write(" lang"u8);
writer.Write(validated);
}

writer.Write("><head><meta charset=utf-8><meta name=viewport content=\"width=device-width, initial-scale=1\">"u8);


if ((validated = document.Title.value) is not null)
{
writer.Write("<title>"u8);
writer.Write(validated);
writer.Write("</title>"u8);
}

if ((validated = document.Description.value) is not null)
{
writer.Write("<meta name=description content"u8);
writer.Write(validated);
writer.Write(">"u8);
}

writer.Write("</head><body>"u8);

await document.WriteBodyContentsAsync(new HtmlWriter(writer), cancellationToken).ConfigureAwait(false);

writer.Write("</body></html>"u8);
}
}
2 changes: 1 addition & 1 deletion HtmlUtilities/HtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public readonly struct HtmlWriter

private readonly IBufferWriter<byte> writer;

private HtmlWriter(IBufferWriter<byte> writer)
internal HtmlWriter(IBufferWriter<byte> writer)
{
ArgumentNullException.ThrowIfNull(this.writer = writer, nameof(writer));
}
Expand Down
54 changes: 54 additions & 0 deletions HtmlUtilities/IHtmlDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using HtmlUtilities.Validated;
using System.Buffers;

namespace HtmlUtilities;

/// <summary>
/// Automatically handles many of the standard features of an HTML document.
/// </summary>
public interface IHtmlDocument
{
/// <summary>
/// The IETF language tag of the document's language.
/// By default, this attribute is not emitted.
/// </summary>
ValidatedAttributeValue Language => new();

/// <summary>
/// The document's title.
/// By default, this attribute is not emitted.
/// </summary>
ValidatedText Title => new();

/// <summary>
/// A description of the contents of the document.
/// By default, this is not emitted.
/// </summary>
ValidatedAttributeValue Description => new();

/// <summary>
/// Writes the content of an HTML document's body.
/// </summary>
/// <param name="writer">Receives the write commands.</param>
/// <param name="cancellationToken">Indicates that the document is no longer needed so processing can be cancelled.</param>
/// <returns>A task that, upon completion, indicates document writing is complete.</returns>
/// <remarks>By default, directs the viewer to https://github.com/RyanLamansky/html-utilities to learn how to use this function.</remarks>
Task WriteBodyContentsAsync(HtmlWriter writer, CancellationToken cancellationToken = default)
{
writer.WriteElement("p", null, children =>
{
writer.WriteText("Visit ");
writer.WriteElement("a", writer =>
{
writer.Write("href", "https://github.com/RyanLamansky/html-utilities");
},
writer =>
{
writer.WriteText("https://github.com/RyanLamansky/html-utilities");
});
writer.WriteText(" to learn how to customize an HtmlDocument instance.");
});

return Task.CompletedTask;
}
}
12 changes: 12 additions & 0 deletions HtmlUtilities/Validated/ValidatedAttributeValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,16 @@ private static void EmitQuoted(ReadOnlySpan<char> value, ref ArrayBuilder<byte>
/// </summary>
/// <returns>A string representation of this value.</returns>
public override string ToString() => value is null ? "" : Encoding.UTF8.GetString(value);

/// <summary>
/// Creates a new <see cref="ValidatedAttributeValue"/> from the provided <see cref="ReadOnlySpan{T}"/> of type <see cref="char"/>.
/// </summary>
/// <param name="value">The value to prepare as an attribute.</param>
public static implicit operator ValidatedAttributeValue(ReadOnlySpan<char> value) => new(value);

/// <summary>
/// Creates a new <see cref="ValidatedAttributeValue"/> from the provided <see cref="ReadOnlySpan{T}"/> of type <see cref="char"/>.
/// </summary>
/// <param name="value">The value to prepare as an attribute.</param>
public static implicit operator ValidatedAttributeValue(string value) => new(value);
}
14 changes: 14 additions & 0 deletions HtmlUtilities/Validated/ValidatedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,18 @@ internal static void Validate(ReadOnlySpan<char> text, ref ArrayBuilder<byte> wr
/// </summary>
/// <returns>A string representation of this value.</returns>
public override string ToString() => value is null ? "" : Encoding.UTF8.GetString(value);

/// <summary>
/// Creates a new <see cref="ValidatedText"/> with the provided content.
/// </summary>
/// <param name="text">The text to use.</param>
/// <remarks>Characters are escaped if needed. Invalid characters are skipped.</remarks>
public static implicit operator ValidatedText(ReadOnlySpan<char> text) => new(text);

/// <summary>
/// Creates a new <see cref="ValidatedText"/> with the provided content.
/// </summary>
/// <param name="text">The text to use.</param>
/// <remarks>Characters are escaped if needed. Invalid characters are skipped.</remarks>
public static implicit operator ValidatedText(string text) => new(text);
}

0 comments on commit bfd57eb

Please sign in to comment.