Browse Source

Properly support relative Uris in StyleInclude/ResourceInclude

pull/9571/head
Max Katz 3 years ago
parent
commit
98cf91c385
  1. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  2. 21
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  3. 2
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs
  4. 88
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

2
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -274,7 +274,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{ {
var uriText = text.Trim(); var uriText = text.Trim();
var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.Absolute : UriKind.Relative); var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.RelativeOrAbsolute : UriKind.Relative);
if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _)) if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _))
{ {

21
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@ -36,34 +36,37 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node);
} }
// We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`.
if (sourceProperty.Values.OfType<XamlAstNewClrObjectNode>().FirstOrDefault() is not { } sourceUriNode if (sourceProperty.Values.OfType<XamlAstNewClrObjectNode>().FirstOrDefault() is not { } sourceUriNode
|| sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri
|| sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }) || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }
|| sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind })
{ {
// TODO: make it a compiler warning // TODO: make it a compiler warning
// Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet.
return node; return node;
} }
if (originalAssetPath.StartsWith("/")) var uriPath = new Uri(originalAssetPath, (UriKind)uriKind);
if (!uriPath.IsAbsoluteUri)
{ {
var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null.");
originalAssetPath = baseUrl.Substring(0, baseUrl.LastIndexOf('/')) + originalAssetPath; uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath);
} }
else if (!originalAssetPath.StartsWith("avares://")) else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase))
{ {
return context.ParseError( return context.ParseError(
$"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.", $"Avalonia supports only \"avares://\" sources or relative sources starting with \"/\" on the \"{nodeTypeName}\" node.",
node); node);
} }
originalAssetPath = Uri.UnescapeDataString(new Uri(originalAssetPath).AbsoluteUri); var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri);
var assetPath = originalAssetPath.Replace("avares://", ""); var assetPath = assetPathUri.Replace("avares://", "");
var assemblyNameSeparator = assetPath.IndexOf('/'); var assemblyNameSeparator = assetPath.IndexOf('/');
var assembly = assetPath.Substring(0, assemblyNameSeparator); var assembly = assetPath.Substring(0, assemblyNameSeparator);
var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.')); var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.'));
if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument) if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, assetPathUri, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument)
{ {
if (targetDocument.ClassType is not null) if (targetDocument.ClassType is not null)
{ {
@ -72,7 +75,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
if (targetDocument.BuildMethod is null) if (targetDocument.BuildMethod is null)
{ {
return context.ParseError($"\"{originalAssetPath}\" cannot be instantiated.", node); return context.ParseError($"\"{assetPathUri}\" cannot be instantiated.", node);
} }
return FromMethod(context, targetDocument.BuildMethod, node); return FromMethod(context, targetDocument.BuildMethod, node);
@ -81,7 +84,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly) if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly)
{ {
return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{originalAssetPath}\" source.", node); return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{assetPathUri}\" source.", node);
} }
if (assetAssembly.FindType(fullTypeName) is { } type if (assetAssembly.FindType(fullTypeName) is { } type

2
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs

@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
if (s == null) if (s == null)
return null; return null;
//On Unix Uri tries to interpret paths starting with "/" as file Uris //On Unix Uri tries to interpret paths starting with "/" as file Uris
var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.Absolute; var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.RelativeOrAbsolute;
if (!Uri.TryCreate(s, kind, out var res)) if (!Uri.TryCreate(s, kind, out var res))
throw new ArgumentException("Unable to parse URI: " + s); throw new ArgumentException("Unable to parse URI: " + s);
return res; return res;

88
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

@ -79,7 +79,35 @@ public class StyleIncludeTests
} }
[Fact] [Fact]
public void Relative_StyleInclude_Is_Resolved_With_Two_Files() public void Relative_Back_StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='../Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_Root_StyleInclude_Is_Resolved_With_Two_Files()
{ {
var documents = new[] var documents = new[]
{ {
@ -94,7 +122,63 @@ public class StyleIncludeTests
<ContentControl xmlns='https://github.com/avaloniaui' <ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources> <ContentControl.Resources>
<StyleInclude x:Key='Include' Source='/../Style.xaml'/> <StyleInclude x:Key='Include' Source='/Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='Style.xaml'/>
</ContentControl.Resources>
</ContentControl>")
};
var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
var style = Assert.IsType<Style>(objects[0]);
var contentControl = Assert.IsType<ContentControl>(objects[1]);
Assert.IsType<Style>(contentControl.Resources["Include"]);
}
[Fact]
public void Relative_Dot_Syntax__StyleInclude_Is_Resolved_With_Two_Files()
{
var documents = new[]
{
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
</Style.Resources>
</Style>"),
new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Resources>
<StyleInclude x:Key='Include' Source='./Style.xaml'/>
</ContentControl.Resources> </ContentControl.Resources>
</ContentControl>") </ContentControl>")
}; };

Loading…
Cancel
Save