diff --git a/Examples/AspNetPageEmitter/Program.cs b/Examples/AspNetPageEmitter/Program.cs index 90661ab..d2bf5e3 100644 --- a/Examples/AspNetPageEmitter/Program.cs +++ b/Examples/AspNetPageEmitter/Program.cs @@ -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(); \ No newline at end of file +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); + } +} \ No newline at end of file diff --git a/HtmlUtilities/HtmlDocumentExtensions.cs b/HtmlUtilities/HtmlDocumentExtensions.cs new file mode 100644 index 0000000..3a39894 --- /dev/null +++ b/HtmlUtilities/HtmlDocumentExtensions.cs @@ -0,0 +1,57 @@ +using System.Buffers; + +namespace HtmlUtilities; + +/// +/// Provides extended functionality for implementations. +/// +public static class HtmlDocumentExtensions +{ + /// + /// Writes the document to . + /// + /// The document to write. + /// Receives the written document. + /// Cancels emission of document data. + /// + public static async Task WriteAsync( + this IHtmlDocument document, + IBufferWriter writer, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + writer.Write(""u8); + + + if ((validated = document.Title.value) is not null) + { + writer.Write(""u8); + writer.Write(validated); + writer.Write(""u8); + } + + if ((validated = document.Description.value) is not null) + { + writer.Write(""u8); + } + + writer.Write(""u8); + + await document.WriteBodyContentsAsync(new HtmlWriter(writer), cancellationToken).ConfigureAwait(false); + + writer.Write(""u8); + } +} diff --git a/HtmlUtilities/HtmlWriter.cs b/HtmlUtilities/HtmlWriter.cs index ea246b3..505e0e2 100644 --- a/HtmlUtilities/HtmlWriter.cs +++ b/HtmlUtilities/HtmlWriter.cs @@ -17,7 +17,7 @@ public readonly struct HtmlWriter private readonly IBufferWriter writer; - private HtmlWriter(IBufferWriter writer) + internal HtmlWriter(IBufferWriter writer) { ArgumentNullException.ThrowIfNull(this.writer = writer, nameof(writer)); } diff --git a/HtmlUtilities/IHtmlDocument.cs b/HtmlUtilities/IHtmlDocument.cs new file mode 100644 index 0000000..032ad78 --- /dev/null +++ b/HtmlUtilities/IHtmlDocument.cs @@ -0,0 +1,54 @@ +using HtmlUtilities.Validated; +using System.Buffers; + +namespace HtmlUtilities; + +/// +/// Automatically handles many of the standard features of an HTML document. +/// +public interface IHtmlDocument +{ + /// + /// The IETF language tag of the document's language. + /// By default, this attribute is not emitted. + /// + ValidatedAttributeValue Language => new(); + + /// + /// The document's title. + /// By default, this attribute is not emitted. + /// + ValidatedText Title => new(); + + /// + /// A description of the contents of the document. + /// By default, this is not emitted. + /// + ValidatedAttributeValue Description => new(); + + /// + /// Writes the content of an HTML document's body. + /// + /// Receives the write commands. + /// Indicates that the document is no longer needed so processing can be cancelled. + /// A task that, upon completion, indicates document writing is complete. + /// By default, directs the viewer to https://github.com/RyanLamansky/html-utilities to learn how to use this function. + 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; + } +} diff --git a/HtmlUtilities/Validated/ValidatedAttributeValue.cs b/HtmlUtilities/Validated/ValidatedAttributeValue.cs index 3492531..1183650 100644 --- a/HtmlUtilities/Validated/ValidatedAttributeValue.cs +++ b/HtmlUtilities/Validated/ValidatedAttributeValue.cs @@ -303,4 +303,16 @@ private static void EmitQuoted(ReadOnlySpan value, ref ArrayBuilder /// /// A string representation of this value. public override string ToString() => value is null ? "" : Encoding.UTF8.GetString(value); + + /// + /// Creates a new from the provided of type . + /// + /// The value to prepare as an attribute. + public static implicit operator ValidatedAttributeValue(ReadOnlySpan value) => new(value); + + /// + /// Creates a new from the provided of type . + /// + /// The value to prepare as an attribute. + public static implicit operator ValidatedAttributeValue(string value) => new(value); } diff --git a/HtmlUtilities/Validated/ValidatedText.cs b/HtmlUtilities/Validated/ValidatedText.cs index 11a2202..a57d627 100644 --- a/HtmlUtilities/Validated/ValidatedText.cs +++ b/HtmlUtilities/Validated/ValidatedText.cs @@ -59,4 +59,18 @@ internal static void Validate(ReadOnlySpan text, ref ArrayBuilder wr /// /// A string representation of this value. public override string ToString() => value is null ? "" : Encoding.UTF8.GetString(value); + + /// + /// Creates a new with the provided content. + /// + /// The text to use. + /// Characters are escaped if needed. Invalid characters are skipped. + public static implicit operator ValidatedText(ReadOnlySpan text) => new(text); + + /// + /// Creates a new with the provided content. + /// + /// The text to use. + /// Characters are escaped if needed. Invalid characters are skipped. + public static implicit operator ValidatedText(string text) => new(text); }