From 7bb4bbbe5cb1fc6056a01eb223d5820abaca66bc Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 12 Feb 2018 23:05:47 +0000 Subject: [PATCH] In IndexHtmlFileProvider, preserve original source formatting --- .../Core/FileSystem/IndexHtmlFileProvider.cs | 120 +++++++++++++----- .../IndexHtmlFileProviderTest.cs | 46 ++++--- 2 files changed, 119 insertions(+), 47 deletions(-) diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Core/FileSystem/IndexHtmlFileProvider.cs b/src/Microsoft.AspNetCore.Blazor.Build/Core/FileSystem/IndexHtmlFileProvider.cs index d38e3ade96..9312b923e3 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Core/FileSystem/IndexHtmlFileProvider.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Core/FileSystem/IndexHtmlFileProvider.cs @@ -2,14 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Blazor.Internal.Common.FileProviders; -using System.IO; using System.Collections.Generic; using System.Text; using Microsoft.Extensions.FileProviders; using System.Linq; using AngleSharp.Parser.Html; -using AngleSharp.Dom; using AngleSharp; +using AngleSharp.Html; +using System; namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem { @@ -50,38 +50,81 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem /// private static string GetIndexHtmlContents(string htmlTemplate, string assemblyName, IEnumerable binFiles) { - var parser = new HtmlParser(); - var dom = parser.Parse(htmlTemplate); + var resultBuilder = new StringBuilder(); - // First see if the user has declared a 'boot' script, - // then it's their responsibility to load blazor.js - var bootScript = dom.Body?.QuerySelectorAll("script") - .Where(x => x.Attributes["type"]?.Value == "blazor-boot").FirstOrDefault(); - - // If we find a script tag that is decorated with a type="blazor-boot" - // this will be the point at which we start the Blazor boot process - if (bootScript != null) + // Search for a tag of the form , and replace + // it with a fully-configured Blazor boot script tag + var tokenizer = new HtmlTokenizer( + new TextSource(htmlTemplate), + HtmlEntityService.Resolver); + var currentRangeStartPos = 0; + var isInBlazorBootTag = false; + var resumeOnNextToken = false; + while (true) { - // We need to remove the 'type="blazor-boot"' so that - // it reverts to being processed as JS by the browser - bootScript.RemoveAttribute("type"); + var token = tokenizer.Get(); + if (resumeOnNextToken) + { + resumeOnNextToken = false; + currentRangeStartPos = token.Position.Position; + } - // Leave any user-specified attributes on the tag as-is - // and add/overwrite the config data needed to boot Blazor - InjectBootConfig(bootScript, assemblyName, binFiles); - } + switch (token.Type) + { + case HtmlTokenType.StartTag: + { + // Only do anything special if this is a Blazor boot tag + var tag = token.AsTag(); + if (IsBlazorBootTag(tag)) + { + // First, emit the original source text prior to this special tag, since + // we want that to be unchanged + resultBuilder.Append(htmlTemplate, currentRangeStartPos, token.Position.Position - currentRangeStartPos - 1); - // If no blazor-boot script tag was found, we skip it and - // leave it up to the user to handle kicking off the boot + // Instead of emitting the source text for this special tag, emit a fully- + // configured Blazor boot script tag + AppendScriptTagWithBootConfig( + resultBuilder, + assemblyName, + binFiles, + tag.Attributes); - using (var writer = new StringWriter()) - { - dom.ToHtml(writer, new AutoSelectedMarkupFormatter()); - return writer.ToString(); + // Set a flag so we know not to emit anything else until the special + // tag is closed + isInBlazorBootTag = true; + } + break; + } + + case HtmlTokenType.EndTag: + // If this is an end tag corresponding to the Blazor boot script tag, we + // can switch back into the mode of emitting the original source text + if (isInBlazorBootTag) + { + isInBlazorBootTag = false; + resumeOnNextToken = true; + } + break; + + case HtmlTokenType.EndOfFile: + // Finally, emit any remaining text from the original source file + resultBuilder.Append(htmlTemplate, currentRangeStartPos, htmlTemplate.Length - currentRangeStartPos); + return resultBuilder.ToString(); + } } } - private static void InjectBootConfig(IElement script, string assemblyName, IEnumerable binFiles) + private static bool IsBlazorBootTag(HtmlTagToken tag) + => string.Equals(tag.Name, "script", StringComparison.Ordinal) + && tag.Attributes.Any(pair => + string.Equals(pair.Key, "type", StringComparison.Ordinal) + && string.Equals(pair.Value, "blazor-boot", StringComparison.Ordinal)); + + private static void AppendScriptTagWithBootConfig( + StringBuilder resultBuilder, + string assemblyName, + IEnumerable binFiles, + List> attributes) { var assemblyNameWithExtension = $"{assemblyName}.dll"; var referenceNames = binFiles @@ -89,9 +132,28 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem .Select(file => file.Name); var referencesAttribute = string.Join(",", referenceNames.ToArray()); - script.SetAttribute("src", "/_framework/blazor.js"); - script.SetAttribute("main", assemblyNameWithExtension); - script.SetAttribute("references", referencesAttribute); + var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value); + attributesDict.Remove("type"); + attributesDict["src"] = "/_framework/blazor.js"; + attributesDict["main"] = assemblyNameWithExtension; + attributesDict["references"] = referencesAttribute; + + resultBuilder.Append(""); } } } diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlFileProviderTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlFileProviderTest.cs index c86632eb0b..221f844217 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlFileProviderTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlFileProviderTest.cs @@ -32,7 +32,6 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test { // Arrange var htmlTemplate = "test"; - var htmlOutput = $"{htmlTemplate}"; var instance = new IndexHtmlFileProvider( htmlTemplate, "fakeassembly", Enumerable.Empty()); @@ -44,8 +43,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test Assert.False(file.IsDirectory); Assert.Equal("/index.html", file.PhysicalPath); Assert.Equal("index.html", file.Name); - Assert.Equal(htmlOutput, ReadString(file)); - Assert.Equal(htmlOutput.Length, file.Length); + Assert.Equal(htmlTemplate, ReadString(file)); } [Fact] @@ -69,7 +67,19 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test public void InjectsScriptTagReferencingAssemblyAndDependencies() { // Arrange - var htmlTemplate = "

Hello

Some text"; + var htmlTemplatePrefix = @" + + +

Hello

+ Some text + "; + var htmlTemplateSuffix = @" + + "; + var htmlTemplate = + $@"{htmlTemplatePrefix} + + {htmlTemplateSuffix}"; var dependencies = new IFileInfo[] { new TestFileInfo("System.Abc.dll"), @@ -80,25 +90,30 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test // Act var file = instance.GetFileInfo("/index.html"); - var parsedHtml = new HtmlParser().Parse(ReadString(file)); - var firstElem = parsedHtml.Body.FirstElementChild; - var scriptElem = parsedHtml.Body.QuerySelector("script"); + var fileContents = ReadString(file); - // Assert - Assert.Equal("h1", firstElem.TagName.ToLowerInvariant()); - Assert.Equal("script", scriptElem.TagName.ToLowerInvariant()); + // Assert: Start and end is not modified (including formatting) + Assert.StartsWith(htmlTemplatePrefix, fileContents); + Assert.EndsWith(htmlTemplateSuffix, fileContents); + + // Assert: Boot tag is correct + var scriptTagText = fileContents.Substring(htmlTemplatePrefix.Length, fileContents.Length - htmlTemplatePrefix.Length - htmlTemplateSuffix.Length); + var parsedHtml = new HtmlParser().Parse("" + scriptTagText + ""); + var scriptElem = parsedHtml.Body.QuerySelector("script"); Assert.False(scriptElem.HasChildNodes); Assert.Equal("/_framework/blazor.js", scriptElem.GetAttribute("src")); Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main")); Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references")); Assert.False(scriptElem.HasAttribute("type")); + Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value); + Assert.Equal("value", scriptElem.Attributes["custom2"].Value); } [Fact] - public void MissingBootScriptTagReferencingAssemblyAndDependencies() + public void SuppliesHtmlTemplateUnchangedIfNoBootScriptPresent() { // Arrange - var htmlTemplate = "

Hello

Some text"; + var htmlTemplate = "

Hello

Some text"; var dependencies = new IFileInfo[] { new TestFileInfo("System.Abc.dll"), @@ -109,14 +124,9 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test // Act var file = instance.GetFileInfo("/index.html"); - var parsedHtml = new HtmlParser().Parse(ReadString(file)); - var firstElem = parsedHtml.Body.FirstElementChild; - var scriptElem = parsedHtml.Body.QuerySelector("script"); - // Assert - Assert.Equal("h1", firstElem.TagName.ToLowerInvariant()); - Assert.Null(scriptElem); + Assert.Equal(htmlTemplate, ReadString(file)); } private static string ReadString(IFileInfo file)