In IndexHtmlFileProvider, preserve original source formatting
This commit is contained in:
parent
b38718d77f
commit
7bb4bbbe5c
|
|
@ -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>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue