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