In IndexHtmlFileProvider, preserve original source formatting

This commit is contained in:
Steve Sanderson 2018-02-12 23:05:47 +00:00
parent b38718d77f
commit 7bb4bbbe5c
2 changed files with 119 additions and 47 deletions

View File

@ -2,14 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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 Microsoft.AspNetCore.Blazor.Internal.Common.FileProviders;
using System.IO;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using System.Linq; using System.Linq;
using AngleSharp.Parser.Html; using AngleSharp.Parser.Html;
using AngleSharp.Dom;
using AngleSharp; using AngleSharp;
using AngleSharp.Html;
using System;
namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem
{ {
@ -50,38 +50,81 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem
/// </remarks> /// </remarks>
private static string GetIndexHtmlContents(string htmlTemplate, string assemblyName, IEnumerable<IFileInfo> binFiles) private static string GetIndexHtmlContents(string htmlTemplate, string assemblyName, IEnumerable<IFileInfo> binFiles)
{ {
var parser = new HtmlParser(); var resultBuilder = new StringBuilder();
var dom = parser.Parse(htmlTemplate);
// First see if the user has declared a 'boot' script, // Search for a tag of the form <script type="boot-blazor"></script>, and replace
// then it's their responsibility to load blazor.js // it with a fully-configured Blazor boot script tag
var bootScript = dom.Body?.QuerySelectorAll("script") var tokenizer = new HtmlTokenizer(
.Where(x => x.Attributes["type"]?.Value == "blazor-boot").FirstOrDefault(); new TextSource(htmlTemplate),
HtmlEntityService.Resolver);
// If we find a script tag that is decorated with a type="blazor-boot" var currentRangeStartPos = 0;
// this will be the point at which we start the Blazor boot process var isInBlazorBootTag = false;
if (bootScript != null) var resumeOnNextToken = false;
while (true)
{ {
// We need to remove the 'type="blazor-boot"' so that var token = tokenizer.Get();
// it reverts to being processed as JS by the browser if (resumeOnNextToken)
bootScript.RemoveAttribute("type"); {
resumeOnNextToken = false;
currentRangeStartPos = token.Position.Position;
}
// Leave any user-specified attributes on the tag as-is switch (token.Type)
// and add/overwrite the config data needed to boot Blazor {
InjectBootConfig(bootScript, assemblyName, binFiles); 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 // Instead of emitting the source text for this special tag, emit a fully-
// leave it up to the user to handle kicking off the boot // configured Blazor boot script tag
AppendScriptTagWithBootConfig(
resultBuilder,
assemblyName,
binFiles,
tag.Attributes);
using (var writer = new StringWriter()) // Set a flag so we know not to emit anything else until the special
{ // tag is closed
dom.ToHtml(writer, new AutoSelectedMarkupFormatter()); isInBlazorBootTag = true;
return writer.ToString(); }
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<IFileInfo> 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<IFileInfo> binFiles,
List<KeyValuePair<string, string>> attributes)
{ {
var assemblyNameWithExtension = $"{assemblyName}.dll"; var assemblyNameWithExtension = $"{assemblyName}.dll";
var referenceNames = binFiles var referenceNames = binFiles
@ -89,9 +132,28 @@ namespace Microsoft.AspNetCore.Blazor.Build.Core.FileSystem
.Select(file => file.Name); .Select(file => file.Name);
var referencesAttribute = string.Join(",", referenceNames.ToArray()); var referencesAttribute = string.Join(",", referenceNames.ToArray());
script.SetAttribute("src", "/_framework/blazor.js"); var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value);
script.SetAttribute("main", assemblyNameWithExtension); attributesDict.Remove("type");
script.SetAttribute("references", referencesAttribute); attributesDict["src"] = "/_framework/blazor.js";
attributesDict["main"] = assemblyNameWithExtension;
attributesDict["references"] = referencesAttribute;
resultBuilder.Append("<script");
foreach (var attributePair in attributesDict)
{
if (!string.IsNullOrEmpty(attributePair.Value))
{
resultBuilder.AppendFormat(" {0}=\"{1}\"",
attributePair.Key,
attributePair.Value); // TODO: HTML attribute encode
}
else
{
resultBuilder.AppendFormat(" {0}",
attributePair.Key);
}
}
resultBuilder.Append("></script>");
} }
} }
} }

View File

@ -32,7 +32,6 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test
{ {
// Arrange // Arrange
var htmlTemplate = "test"; var htmlTemplate = "test";
var htmlOutput = $"<html><head></head><body>{htmlTemplate}</body></html>";
var instance = new IndexHtmlFileProvider( var instance = new IndexHtmlFileProvider(
htmlTemplate, "fakeassembly", Enumerable.Empty<IFileInfo>()); htmlTemplate, "fakeassembly", Enumerable.Empty<IFileInfo>());
@ -44,8 +43,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test
Assert.False(file.IsDirectory); Assert.False(file.IsDirectory);
Assert.Equal("/index.html", file.PhysicalPath); Assert.Equal("/index.html", file.PhysicalPath);
Assert.Equal("index.html", file.Name); Assert.Equal("index.html", file.Name);
Assert.Equal(htmlOutput, ReadString(file)); Assert.Equal(htmlTemplate, ReadString(file));
Assert.Equal(htmlOutput.Length, file.Length);
} }
[Fact] [Fact]
@ -69,7 +67,19 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test
public void InjectsScriptTagReferencingAssemblyAndDependencies() public void InjectsScriptTagReferencingAssemblyAndDependencies()
{ {
// Arrange // Arrange
var htmlTemplate = "<html><body><h1>Hello</h1>Some text</body><script type='blazor-boot'></script></html>"; var htmlTemplatePrefix = @"
<html>
<body>
<h1>Hello</h1>
Some text
<script>alert(1)</script>";
var htmlTemplateSuffix = @"
</body>
</html>";
var htmlTemplate =
$@"{htmlTemplatePrefix}
<script type='blazor-boot' custom1 custom2=""value"">some text that should be removed</script>
{htmlTemplateSuffix}";
var dependencies = new IFileInfo[] var dependencies = new IFileInfo[]
{ {
new TestFileInfo("System.Abc.dll"), new TestFileInfo("System.Abc.dll"),
@ -80,25 +90,30 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test
// Act // Act
var file = instance.GetFileInfo("/index.html"); var file = instance.GetFileInfo("/index.html");
var parsedHtml = new HtmlParser().Parse(ReadString(file)); var fileContents = ReadString(file);
var firstElem = parsedHtml.Body.FirstElementChild;
var scriptElem = parsedHtml.Body.QuerySelector("script");
// Assert // Assert: Start and end is not modified (including formatting)
Assert.Equal("h1", firstElem.TagName.ToLowerInvariant()); Assert.StartsWith(htmlTemplatePrefix, fileContents);
Assert.Equal("script", scriptElem.TagName.ToLowerInvariant()); 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("<html><body>" + scriptTagText + "</body></html>");
var scriptElem = parsedHtml.Body.QuerySelector("script");
Assert.False(scriptElem.HasChildNodes); Assert.False(scriptElem.HasChildNodes);
Assert.Equal("/_framework/blazor.js", scriptElem.GetAttribute("src")); Assert.Equal("/_framework/blazor.js", scriptElem.GetAttribute("src"));
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main")); Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references")); Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references"));
Assert.False(scriptElem.HasAttribute("type")); Assert.False(scriptElem.HasAttribute("type"));
Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value);
Assert.Equal("value", scriptElem.Attributes["custom2"].Value);
} }
[Fact] [Fact]
public void MissingBootScriptTagReferencingAssemblyAndDependencies() public void SuppliesHtmlTemplateUnchangedIfNoBootScriptPresent()
{ {
// Arrange // Arrange
var htmlTemplate = "<html><body><h1>Hello</h1>Some text</body></html>"; var htmlTemplate = "<!DOCTYPE html><html><body><h1 style='color:red'>Hello</h1>Some text<script type='irrelevant'>blah</script></body></html>";
var dependencies = new IFileInfo[] var dependencies = new IFileInfo[]
{ {
new TestFileInfo("System.Abc.dll"), new TestFileInfo("System.Abc.dll"),
@ -109,14 +124,9 @@ namespace Microsoft.AspNetCore.Blazor.Server.Test
// Act // Act
var file = instance.GetFileInfo("/index.html"); 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
Assert.Equal("h1", firstElem.TagName.ToLowerInvariant()); Assert.Equal(htmlTemplate, ReadString(file));
Assert.Null(scriptElem);
} }
private static string ReadString(IFileInfo file) private static string ReadString(IFileInfo file)