-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implementing Relative Links/Images/Emails in MarkdownTextBlock #1639
Implementing Relative Links/Images/Emails in MarkdownTextBlock #1639
Conversation
ping @nmetulev |
|
||
>[Relative Link 2](../Photos/Photos.json) | ||
|
||
**Note:** Relative Links has to be Manually Handled in `LinkClicked` Event. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michael-hawker I thought that was for control documentation. This is the initial content that loads when MarkdownTextblock loads
/// <returns> <c>true</c> if the URL is valid; <c>false</c> otherwise. </returns> | ||
private static bool IsUrlEmail(string url) | ||
{ | ||
if (Regex.IsMatch(url, @"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,})+)$")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the RegEx here is sufficient.
If we're on RS3, we should probably add a method on a .NET Standard 2.0 lib that exposes the MailAddress class, but not sure how we could easily check/switch for that in the main code. @nmetulev thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't use .NET standard 2.0 only APIs just yet. But we can use the C# version from here, not sure if that is much different than what you already have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but that wouldn't get an e-mail like blah'-m$&an-1@gmail.com
which is valid according to the spec. On the reference page from @nmetulev, the C#
one or the full General Email Regex (RFC 53222 Official Standard)
both should work in UWP and cover unusual addresses.
We just want to make sure since we're re-adding this sort of thing, that we're grabbing the right source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michael-hawker @nmetulev will make change asap.
@@ -117,6 +117,7 @@ internal static Common.InlineParseResult Parse(string markdown, int start, int e | |||
} | |||
|
|||
string url = TextRunInline.ResolveEscapeSequences(markdown, urlStart, pos); | |||
url = url.StartsWith("/") ? string.Format("ms-appx://{0}", url) : url; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we have a property which lets the developer decide where the base url for relative links should be (and then default to ms-appx://
)?
This would let them specify an alternate assets folder (ms-appx://Assets
) or an actual url on the web (http://www.mysite.com/Images
) as a baseline instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michael-hawker if the URL starts with http://
it renders by default. that is the reason why url checks for /
only. IDK how much of a help a separate property will be. Also the LinkClicked events does not apply on Images. It renders if it can or else it wont.
@nmetulev thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @michael-hawker is referring to string.Format("ms-appx://{0}", url)
, and changing it to somthing like string.Format("{0}://{1}", prefix, url)
where prefix can be set by the developer to anything and defaults to ms-appx:/// if not set
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, I was thinking the full prefix string.Format("{0}{1}", prefix, url) where prefix default would be "msappx://"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing my example :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it. Will add new Property and update commit.
ping @nmetulev @michael-hawker |
ping @nmetulev @michael-hawker |
@michael-hawker @nmetulev can you guys verify this PR? I would like to use this feature as a Nuget instead of building the code and using the DLL's in my project. Let me know if i missed something. |
Hey @avknaidu, I'll review this soon. Most folks are on a break this time of year, so it might seem a bit slow sometimes :) |
@@ -1096,6 +1114,7 @@ private void RenderMarkdown() | |||
// Try to parse the markdown. | |||
MarkdownDocument markdown = new MarkdownDocument(); | |||
markdown.Parse(Text); | |||
Common.ImageLinkPrefix = ImageLinkPrefix; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of having a static ImageLinkPrefix property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need this set up the Link/Image prefixes Here so that when links are clicked, they will have full URL. And i thought having a static property in Common
will be the easiest way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is that having a static property means that if I use MarkdownTextBlock in multiple places, all images must be in the same location. Wouldn't it make more sense to make this a dependency property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. It just doesn't make sense to also have a static property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you point me on a way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev I see the problem, it's because he has to use the value here which is a helper in a different class.
@WilliamABradley does your refactoring stuff somehow make this easier? Or any suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is because Processing of Blocks and Inlines has no context of the MarkdownDocument. That would require some more refactoring.
Instead it should be kept relative at the parser level, and then handled by the Renderer, where you can add it to the RenderContext. (This will be easier with my upcoming changes).
Add ImageLinkPrefix to the RenderContext class, and then in the Constructor of XamlRenderer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the renderer simply add the image prefix before rendering the images? So the parser provides the absolute link (as it was written) and the renderer handles how to render it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WilliamABradley I think @nmetulev's idea is better since the rendering requires full URL and we can append it at the renderer level. I will update my PR shortly.
@nmetulev @michael-hawker please check if there is anything that i need to finish on this PR. I am really looking for using these changes as Nuget packages instead of DLL's. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently ImageLinkPrefix applies to both Absolute and Relative links, breaking Absolute images. Shouldn't it only affect Relative images? It limits it's usefulness.
ping @avknaidu |
@WilliamABradley I am planning to check if the URL provided can be resolved to image and if not, then see if it resolves after applying prefix. I will try to finish this over the weekend. Let me know if this is not a good way. |
You can instead use Uri.TryCreate with Absolute as the Enum set, then if that fails we know it is a Relative Uri. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev some thoughts on naming and the E-mail Regex. I can't find the previous discussion we had with the result on it. Must have been in another PR/bug?
@@ -1243,6 +1261,14 @@ public void RegisterNewHyperLink(Hyperlink newHyperlink, string linkUrl) | |||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | |||
async Task<ImageSource> IImageResolver.ResolveImageAsync(string url, string tooltip) | |||
{ | |||
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest creating the uri outside of the if block, as if this does succeed then we don't have to recreate the Uri object again to pass to the BitmapImage on line 1281.
Then on line 1268, you can create a Uri object there and reuse the uri instance. Then it just gets passed directly into the BitmapImage constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should actually be able to just use the uri object outside of this if block, it should still be in scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, apparently it does leak into the outer scope, that's a bit odd.
Apparently is
works similarly, kind-of, it's a bit odd, but documented here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's by design, to be used in scenarios exactly like this
/// <summary> | ||
/// Gets or sets the Prefix of Image Link. | ||
/// </summary> | ||
public string ImageLinkPrefix |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was thinking since these aren't actually links, should this name be ImageUriPrefix
? @nmetulev @WilliamABradley thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be a better name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason behind ImageLinkPrefix
is because i want to use the same Prefix
for actual links also instead of 2 different DP's. While this is still not implemented, I want to implement this in such a way that even a relative link works like a normal link when LinkClicked
event is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@avknaidu you mean so that the developer doesn't have to add the detection and relative link parsing to their LinkClicked
event handler?
In that case, I'd suggest UriPrefix
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michael-hawker . Done. I will push the commit shortly.
@@ -69,7 +70,14 @@ private void SetInitalText(string text) | |||
|
|||
private async void MarkdownText_LinkClicked(object sender, UI.Controls.LinkClickedEventArgs e) | |||
{ | |||
await Launcher.LaunchUriAsync(new Uri(e.Link)); | |||
if (e.Link.StartsWith("../") || e.Link.StartsWith("/")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should put the # here as well, maybe we should have a helper method for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where do we put all Helper Methods? I see Common.cs
. Should I use that?
Also i see Url's being handled this way in HyperlinkInline.cs
. I can change it over there also if Common.cs
is the right place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, Common is for Markdown still, and the e-mail regex is in the TextBoxRegEx control as well.
@nmetulev would adding an extensions namespace to Microsoft.Toolkit.Uwp make sense here or use the existing extensions namespace under Microsoft.Toolkit.Uwp.UI
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the helper method here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev IsUriRelative
to go with the IsEmail
below. Thoughts on the namespace choice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it used outside of the controls package? If it's only used here, I'd try to keep it internal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev Issue here is it cannot be internal because the same Link check is Happening on SampleApp Project. Only way we can do this is to move this to Microsoft.Toolkit.UWP.UI so that it can be used in both places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, i missed that this is in the sample app. Why not use the standard way of checking for a relative URI?
Uri.TryCreate(url, UriKind.Absolute, out Uri result)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now for relative URI's we only check if URL's start with /
, #
, ../
. If we check for the above, we are practically allowing anything. I don't think that is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly, I like leaving it up to the developer to decide what to check, so I'm good with the current implementation :)
/// <returns> <c>true</c> if the URL is valid; <c>false</c> otherwise. </returns> | ||
private static bool IsUrlEmail(string url) | ||
{ | ||
return Regex.IsMatch(url, "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev didn't we have a discussion on e-mail regex parsing? I was trying to find the link we found with the reference to this. It'd be nice to put the url reference in a comment here. Also, we use this elsewhere, like the TextBoxRegEx (though that didn't seem to be the one we talked about).
So, it might be nice to have a centralized helper for e-mail validation that can be used in all places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found it. Yeah, it'd be nice to have a comment with our reference for the future:
//General Email Regex (RFC 5322 Official Standard) from emailregex.com
But since we also use it in TextBoxRegEx, it might be nice to pull it out into its own helper that can be used in both places. Maybe as a string extension?
@nmetulev thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, @avknaidu, do you think you can create string extensions and add this method to it to be used with this control and the TextBoxRegEx?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nmetulev Done. Waiting for an answer on the question above to push the commit.
|
||
using System.Text.RegularExpressions; | ||
|
||
namespace Microsoft.Toolkit.Uwp.UI.Extensions.Common |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be in Microsoft.Toolkit.Uwp package instead, it has nothing to do with UI? Similar to the ColorHelper for example. @michael-hawker thoughts?
/// Uses general Email Regex (RFC 5322 Official Standard) from emailregex.com | ||
/// </summary> | ||
/// <returns><c>true</c> for valid email.<c>false</c> otherwise</returns> | ||
public static bool IsEmail(this string str) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TextBoxRegEx implementation should also be updated to use this new method now too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do another PR for moving everything related to TextBoxRegEx
to this helper method. For now I will update this PR moving the string constants separate.
/// <returns><c>true</c> for valid email.<c>false</c> otherwise</returns> | ||
public static bool IsEmail(this string str) | ||
{ | ||
return Regex.IsMatch(str, "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make the TextBoxRegEx change easier, you could pull out the regex pattern string as an internal
constant and just have the TextBoxRegEx switch statement reference that.
@nmetulev think it'd actually be useful to pull all the RegEx expressions out of the TextBoxRegEx class and put them here? Then we can have nice helper methods for string for each of the different types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do another PR for moving everything related to TextBoxRegEx
to this helper method. For now I will update this PR moving the string constants separate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. You should still move the extension to the Microsoft.Toolkit.Uwp package in this PR
namespace Microsoft.Toolkit.Uwp.UI.Controls.Markdown.Parse | ||
{ | ||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These using statements should be outside of the namespace block.
Thanks @avknaidu. Up to @michael-hawker now for his review :) |
Think my last comment is that Basically keep the folder structure @nmetulev would it make sense to put this in |
I vote for Microsoft.Toolkit, since that will be going to Microsoft.Toolkit.Parsers anyway. |
@michael-hawker done. |
Issue: #1594 #1607
PR Type
What kind of change does this PR introduce?
What is the current behavior?
PR Checklist
Please check if your PR fulfills the following requirements:
What is the new behavior?
eg: ![Toolkit Logo](/Assets/ToolkitLogo.png) will render the image in MarkdownTextBlock
LinkClicked
event in MarkdownTextBlock.Does this PR introduce a breaking change?
Other information