Skip to content
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

Merged
merged 15 commits into from
Feb 10, 2018
Merged

Implementing Relative Links/Images/Emails in MarkdownTextBlock #1639

merged 15 commits into from
Feb 10, 2018

Conversation

avknaidu
Copy link
Member

Issue: #1594 #1607

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[x] Feature
[ ] Code style update (formatting)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build or CI related changes
[ ] Documentation content changes
[ ] Sample app changes
[ ] Other... Please describe:

What is the current behavior?

  1. Images are not rendered when set to MarkdownTextBlock as (/Assets/Image.png)
  2. Masking Emails was not an option before.
  3. Relative Links were not masked properly before. See Missing support for relative reference link and svg image in MarkdownTextBlock #1594

PR Checklist

Please check if your PR fulfills the following requirements:

  • Tested code with current supported SDKs
  • Docs have been added / updated (for bug fixes / features)
  • Sample in sample app has been added / updated (for bug fixes / features)
  • Tests for the changes have been added (for bug fixes / features) (if applicable)

What is the new behavior?

  1. Images will be rendered now if used with Masked Reference
    eg: ![Toolkit Logo](/Assets/ToolkitLogo.png) will render the image in MarkdownTextBlock
  2. Email Links will be rendered as Masked Links and when clicked will open default mail app like it does with unmasked email link.
  3. Feature update for Missing support for relative reference link and svg image in MarkdownTextBlock #1594 related to Relative Links added. Note: Relative Links needs to be handled in LinkClicked event in MarkdownTextBlock.
  4. Sample updated with changes, Note about Handling Relative Links.

Does this PR introduce a breaking change?

[ ] Yes
[x] No

Other information

Consider Line feed and Carriage Return
This reverts commit 7aad125.
This reverts commit 3201148.
@avknaidu
Copy link
Member Author

ping @nmetulev


>[Relative Link 2](../Photos/Photos.json)

**Note:** Relative Links has to be Manually Handled in `LinkClicked` Event.
Copy link
Member Author

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,})+)$"))
Copy link
Member

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?

Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmetulev the regex is same as the one which was used before in RegexTextBox which is now depreciated. See #1192.

Copy link
Member

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.

Copy link
Member Author

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;
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Contributor

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

Copy link
Member

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://"

Copy link
Contributor

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 :)

Copy link
Member Author

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.

@avknaidu
Copy link
Member Author

ping @nmetulev @michael-hawker

@avknaidu
Copy link
Member Author

ping @nmetulev @michael-hawker

@avknaidu
Copy link
Member Author

avknaidu commented Jan 4, 2018

@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.

@nmetulev
Copy link
Contributor

nmetulev commented Jan 4, 2018

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;
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

@avknaidu avknaidu Jan 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nmetulev This is indeed a dependancy property, See here. Since I want to access this inside parsers, i used it as a static property in Common.cs

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Member

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?

Copy link
Contributor

@WilliamABradley WilliamABradley Jan 24, 2018

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

@avknaidu avknaidu changed the title Markdown text block fix Implementing Relative Links/Images/Emails in MarkdownTextBlock Jan 23, 2018
@avknaidu
Copy link
Member Author

@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.

Copy link
Contributor

@WilliamABradley WilliamABradley left a 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.

@WilliamABradley
Copy link
Contributor

ping @avknaidu

@avknaidu
Copy link
Member Author

@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.

@WilliamABradley
Copy link
Contributor

You can instead use Uri.TryCreate with Absolute as the Enum set, then if that fails we know it is a Relative Uri.

@avknaidu
Copy link
Member Author

avknaidu commented Feb 1, 2018

ping @WilliamABradley @michael-hawker

Copy link
Contributor

@WilliamABradley WilliamABradley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

Copy link
Member

@michael-hawker michael-hawker left a 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))
Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor

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
Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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("/"))
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Contributor

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?

Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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)

Copy link
Member Author

@avknaidu avknaidu Feb 9, 2018

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.

Copy link
Contributor

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])+)\\])");
Copy link
Member

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?

Copy link
Member

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?

Copy link
Contributor

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?

Copy link
Member Author

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
Copy link
Contributor

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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])+)\\])");
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Contributor

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;
Copy link
Contributor

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.

@nmetulev
Copy link
Contributor

Thanks @avknaidu. Up to @michael-hawker now for his review :)

@michael-hawker
Copy link
Member

Think my last comment is that StringExtensions.cs should probably live in an Extensions folder rather than Helpers.

Basically keep the folder structure Extensions\Common you had before but in the Microsoft.Toolkit.Uwp project, to mimic what we've done in the Microsoft.Toolkit.Uwp.UI project.

@nmetulev would it make sense to put this in Microsoft.Toolkit as it's not UWP specific?

@WilliamABradley
Copy link
Contributor

I vote for Microsoft.Toolkit, since that will be going to Microsoft.Toolkit.Parsers anyway.

@avknaidu
Copy link
Member Author

@michael-hawker done.

@michael-hawker michael-hawker merged commit b63ee18 into CommunityToolkit:master Feb 10, 2018
@windowstoolkitbot
Copy link

This PR is linked to unclosed issues. Please check if one of these issues should be closed: #1594, #1607

@avknaidu avknaidu deleted the MarkdownTextBlockFix branch February 10, 2018 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants