diff --git a/Maui/.editorconfig b/Maui/.editorconfig new file mode 100644 index 0000000..d753c22 --- /dev/null +++ b/Maui/.editorconfig @@ -0,0 +1,19 @@ +[*.cs] + +# CA1010: Collections should implement generic interface +dotnet_diagnostic.CA1010.severity = silent + +# CA1710: Identifiers should have correct suffix +dotnet_diagnostic.CA1710.severity = silent + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = silent + +# CS0618: Type or member is obsolete +dotnet_diagnostic.CS0618.severity = silent + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = silent + +# CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2000.severity = silent diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml new file mode 100644 index 0000000..fd8f0d8 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml.cs new file mode 100644 index 0000000..6360e31 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/App.xaml.cs @@ -0,0 +1,11 @@ +namespace HyperTextLabel.Maui.Tests.App; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + + MainPage = new AppShell(); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml new file mode 100644 index 0000000..31a5d7e --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml.cs new file mode 100644 index 0000000..a404912 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/AppShell.xaml.cs @@ -0,0 +1,9 @@ +namespace HyperTextLabel.Maui.Tests.App; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.Designer.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.Designer.cs new file mode 100644 index 0000000..5c45ee2 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.Designer.cs @@ -0,0 +1,252 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HyperTextLabel.Maui.Tests.App { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class HtmlSources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal HtmlSources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HyperTextLabel.Maui.Tests.App.HtmlSources", typeof(HtmlSources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An align center string.. + /// + internal static string AlignCenter { + get { + return ResourceManager.GetString("AlignCenter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An align end string.. + /// + internal static string AlignEnd { + get { + return ResourceManager.GetString("AlignEnd", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to سلسلة عربية.. + /// + internal static string Arab { + get { + return ResourceManager.GetString("Arab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A <strong>bold</strong> string.. + /// + internal static string Bold { + get { + return ResourceManager.GetString("Bold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Red color string.. + /// + internal static string Color { + get { + return ResourceManager.GetString("Color", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a string with a custom font.. + /// + internal static string CustomFont { + get { + return ResourceManager.GetString("CustomFont", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Text with image <img src="https://raw.githubusercontent.com/matteobortolazzo/HtmlLabelPlugin/master/Assets/icon.png" />. + /// + internal static string Image { + get { + return ResourceManager.GetString("Image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An <em>italic</em> string.. + /// + internal static string Italic { + get { + return ResourceManager.GetString("Italic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a string with a different line height.. + /// + internal static string LineHeight { + get { + return ResourceManager.GetString("LineHeight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a>.. + /// + internal static string Links { + get { + return ResourceManager.GetString("Links", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with options.. + /// + internal static string LinksWithOptions { + get { + return ResourceManager.GetString("LinksWithOptions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Email to <a href="mailto:github@github.com?subject=Awesome&body=Awesome%20plugin">github@github.com</a>.. + /// + internal static string LinkToEmail { + get { + return ResourceManager.GetString("LinkToEmail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Link to open maps <a href="geo:53.3242377,-6.3861295;u=5">here</a>. + /// + internal static string LinkToGeo { + get { + return ResourceManager.GetString("LinkToGeo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send SMS to <a href="sms://8372717112?body=Awesome%20plugin">8372717112</a>.. + /// + internal static string LinkToSms { + get { + return ResourceManager.GetString("LinkToSms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Call to <a href="tel://8372717112">8372717112</a>.. + /// + internal static string LinkToTel { + get { + return ResourceManager.GetString("LinkToTel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Call to <a href="tel:083 7271 7112">08372717112</a>.. + /// + internal static string LinkToTelAlternate { + get { + return ResourceManager.GetString("LinkToTelAlternate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with custom color.. + /// + internal static string LinkWithColor { + get { + return ResourceManager.GetString("LinkWithColor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A label with custom gestures.. + /// + internal static string LinkWithGestures { + get { + return ResourceManager.GetString("LinkWithGestures", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with no underline.. + /// + internal static string LinkWithoutUnderline { + get { + return ResourceManager.GetString("LinkWithoutUnderline", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List<ul><li>First item</li><li>Second item</li></ul>. + /// + internal static string List { + get { + return ResourceManager.GetString("List", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to <p>First paragraph</p><p>Second paragraph</p>. + /// + internal static string Paragraphs { + get { + return ResourceManager.GetString("Paragraphs", resourceCulture); + } + } + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.resx b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.resx new file mode 100644 index 0000000..d6be7ee --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HtmlSources.resx @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An align center string. + + + An align end string. + + + سلسلة عربية. + + + A <strong>bold</strong> string. + + + Red color string. + + + This is a string with a custom font. + + + Text with image <img src="https://raw.githubusercontent.com/matteobortolazzo/HtmlLabelPlugin/master/Assets/icon.png" /> + + + An <em>italic</em> string. + + + This is a string with a different line height. + + + This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a>. + + + This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with options. + + + Email to <a href="mailto:github@github.com?subject=Awesome&body=Awesome%20plugin">github@github.com</a>. + + + Link to open maps <a href="geo:53.3242377,-6.3861295;u=5">here</a> + + + Send SMS to <a href="sms://8372717112?body=Awesome%20plugin">8372717112</a>. + + + Call to <a href="tel://8372717112">8372717112</a>. + + + Call to <a href="tel:083 7271 7112">08372717112</a>. + + + This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with custom color. + + + A label with custom gestures. + + + This is a <a href="https://github.com/matteobortolazzo/HtmlLabelPlugin">Link</a> with no underline. + + + List<ul><li>First item</li><li>Second item</li></ul> + + + <p>First paragraph</p><p>Second paragraph</p> + + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/HyperTextLabel.Maui.Tests.App.csproj b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HyperTextLabel.Maui.Tests.App.csproj new file mode 100644 index 0000000..64a353b --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/HyperTextLabel.Maui.Tests.App.csproj @@ -0,0 +1,71 @@ + + + + net6.0-android;net6.0-ios + $(TargetFrameworks);net6.0-windows10.0.19041.0 + + + Exe + HyperTextLabel.Maui.Tests.App + true + true + enable + + + HyperTextLabel.Maui.Tests.App + + + com.companyname.HyperTextLabel.Maui.Tests.App + F49B1950-D2D6-4246-861F-A061DA235263 + + + 1.0 + 1 + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + en-US + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + HtmlSources.resx + + + + + + ResXFileCodeGenerator + HtmlSources.Designer.cs + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml new file mode 100644 index 0000000..5ad6829 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml.cs new file mode 100644 index 0000000..6afd02b --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MainPage.xaml.cs @@ -0,0 +1,43 @@ +namespace HyperTextLabel.Maui.Tests.App; + +public partial class MainPage : ContentPage +{ + public MainPage() + { + InitializeComponent(); + BindingContext = new Sources(); + } +} + +public class Sources +{ + public string Bold => HtmlSources.Bold; + public string Italic => HtmlSources.Italic; + public string Color => HtmlSources.Color; + public string List => HtmlSources.List; + public string AlignCenter => HtmlSources.AlignCenter; + public string AlignEnd => HtmlSources.AlignEnd; + public string Links => HtmlSources.Links; + public string LinksWithOptions => HtmlSources.LinksWithOptions; + public string LinkToEmail => HtmlSources.LinkToEmail; + public string LinkToTel => HtmlSources.LinkToTel; + public string LinkToTelAlternative => HtmlSources.LinkToTelAlternate; + public string LinkToSms => HtmlSources.LinkToSms; + public string LinkToGeo => HtmlSources.LinkToGeo; + public string LinkWithColor => HtmlSources.LinkWithColor; + public string LinkWithoutUnderline => HtmlSources.LinkWithoutUnderline; + public string LinkWithGestures => HtmlSources.LinkWithGestures; + public string CustomFont => HtmlSources.CustomFont; + public string Arab => HtmlSources.Arab; + public string Image => HtmlSources.Image; + public string Paragraphs => HtmlSources.Paragraphs; + public string LineHeight => HtmlSources.LineHeight; + public Command Clicked => new Command(() => Browser.OpenAsync("https://github.com/matteobortolazzo/HtmlLabelPlugin")); + public BrowserLaunchOptions BrowserLaunchOptions => new BrowserLaunchOptions + { + LaunchMode = BrowserLaunchMode.SystemPreferred, + TitleMode = BrowserTitleMode.Show, + PreferredToolbarColor = Colors.AliceBlue, + PreferredControlColor = Colors.Violet + }; +} \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/MauiProgram.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MauiProgram.cs new file mode 100644 index 0000000..cf503b6 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/MauiProgram.cs @@ -0,0 +1,22 @@ +using HyperTextLabel.Maui.Hosting; + +namespace HyperTextLabel.Maui.Tests.App; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureHyperTextLabel() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + fonts.AddFont("Allura-Regular.otf", "AlluraRegular"); + }); + + return builder.Build(); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/AndroidManifest.xml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..fb20840 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainActivity.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..c08ae90 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainActivity.cs @@ -0,0 +1,10 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace HyperTextLabel.Maui.Tests.App; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainApplication.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..06c43ec --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace HyperTextLabel.Maui.Tests.App; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/Resources/values/colors.xml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/AppDelegate.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..ecdac4d --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace HyperTextLabel.Maui.Tests.App; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Info.plist b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..c96dd0a --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Program.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..eeea0f7 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace HyperTextLabel.Maui.Tests.App; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/Main.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/Main.cs new file mode 100644 index 0000000..d00d340 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/Main.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace HyperTextLabel.Maui.Tests.App; + +class Program : MauiApplication +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/tizen-manifest.xml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..a67d6c8 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + appicon.xhigh.png + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml new file mode 100644 index 0000000..2cbb57c --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..00e6ebc --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/App.xaml.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace HyperTextLabel.Maui.Tests.App.WinUI; + +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/Package.appxmanifest b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..299c62a --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,41 @@ + + + + + + + + + c0d34b5b-a78e-4983-bba9-3166df9c2a11 + HtmlLabel.Forms.Plugin.Tests.App.UWP + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/app.manifest b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/app.manifest new file mode 100644 index 0000000..25169e8 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/AppDelegate.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..ecdac4d --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,9 @@ +using Foundation; + +namespace HyperTextLabel.Maui.Tests.App; + +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Info.plist b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Info.plist new file mode 100644 index 0000000..3614e68 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Info.plist @@ -0,0 +1,53 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + MinimumOSVersion + 14.2 + CFBundleDisplayName + HyperTextLabel.Maui.Tests.App + CFBundleIdentifier + com.companyname.HyperTextLabel.Maui.Tests.App + CFBundleVersion + 1.0 + CFBundleName + HyperTextLabel.Maui.Tests.App + LSApplicationQueriesSchemes + + mailto + tel + sms + + CFBundleDevelopmentRegion + en + CFBundleLocalizations + + en + ar + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Program.cs b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Program.cs new file mode 100644 index 0000000..eeea0f7 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Platforms/iOS/Program.cs @@ -0,0 +1,15 @@ +using ObjCRuntime; +using UIKit; + +namespace HyperTextLabel.Maui.Tests.App; + +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Properties/launchSettings.json b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/Allura-Regular.otf b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/Allura-Regular.otf new file mode 100644 index 0000000..cad9a53 Binary files /dev/null and b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/Allura-Regular.otf differ diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Regular.ttf b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..c9237de Binary files /dev/null and b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Semibold.ttf b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..0da9f99 Binary files /dev/null and b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Images/dotnet_bot.svg b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..abfaff2 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Raw/AboutAssets.txt b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..3f7a940 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Raw/AboutAssets.txt @@ -0,0 +1,14 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "MauiAsset": + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Styles.xaml b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Styles.xaml new file mode 100644 index 0000000..0eb4369 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/Styles.xaml @@ -0,0 +1,456 @@ + + + + + #512BD4 + #DFD8F7 + #2B0B98 + White + Black + #E5E5E1 + #969696 + #505050 + + + + + + + + + + #F7B548 + #FFD590 + #FFE5B9 + #28C2D1 + #7BDDEF + #C3F2F4 + #3E8EED + #72ACF1 + #A7CBF6 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appicon.svg b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appiconfg.svg b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/Maui/HtmlLabel.Forms.Plugin.Tests.App/Resources/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Maui/HtmlLabel.sln b/Maui/HtmlLabel.sln new file mode 100644 index 0000000..42a4a87 --- /dev/null +++ b/Maui/HtmlLabel.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32422.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HyperTextLabel.Maui", "HtmlLabel\HyperTextLabel.Maui.csproj", "{49120CED-9682-4AAF-A31C-74D1164A009C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HyperTextLabel.Maui.Tests.App", "HtmlLabel.Forms.Plugin.Tests.App\HyperTextLabel.Maui.Tests.App.csproj", "{20CB997D-35C9-48D9-95C8-006D04DDF026}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49120CED-9682-4AAF-A31C-74D1164A009C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49120CED-9682-4AAF-A31C-74D1164A009C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49120CED-9682-4AAF-A31C-74D1164A009C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49120CED-9682-4AAF-A31C-74D1164A009C}.Release|Any CPU.Build.0 = Release|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Release|Any CPU.Build.0 = Release|Any CPU + {20CB997D-35C9-48D9-95C8-006D04DDF026}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {74AEE0DB-B50C-451B-A0C5-3D939F8A5147} + EndGlobalSection +EndGlobal diff --git a/Maui/HtmlLabel/Controls/HtmlLabel.cs b/Maui/HtmlLabel/Controls/HtmlLabel.cs new file mode 100644 index 0000000..da2c4fc --- /dev/null +++ b/Maui/HtmlLabel/Controls/HtmlLabel.cs @@ -0,0 +1,106 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("HyperTextLabel.Maui.Shared.Tests")] +namespace HyperTextLabel.Maui.Controls +{ + /// + /// + /// A label that is able to display HTML content + /// + public class HtmlLabel : Label, IHtmlLabel + { + /// + /// Identify the UnderlineText property. + /// + public static readonly BindableProperty UnderlineTextProperty = + BindableProperty.Create(nameof(UnderlineText), typeof(bool), typeof(HtmlLabel), true); + + /// + public bool UnderlineText + { + get { return (bool)GetValue(UnderlineTextProperty); } + set { SetValue(UnderlineTextProperty, value); } + } + + /// + /// Identify the LinkColor property. + /// + public static readonly BindableProperty LinkColorProperty = + BindableProperty.Create(nameof(LinkColor), typeof(Color), typeof(HtmlLabel), default); + + /// + public Color LinkColor + { + get { return (Color)GetValue(LinkColorProperty); } + set { SetValue(LinkColorProperty, value); } + } + + /// + /// Identify the BrowserLaunchOptions property. + /// + public static readonly BindableProperty BrowserLaunchOptionsProperty = + BindableProperty.Create(nameof(BrowserLaunchOptions), typeof(BrowserLaunchOptions), typeof(HtmlLabel), default); + + /// + public BrowserLaunchOptions BrowserLaunchOptions + { + get { return (BrowserLaunchOptions)GetValue(BrowserLaunchOptionsProperty); } + set { SetValue(BrowserLaunchOptionsProperty, value); } + } + + /// + /// Identify the AndroidLegacyMode property. + /// + public static readonly BindableProperty AndroidLegacyModeProperty = + BindableProperty.Create(nameof(AndroidLegacyMode), typeof(bool), typeof(HtmlLabel), default); + + /// + public bool AndroidLegacyMode + { + get { return (bool)GetValue(AndroidLegacyModeProperty); } + set { SetValue(AndroidLegacyModeProperty, value); } + } + + /// + /// Identify the AndroidListIndent property KWI-FIX. + /// Default value = 20 (to continue support `old value`) + /// + public static readonly BindableProperty AndroidListIndentProperty = + BindableProperty.Create(nameof(AndroidListIndent), typeof(int), typeof(HtmlLabel), defaultValue: 20); + + /// + public int AndroidListIndent + { + get { return (int)GetValue(AndroidListIndentProperty); } + set { SetValue(AndroidListIndentProperty, value); } + } + + /// + /// Fires before the open URL request is done. + /// + public event EventHandler Navigating; + + /// + /// Fires when the open URL request is done. + /// + public event EventHandler Navigated; + + /// + /// Send the Navigating event + /// + /// + void IHtmlLabelInternals.SendNavigating(WebNavigatingEventArgs args) + { + Navigating?.Invoke(this, args); + } + + /// + /// Send the Navigated event + /// + /// + void IHtmlLabelInternals.SendNavigated(WebNavigatingEventArgs args) + { + Navigated?.Invoke(this, args); + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Controls/IHtmlLabel.cs b/Maui/HtmlLabel/Controls/IHtmlLabel.cs new file mode 100644 index 0000000..4032dd7 --- /dev/null +++ b/Maui/HtmlLabel/Controls/IHtmlLabel.cs @@ -0,0 +1,56 @@ +using Microsoft.Maui.Controls.Internals; + +namespace HyperTextLabel.Maui.Controls +{ + /// + /// Internal class requirments + /// + public interface IHtmlLabelInternals + { + void SendNavigating(WebNavigatingEventArgs args); + + void SendNavigated(WebNavigatingEventArgs args); + } + + /// + /// Properties that the HtmlLabel needs the base class to implement. + /// is currently a public interface but in an internal namespace and + /// EditorBrowsableState.Never so I'm not sure how that's going to play out. I don't want to reference the + /// Xamarin.Forms interface with MAUI custom control. + /// + public interface IHtmlLabelRequiredProperties : ILabel, IFontElement + { + IList GestureRecognizers { get; } + } + + /// + /// Need to look into what should be public vs internal. + /// + public interface IHtmlLabel : IHtmlLabelInternals, IHtmlLabelRequiredProperties + { + /// + /// Get or set if hyperlinks are underlined. + /// + bool UnderlineText { get; } + + /// + /// Get or set the color of hyperlinks. + /// + Color LinkColor { get; } + + /// + /// Get or set the options to use when opening a web link. + /// + BrowserLaunchOptions BrowserLaunchOptions { get; } + + /// + /// Get or set if the Android renderer separates block-level elements with blank lines. + /// + bool AndroidLegacyMode { get; } + + /// + /// Get or set if the Android List Indent property KWI-FIX. + /// + int AndroidListIndent { get; } + } +} diff --git a/Maui/HtmlLabel/Extensions/MauiServiceExtensions.cs b/Maui/HtmlLabel/Extensions/MauiServiceExtensions.cs new file mode 100644 index 0000000..cecf4cd --- /dev/null +++ b/Maui/HtmlLabel/Extensions/MauiServiceExtensions.cs @@ -0,0 +1,32 @@ +using HyperTextLabel.Maui.Extensions; + +namespace HyperTextLabel.Maui.Extensions +{ + /// + /// These extension methods were pulled in from MAUI codebase as they are not public. + /// + internal static class MauiServiceExtensions + { + public static IServiceProvider GetServiceProvider(this IElementHandler handler) + { + var context = handler.MauiContext ?? + throw new InvalidOperationException($"Unable to find the context. The {nameof(handler.MauiContext)} property should have been set by the host."); + + var services = context?.Services ?? + throw new InvalidOperationException($"Unable to find the service provider. The {nameof(handler.MauiContext)} property should have been set by the host."); + + return services; + } + + public static T GetRequiredService(this IElementHandler handler) + where T : notnull + { + var services = handler.GetServiceProvider(); + + var service = services.GetRequiredService(); + + return service; + } + + } +} diff --git a/Maui/HtmlLabel/Extensions/StringExtensions.cs b/Maui/HtmlLabel/Extensions/StringExtensions.cs new file mode 100644 index 0000000..74db7e1 --- /dev/null +++ b/Maui/HtmlLabel/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +using System.Text.RegularExpressions; + +namespace HyperTextLabel.Maui.Extensions +{ + internal static class StringExtensions + { + public static string ReplaceTag(this string html, string oldTagRegex, string newTag) => + Regex.Replace(html, @"(<\s*\/?\s*)" + oldTagRegex + @"((\s+[\w\-\,\.\(\)\=""\:\;]*)*>)", "$1" + newTag + "$2"); + } +} diff --git a/Maui/HtmlLabel/Extensions/UriExtensions.cs b/Maui/HtmlLabel/Extensions/UriExtensions.cs new file mode 100644 index 0000000..7dad788 --- /dev/null +++ b/Maui/HtmlLabel/Extensions/UriExtensions.cs @@ -0,0 +1,125 @@ +using HyperTextLabel.Maui.Utilities; +using System.Globalization; + +namespace HyperTextLabel.Maui.Extensions +{ + public static class UriExtensions + { + public static bool IsHttp(this Uri uri) => uri != null && uri.Scheme.ToUpperInvariant().Contains("HTTP"); + public static bool IsEmail(this Uri uri) => uri.MatchSchema("mailto"); + public static bool IsTel(this Uri uri) => uri.MatchSchema("tel"); + public static bool IsSms(this Uri uri) => uri.MatchSchema("sms"); + public static bool IsGeo(this Uri uri) => uri.MatchSchema("geo"); + + public static void LaunchBrowser(this Uri uri, BrowserLaunchOptions options) + { + if (options == null) + { + Browser.OpenAsync(uri); + } + else + { + Browser.OpenAsync(uri, options); + } + } + + public static bool LaunchEmail(this Uri uri) + { + if (uri == null) + return false; + + var qParams = uri.ParseQueryString(); + var to = uri.Target(); + try + { + var message = new EmailMessage + { + To = new List { to }, + Subject = qParams.GetFirst("subject") ?? string.Empty, + Body = qParams.GetFirst("body") ?? string.Empty, + Cc = qParams.Get("cc") ?? new List(), + Bcc = qParams.Get("bcc") ?? new List() + }; + Email.ComposeAsync(message); + return true; + } + catch (FeatureNotSupportedException ex) + { + System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message); + return false; + } + + } + + public static bool LaunchTel(this Uri uri) + { + if (uri == null) + return false; + + var to = uri.Target(); + try + { + PhoneDialer.Open(to); + return true; + } + catch (FeatureNotSupportedException ex) + { + System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message); + return false; + } + } + + public static bool LaunchSms(this Uri uri) + { + if (uri == null) + return false; + + var qParams = uri.ParseQueryString(); + var to = uri.Target(); + try + { + var messageText = qParams.GetFirst("body"); + var message = new SmsMessage(messageText, new[] { to }); + Sms.ComposeAsync(message); + return true; + } + catch (FeatureNotSupportedException ex) + { + System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message); + return false; + } + } + + public static bool LaunchMaps(this Uri uri) + { + if (uri == null) + return false; + + var target = uri.Target(); + try + { + var coordinates = target.Split(','); + var latitude = double.Parse(coordinates[0], CultureInfo.InvariantCulture.NumberFormat); + var longitude = double.Parse(coordinates[1].Split(';')[0], CultureInfo.InvariantCulture.NumberFormat); + var location = new Location(latitude, longitude); + Map.OpenAsync(location); + return true; + } + catch (FeatureNotSupportedException ex) + { + System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message); + return false; + } + } + + private static string Target(this Uri uri) + { + return Uri.UnescapeDataString(uri.AbsoluteUri.Substring(uri.Scheme.Length + 1).Split('?')[0].Replace("/", "")); + } + + private static bool MatchSchema(this Uri uri, string schema) + { + return uri != null && uri.Scheme.Equals(schema, StringComparison.InvariantCultureIgnoreCase); + } + } +} diff --git a/Maui/HtmlLabel/Handlers/HtmlLabelHandler.cs b/Maui/HtmlLabel/Handlers/HtmlLabelHandler.cs new file mode 100644 index 0000000..ce0d27c --- /dev/null +++ b/Maui/HtmlLabel/Handlers/HtmlLabelHandler.cs @@ -0,0 +1,32 @@ +using HyperTextLabel.Maui.Controls; +using Microsoft.Maui.Handlers; + +namespace HyperTextLabel.Maui.Handlers +{ + public partial class HtmlLabelHandler + { + public static IPropertyMapper Mapper = + new PropertyMapper(LabelHandler.Mapper) + { + [nameof(ILabel.Text)] = MapLabelText, + [nameof(Label.FormattedText)] = MapLabelText, + [nameof(Label.TextTransform)] = MapLabelText, + [nameof(Label.TextType)] = MapLabelText, + [nameof(IHtmlLabel.UnderlineText)] = MapUnderlineText, + [nameof(IHtmlLabel.LinkColor)] = MapLinkColor, + [nameof(IHtmlLabel.BrowserLaunchOptions)] = MapBrowserLaunchOptions, + [nameof(IHtmlLabel.AndroidLegacyMode)] = MapAndroidLegacyMode, + [nameof(IHtmlLabel.AndroidListIndent)] = MapAndroidListIndent, + }; + + public HtmlLabelHandler() : this(null) + { + + } + + public HtmlLabelHandler(IPropertyMapper mapper = null) : base(mapper ?? Mapper) + { + + } + } +} diff --git a/Maui/HtmlLabel/Handlers/HtmlLableMapper.Standard.cs b/Maui/HtmlLabel/Handlers/HtmlLableMapper.Standard.cs new file mode 100644 index 0000000..ddb9b91 --- /dev/null +++ b/Maui/HtmlLabel/Handlers/HtmlLableMapper.Standard.cs @@ -0,0 +1,22 @@ +using HyperTextLabel.Maui.Controls; +using Microsoft.Maui.Handlers; + +namespace HyperTextLabel.Maui.Handlers +{ + public partial class HtmlLabelHandler : ViewHandler + { + protected override object CreatePlatformView() => throw new NotImplementedException(); + + public static void MapLabelText(HtmlLabelHandler handler, IHtmlLabel label) { } + + public static void MapUnderlineText(HtmlLabelHandler handler, IHtmlLabel label) { } + + public static void MapLinkColor(HtmlLabelHandler handler, IHtmlLabel label) { } + + public static void MapBrowserLaunchOptions(HtmlLabelHandler handler, IHtmlLabel label) { } + + public static void MapAndroidLegacyMode(HtmlLabelHandler handler, IHtmlLabel label) { } + + public static void MapAndroidListIndent(HtmlLabelHandler handler, IHtmlLabel label) { } + } +} diff --git a/Maui/HtmlLabel/Hosting/AppHostBuilderExtensions.cs b/Maui/HtmlLabel/Hosting/AppHostBuilderExtensions.cs new file mode 100644 index 0000000..d20e8d9 --- /dev/null +++ b/Maui/HtmlLabel/Hosting/AppHostBuilderExtensions.cs @@ -0,0 +1,36 @@ +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Handlers; +using Microsoft.Maui.Controls.Compatibility.Hosting; + +namespace HyperTextLabel.Maui.Hosting +{ + public static class AppHostBuilderExtensions + { + public static MauiAppBuilder ConfigureHyperTextLabel(this MauiAppBuilder builder, bool useCompatibilityRenderers = false) + { + return useCompatibilityRenderers ? + builder.UseMauiCompatibility() + .ConfigureMauiHandlers(handlers => handlers.AddLibraryCompatibilityRenderers()) : + builder.ConfigureMauiHandlers(handlers => handlers.AddLibraryHandlers()); + } + + private static IMauiHandlersCollection AddLibraryCompatibilityRenderers(this IMauiHandlersCollection handlers) + { +#if __ANDROID__ + //handlers.AddCompatibilityRenderer(typeof(HtmlLabel), typeof(Droid.HtmlLabelRenderer)); +#elif __IOS__ + //handlers.AddCompatibilityRenderer(typeof(HtmlLabel), typeof(iOS.HtmlLabelRenderer)); +#elif WINDOWS10_0_17763_0_OR_GREATER + //handlers.AddCompatibilityRenderer(typeof(HtmlLabel), typeof(UWP.HtmlLabelRenderer)); +#endif + return handlers; + } + + private static IMauiHandlersCollection AddLibraryHandlers(this IMauiHandlersCollection handlers) + { + handlers.AddHandler(); + + return handlers; + } + } +} diff --git a/Maui/HtmlLabel/HyperTextLabel.Maui.csproj b/Maui/HtmlLabel/HyperTextLabel.Maui.csproj new file mode 100644 index 0000000..a42d29c --- /dev/null +++ b/Maui/HtmlLabel/HyperTextLabel.Maui.csproj @@ -0,0 +1,109 @@ + + + + net6.0;net6.0-android;net6.0-ios + $(TargetFrameworks);net6.0-windows10.0.19041.0 + + + true + true + enable + + 14.2 + 14.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + HyperTextLabel.Maui + HyperTextLabel.Maui + Maui.Plugin.HtmlLabel + HtmlLabel.Maui.Plugin + icon.png + Maui.Plugin.HtmlLabel: display HTML content in labels + MAUI, windows, uwp, ios, android, Maui.HtmlLabel, html + Maui.Plugin.HtmlLabel + Maui.Plugin.HtmlLabel: display HTML content in labels + $(AssemblyName) ($(TargetFramework)) + $(Version)$(VersionSuffix) + Matteo Bortolazzo + Matteo Bortolazzo + en + © Matteo Bortolazzo. All rights reserved. + https://github.com/matteobortolazzo/HtmlLabelPlugin + $(DefineConstants); + false + LICENSE + true + https://github.com/matteobortolazzo/HtmlLabelPlugin + portable + Debug;Release + Latest + false + + + + true + + + + + true + + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.0.8 + + + + diff --git a/Maui/HtmlLabel/Platforms/Android/HtmlLabelExtensions.cs b/Maui/HtmlLabel/Platforms/Android/HtmlLabelExtensions.cs new file mode 100644 index 0000000..35bfe9e --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Android/HtmlLabelExtensions.cs @@ -0,0 +1,153 @@ +using System.ComponentModel; +using Android.OS; +using Android.Text; +using Android.Text.Method; +using Android.Text.Style; +using Java.Lang; +using HyperTextLabel.Maui.Platform.Droid; +using AndroidX.AppCompat.Widget; +using Microsoft.Maui.Platform; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Utilities; +using HyperTextLabel.Maui.Extensions; + +namespace HyperTextLabel.Maui.Platforms.Droid +{ + internal static class HtmlLabelExtensions + { + private const string _tagUlRegex = "[uU][lL]"; + private const string _tagOlRegex = "[oO][lL]"; + private const string _tagLiRegex = "[lL][iI]"; + + public static void UpdateText(this AppCompatTextView view, IHtmlLabel label) + { + Color linkColor = label.LinkColor; + if (!linkColor.IsDefault()) + { + view.SetLinkTextColor(linkColor.ToPlatform()); + } + + view.SetIncludeFontPadding(false); + var isRtl = AppInfo.RequestedLayoutDirection == LayoutDirection.RightToLeft; + var styledHtml = new RendererHelper(label, label.Text, DevicePlatform.Android, isRtl).ToString(); + /* + * Android's TextView doesn't support lists. + * List tags must be replaces with custom tags, + * that it will be renderer by a custom tag handler. + */ + styledHtml = styledHtml + ?.ReplaceTag(_tagUlRegex, ListTagHandler.TagUl) + ?.ReplaceTag(_tagOlRegex, ListTagHandler.TagOl) + ?.ReplaceTag(_tagLiRegex, ListTagHandler.TagLi); + + if (styledHtml != null) + { + SetText(view, label, styledHtml); + } + } + + public static void UpdateUnderlineText(this AppCompatTextView view, IHtmlLabel label) + { + } + + public static void UpdateLinkColor(this AppCompatTextView view, IHtmlLabel label) + { + } + + public static void UpdateBrowserLaunchOptions(this AppCompatTextView view, IHtmlLabel label) + { + } + + public static void UpdateAndroidLegacyMode(this AppCompatTextView view, IHtmlLabel label) + { + } + + public static void UpdateAndroidListIndent(this AppCompatTextView view, IHtmlLabel label) + { + } + + private static void SetText(AppCompatTextView control, IHtmlLabel htmlLabel, string html) + { + // Set the type of content and the custom tag list handler + using var listTagHandler = new ListTagHandler(htmlLabel.AndroidListIndent); // KWI-FIX: added AndroidListIndent parameter + var imageGetter = new UrlImageParser(control); + FromHtmlOptions fromHtmlOptions = htmlLabel.AndroidLegacyMode ? FromHtmlOptions.ModeLegacy : FromHtmlOptions.ModeCompact; + ISpanned sequence = Build.VERSION.SdkInt >= BuildVersionCodes.N ? + Html.FromHtml(html, fromHtmlOptions, imageGetter, listTagHandler) : + Html.FromHtml(html, imageGetter, listTagHandler); + using var strBuilder = new SpannableStringBuilder(sequence); + + // Make clickable links + if (!htmlLabel.GestureRecognizers.Any()) + { + control.MovementMethod = LinkMovementMethod.Instance; + URLSpan[] urls = strBuilder + .GetSpans(0, sequence.Length(), Class.FromType(typeof(URLSpan))) + .Cast() + .ToArray(); + foreach (URLSpan span in urls) + { + MakeLinkClickable(strBuilder, span, htmlLabel); + } + } + + // Android adds an unnecessary "\n" that must be removed + using ISpanned value = RemoveTrailingNewLines(strBuilder); + + // Finally sets the value of the TextView + control.SetText(value, global::Android.Widget.TextView.BufferType.Spannable); + } + + private static ISpanned RemoveTrailingNewLines(ICharSequence text) + { + var builder = new SpannableStringBuilder(text); + + var count = 0; + for (int i = 1; i <= text.Length(); i++) + { + if (!'\n'.Equals(text.CharAt(text.Length() - i))) + break; + + count++; + } + + if (count > 0) + _ = builder.Delete(text.Length() - count, text.Length()); + + return builder; + } + + private static void MakeLinkClickable(ISpannable strBuilder, URLSpan span, IHtmlLabel htmlLabel) + { + var start = strBuilder.GetSpanStart(span); + var end = strBuilder.GetSpanEnd(span); + SpanTypes flags = strBuilder.GetSpanFlags(span); + var clickable = new HtmlLabelClickableSpan(htmlLabel, span); + strBuilder.SetSpan(clickable, start, end, flags); + strBuilder.RemoveSpan(span); + } + + private class HtmlLabelClickableSpan : ClickableSpan + { + private readonly IHtmlLabel _label; + private readonly URLSpan _span; + + public HtmlLabelClickableSpan(IHtmlLabel label, URLSpan span) + { + _label = label; + _span = span; + } + + public override void UpdateDrawState(TextPaint ds) + { + base.UpdateDrawState(ds); + ds.UnderlineText = _label.UnderlineText; + } + + public override void OnClick(global::Android.Views.View widget) + { + RendererHelper.HandleUriClick(_label, _span.URL); + } + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Android/HtmlLabelHandler.cs b/Maui/HtmlLabel/Platforms/Android/HtmlLabelHandler.cs new file mode 100644 index 0000000..c93a95e --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Android/HtmlLabelHandler.cs @@ -0,0 +1,50 @@ +using HyperTextLabel.Maui.Platforms.Droid; +using HyperTextLabel.Maui.Controls; + +using PlatformView = AndroidX.AppCompat.Widget.AppCompatTextView; + +namespace HyperTextLabel.Maui.Handlers +{ + public partial class HtmlLabelHandler : Microsoft.Maui.Handlers.LabelHandler + { + protected override void ConnectHandler(PlatformView platformView) + { + base.ConnectHandler(platformView); + } + + protected override void DisconnectHandler(PlatformView platformView) + { + base.DisconnectHandler(platformView); + } + + public static void MapLabelText(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateText(label); + } + + public static void MapUnderlineText(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateUnderlineText(label); + } + + public static void MapLinkColor(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateLinkColor(label); + } + + public static void MapBrowserLaunchOptions(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateBrowserLaunchOptions(label); + } + + public static void MapAndroidLegacyMode(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidLegacyMode(label); + } + + public static void MapAndroidListIndent(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidListIndent(label); + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Android/ListBuilder.cs b/Maui/HtmlLabel/Platforms/Android/ListBuilder.cs new file mode 100644 index 0000000..3f90971 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Android/ListBuilder.cs @@ -0,0 +1,142 @@ +using Android.Text; +using Android.Text.Style; +using Android.Widget; +using Java.Lang; + +namespace HyperTextLabel.Maui.Platform.Droid +{ + internal class ListBuilder + { + private int _listIndent = 20; // KWI-FIX : changed from constant to prop + + + private readonly int _gap = 0; + private readonly LiGap _liGap; + private readonly ListBuilder _parent = null; + + private int _liIndex = -1; + private int _liStart = -1; + + public ListBuilder(int listIndent) // KWI-FIX: added listIndent + { + _listIndent = listIndent; + _parent = null; + _gap = 0; + _liGap = GetLiGap(null); + } + + private ListBuilder(ListBuilder parent, bool ordered, int listIndent) // KWI-FIX: added listIndent + { + _listIndent = listIndent; + _parent = parent; + _liGap = parent._liGap; + _gap = parent._gap + _listIndent + _liGap.GetGap(ordered); + _liIndex = ordered ? 0 : -1; + } + + public ListBuilder StartList(bool ordered, IEditable output) + { + if (_parent == null && output.Length() > 0) + { + _ = output.Append("\n "); + } + return new ListBuilder(this, ordered, _listIndent); // KWI-FIX: pass thru listIndent + } + + public void AddListItem(bool isOpening, IEditable output) + { + if (isOpening) + { + EnsureParagraphBoundary(output); + _liStart = output.Length(); + + var lineStart = IsOrdered() + ? ++_liIndex + ". " + : "• "; + _ = output.Append(lineStart); + } + else + { + if (_liStart >= 0) + { + EnsureParagraphBoundary(output); + using var leadingMarginSpan = new LeadingMarginSpanStandard(_gap - _liGap.GetGap(IsOrdered()), _gap); + output.SetSpan(leadingMarginSpan, _liStart, output.Length(), SpanTypes.ExclusiveExclusive); + _liStart = -1; + } + } + } + + public ListBuilder CloseList(IEditable output) + { + EnsureParagraphBoundary(output); + ListBuilder result = _parent; + if (result == null) + { + result = this; + } + + if (result._parent == null) + { + _ = output.Append('\n'); + } + + return result; + } + + private bool IsOrdered() + { + return _liIndex >= 0; + } + + private static void EnsureParagraphBoundary(IEditable output) + { + if (output.Length() == 0) + { + return; + } + + var lastChar = output.CharAt(output.Length() - 1); + if (lastChar != '\n') + { + _ = output.Append('\n'); + } + } + + private class LiGap + { + private readonly int _orderedGap; + private readonly int _unorderedGap; + + internal LiGap(int orderedGap, int unorderedGap) + { + _orderedGap = orderedGap; + _unorderedGap = unorderedGap; + } + + public int GetGap(bool ordered) + { + return ordered ? _orderedGap : _unorderedGap; + } + } + + private static LiGap GetLiGap(TextView tv) + { + var orderedGap = tv == null ? 40 : ComputeWidth(tv, true); + var unorderedGap = tv == null ? 30 : ComputeWidth(tv, false); + + return new LiGap(orderedGap, unorderedGap); + } + + private static int ComputeWidth(TextView tv, bool isOrdered) + { + Android.Graphics.Paint paint = tv.Paint; + using var bounds = new Android.Graphics.Rect(); + var startString = isOrdered ? "99. " : "• "; + paint.GetTextBounds(startString, 0, startString.Length, bounds); + var width = bounds.Width(); + var pt = Android.Util.TypedValue.ApplyDimension(Android.Util.ComplexUnitType.Pt, width, tv.Context.Resources.DisplayMetrics); + return (int)pt; + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Android/ListTagHandler.cs b/Maui/HtmlLabel/Platforms/Android/ListTagHandler.cs new file mode 100644 index 0000000..97a3f04 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Android/ListTagHandler.cs @@ -0,0 +1,46 @@ +using Android.Text; +using Org.Xml.Sax; + +namespace HyperTextLabel.Maui.Platform.Droid +{ + /// + /// Tag handler to support HTML lists. + /// + internal class ListTagHandler : Java.Lang.Object, Html.ITagHandler + { + public const string TagUl = "ULC"; + public const string TagOl = "OLC"; + public const string TagLi = "LIC"; + + private ListBuilder _listBuilder; // KWI-FIX: removed new, set in constructor + public ListTagHandler(int listIndent) // KWI-FIX: added constructor with listIndent property + { + _listBuilder = new ListBuilder(listIndent); + } + + public void HandleTag(bool isOpening, string tag, IEditable output, IXMLReader xmlReader) + { + tag = tag.ToUpperInvariant(); + var isItem = tag == TagLi; + + // Is list item + if (isItem) + { + _listBuilder.AddListItem(isOpening, output); + } + // Is list + else + { + if (isOpening) + { + var isOrdered = tag == TagOl; + _listBuilder = _listBuilder.StartList(isOrdered, output); + } + else + { + _listBuilder = _listBuilder.CloseList(output); + } + } + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Android/URLImageParser.cs b/Maui/HtmlLabel/Platforms/Android/URLImageParser.cs new file mode 100644 index 0000000..e64e944 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Android/URLImageParser.cs @@ -0,0 +1,107 @@ +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Text; +using Android.Widget; +using Java.Net; + +namespace HyperTextLabel.Maui.Platform.Droid +{ + internal class UrlDrawable : BitmapDrawable + { + public Drawable Drawable { get; set; } + + public override void Draw(Canvas canvas) + { + if (Drawable != null) + { + Drawable.Draw(canvas); ; + } + } + } + + internal class ImageGetterAsyncTask : AsyncTask + { + private readonly UrlDrawable _urlDrawable; + private readonly TextView _container; + + public ImageGetterAsyncTask(UrlDrawable urlDrawable, TextView container) + { + _urlDrawable = urlDrawable; + _container = container; + } + + protected override Drawable RunInBackground(params string[] @params) + { + var source = @params[0]; + return FetchDrawable(source); + } + + protected override void OnPostExecute(Drawable result) + { + if (result == null) + { + return; + } + + // Set the correct bound according to the result from HTTP call + _urlDrawable.SetBounds(0, 0, 0 + result.IntrinsicWidth, 0 + result.IntrinsicHeight); + + // Change the reference of the current drawable to the result from the HTTP call + _urlDrawable.Drawable = result; + + // Redraw the image by invalidating the container + _container.Invalidate(); + + // For ICS + _container.SetHeight(_container.Height + result.IntrinsicHeight); + + // Pre ICS + _container.Ellipsize = null; + } + + private Drawable FetchDrawable(string urlString) + { + try + { + Stream stream = Fetch(urlString); + var drawable = Drawable.CreateFromStream(stream, "src"); + drawable.SetBounds(0, 0, 0 + drawable.IntrinsicWidth, 0 + drawable.IntrinsicHeight); + return drawable; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(@" ERROR: ", ex.Message); + return null; + } + } + + private static Stream Fetch(string urlString) + { + var url = new URL(urlString); + var urlConnection = (HttpURLConnection)url.OpenConnection(); + Stream stream = urlConnection.InputStream; + return stream; + } + } + + internal class UrlImageParser : Java.Lang.Object, Html.IImageGetter + { + private readonly TextView _container; + + public UrlImageParser(TextView container) + { + _container = container; + } + + public Drawable GetDrawable(string source) + { + var urlDrawable = new UrlDrawable(); + + var asyncTask = new ImageGetterAsyncTask(urlDrawable, _container); + _ = asyncTask.Execute(source); + + return urlDrawable; + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Windows/Behavior.cs b/Maui/HtmlLabel/Platforms/Windows/Behavior.cs new file mode 100644 index 0000000..efdfe56 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Windows/Behavior.cs @@ -0,0 +1,24 @@ +using Microsoft.UI.Xaml; +using Microsoft.Xaml.Interactivity; + +namespace HyperTextLabel.Maui.Platforms.Windows +{ + internal abstract class Behavior : DependencyObject, IBehavior + { + public void Attach(DependencyObject associatedObject) + { + AssociatedObject = associatedObject; + OnAttached(); + } + + public void Detach() => OnDetaching(); + + protected virtual void OnAttached() { } + + protected virtual void OnDetaching() { } + + protected DependencyObject AssociatedObject { get; set; } + + DependencyObject IBehavior.AssociatedObject => AssociatedObject; + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Platforms/Windows/GenericBehavior.cs b/Maui/HtmlLabel/Platforms/Windows/GenericBehavior.cs new file mode 100644 index 0000000..5c09c00 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Windows/GenericBehavior.cs @@ -0,0 +1,18 @@ +using Microsoft.UI.Xaml; + +namespace HyperTextLabel.Maui.Platforms.Windows +{ + internal abstract class Behavior : Behavior where T : DependencyObject + { + protected new T AssociatedObject => base.AssociatedObject as T; + + protected override void OnAttached() + { + base.OnAttached(); + if (AssociatedObject == null) + { + throw new InvalidOperationException("AssociatedObject is not of the right type"); + } + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Platforms/Windows/HtmlLabelExtensions.cs b/Maui/HtmlLabel/Platforms/Windows/HtmlLabelExtensions.cs new file mode 100644 index 0000000..203259e --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Windows/HtmlLabelExtensions.cs @@ -0,0 +1,55 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.Xaml.Interactivity; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Utilities; + +namespace HyperTextLabel.Maui.Platforms.Windows +{ + internal static class HtmlLabelExtensions + { + public static void UpdateText(this TextBlock view, IHtmlLabel label) + { + ProcessText( view, label); + } + + public static void UpdateUnderlineText(this TextBlock view, IHtmlLabel label) + { + } + + public static void UpdateLinkColor(this TextBlock view, IHtmlLabel label) + { + } + + public static void UpdateBrowserLaunchOptions(this TextBlock view, IHtmlLabel label) + { + } + + public static void UpdateAndroidLegacyMode(this TextBlock view, IHtmlLabel label) + { + } + + public static void UpdateAndroidListIndent(this TextBlock view, IHtmlLabel label) + { + } + + private static void ProcessText(TextBlock view, IHtmlLabel label) + { + // Gets the complete HTML string + var isRtl = AppInfo.RequestedLayoutDirection == LayoutDirection.RightToLeft; + var styledHtml = new RendererHelper(label, label.Text, DevicePlatform.WinUI, isRtl).ToString(); + if (styledHtml == null) + { + return; + } + + view.Text = styledHtml; + + // Adds the HtmlTextBehavior because UWP's TextBlock + // does not natively support HTML content + var behavior = new HtmlTextBehavior() { HtmlLabel = label }; + BehaviorCollection behaviors = Interaction.GetBehaviors(view); + behaviors.Clear(); + behaviors.Add(behavior); + } + } +} diff --git a/Maui/HtmlLabel/Platforms/Windows/HtmlLabelHandler.cs b/Maui/HtmlLabel/Platforms/Windows/HtmlLabelHandler.cs new file mode 100644 index 0000000..59f8798 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Windows/HtmlLabelHandler.cs @@ -0,0 +1,50 @@ +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Platforms.Windows; + +using PlatformView = Microsoft.UI.Xaml.Controls.TextBlock; + +namespace HyperTextLabel.Maui.Handlers +{ + public partial class HtmlLabelHandler : Microsoft.Maui.Handlers.LabelHandler + { + protected override void ConnectHandler(PlatformView platformView) + { + base.ConnectHandler(platformView); + } + + protected override void DisconnectHandler(PlatformView platformView) + { + base.DisconnectHandler(platformView); + } + + public static void MapLabelText(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateText(label); + } + + public static void MapUnderlineText(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateUnderlineText(label); + } + + public static void MapLinkColor(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateLinkColor(label); + } + + public static void MapBrowserLaunchOptions(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateBrowserLaunchOptions(label); + } + + public static void MapAndroidLegacyMode(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidLegacyMode(label); + } + + public static void MapAndroidListIndent(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidListIndent(label); + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Platforms/Windows/HtmlTextBehavior.cs b/Maui/HtmlLabel/Platforms/Windows/HtmlTextBehavior.cs new file mode 100644 index 0000000..f6ba3ce --- /dev/null +++ b/Maui/HtmlLabel/Platforms/Windows/HtmlTextBehavior.cs @@ -0,0 +1,226 @@ +using System.Text.RegularExpressions; +using System.Xml.Linq; +using Microsoft.Maui.Controls.Platform; +using Microsoft.UI.Xaml.Documents; +using Microsoft.UI.Xaml; +using Microsoft.Maui.Platform; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Utilities; + +using PlatformView = Microsoft.UI.Xaml.Controls.TextBlock; +using Span = Microsoft.UI.Xaml.Documents.Span; + +namespace HyperTextLabel.Maui.Platforms.Windows +{ + internal class HtmlTextBehavior : Behavior + { + // All the supported tags + internal const string _elementA = "A"; + internal const string _elementB = "B"; + internal const string _elementBr = "BR"; + internal const string _elementEm = "EM"; + internal const string _elementI = "I"; + internal const string _elementP = "P"; + internal const string _elementStrong = "STRONG"; + internal const string _elementU = "U"; + internal const string _elementUl = "UL"; + internal const string _elementLi = "LI"; + internal const string _elementDiv = "DIV"; + + public IHtmlLabel HtmlLabel { get; set; } + + protected override void OnAttached() + { + base.OnAttached(); + + AssociatedObject.Loaded += OnAssociatedObjectLoaded; + AssociatedObject.LayoutUpdated += OnAssociatedObjectLayoutUpdated; + } + + protected override void OnDetaching() + { + base.OnDetaching(); + + AssociatedObject.Loaded -= OnAssociatedObjectLoaded; + AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated; + } + + private void OnAssociatedObjectLayoutUpdated(object sender, object o) => UpdateText(); + + private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e) => UpdateText(); + + private void UpdateText() + { + if (AssociatedObject == null) + { + return; + } + + if (string.IsNullOrEmpty(AssociatedObject.Text)) + { + return; + } + + AssociatedObject.Loaded -= OnAssociatedObjectLoaded; + AssociatedObject.LayoutUpdated -= OnAssociatedObjectLayoutUpdated; + + var text = AssociatedObject.Text; + + // Just incase we are not given text with elements. + var modifiedText = $"
{text}
"; + + var linkRegex = new Regex(@" 0) + { + foreach (Match match in matches) + { + for (var i = 1; i < match.Groups.Count; i++) + { + Group group = match.Groups[i]; + var escapedUri = Uri.EscapeDataString(group.Value); + modifiedText = modifiedText.Replace(group.Value, escapedUri, StringComparison.InvariantCulture); + } + } + System.Diagnostics.Debug.WriteLine(@$"ERROR: ${matches}"); + } + + modifiedText = Regex.Replace(modifiedText, "
", "

", RegexOptions.IgnoreCase) + .Replace("\n", String.Empty, StringComparison.OrdinalIgnoreCase) // KWI-FIX Enters resulted in multiple lines + .Replace(" ", " ", StringComparison.OrdinalIgnoreCase); // KWI-FIX   is not supported by the UWP TextBlock + + // reset the text because we will add to it. + AssociatedObject.Inlines.Clear(); + + var element = XElement.Parse(modifiedText); + ParseText(element, AssociatedObject.Inlines, HtmlLabel); + } + + private static void ParseText(XElement element, InlineCollection inlines, IHtmlLabel label) + { + if (element == null) + { + return; + } + + InlineCollection currentInlines = inlines; + var elementName = element.Name.ToString().ToUpperInvariant(); + switch (elementName) + { + case _elementA: + var link = new Hyperlink(); + XAttribute href = element.Attribute("href"); + var unescapedUri = Uri.UnescapeDataString(href?.Value); + if (href != null) + { + try + { + link.NavigateUri = new Uri(unescapedUri); + } + catch (FormatException) { /* href is not valid */ } + } + link.Click += (sender, e) => + { + sender.NavigateUri = null; + RendererHelper.HandleUriClick(label, unescapedUri); + }; + if ( !ControlsColorExtensions.IsDefault( label.LinkColor ) ) + { + link.Foreground = label.LinkColor.ToPlatform(); + } + if (!label.UnderlineText) + { + link.UnderlineStyle = UnderlineStyle.None; + } + inlines.Add(link); + currentInlines = link.Inlines; + break; + case _elementB: + case _elementStrong: + var bold = new Bold(); + inlines.Add(bold); + currentInlines = bold.Inlines; + break; + case _elementI: + case _elementEm: + var italic = new Italic(); + inlines.Add(italic); + currentInlines = italic.Inlines; + break; + case _elementU: + var underline = new Underline(); + inlines.Add(underline); + currentInlines = underline.Inlines; + break; + case _elementBr: + inlines.Add(new LineBreak()); + break; + case _elementP: + // Add two line breaks, one for the current text and the second for the gap. + if (AddLineBreakIfNeeded(inlines)) + { + inlines.Add(new LineBreak()); + } + + var paragraphSpan = new Span(); + inlines.Add(paragraphSpan); + currentInlines = paragraphSpan.Inlines; + break; + case _elementLi: + inlines.Add(new LineBreak()); + inlines.Add(new Run { Text = " • " }); + break; + case _elementUl: + case _elementDiv: + _ = AddLineBreakIfNeeded(inlines); + var divSpan = new Span(); + inlines.Add(divSpan); + currentInlines = divSpan.Inlines; + break; + } + foreach (XNode node in element.Nodes()) + { + if (node is XText textElement) + { + currentInlines.Add(new Run { Text = textElement.Value }); + } + else + { + ParseText(node as XElement, currentInlines, label); + } + } + + // Add newlines for paragraph tags + if (elementName == "ElementP") + { + currentInlines.Add(new LineBreak()); + } + } + private static bool AddLineBreakIfNeeded(InlineCollection inlines) + { + if (inlines.Count <= 0) + { + return false; + } + + Inline lastInline = inlines[inlines.Count - 1]; + while ((lastInline is Span)) + { + var span = (Span)lastInline; + if (span.Inlines.Count > 0) + { + lastInline = span.Inlines[span.Inlines.Count - 1]; + } + } + + if (lastInline is LineBreak) + { + return false; + } + + inlines.Add(new LineBreak()); + return true; + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Platforms/iOS/Extensions.cs b/Maui/HtmlLabel/Platforms/iOS/Extensions.cs new file mode 100644 index 0000000..a480f99 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/iOS/Extensions.cs @@ -0,0 +1,440 @@ +using Foundation; +using System.Runtime.InteropServices; +using UIKit; +using Microsoft.Maui.Controls.Compatibility.Platform.iOS; +using HyperTextLabel.Maui.Controls; + +namespace HyperTextLabel.Maui.Platforms.iOS +{ + internal static class ColorExtensions + { + internal static bool IsEqualToColor(this UIColor self, UIColor otherColor) + { + NFloat r; + NFloat g; + NFloat b; + NFloat a; + + self.GetRGBA(out r, out g, out b, out a); + NFloat r2; + NFloat g2; + NFloat b2; + NFloat a2; + + otherColor.GetRGBA(out r2, out g2, out b2, out a2); + + return r == r2 && g == g2 && b == b2 && a == a2; + } + + internal static UIColor LabelColor + { + get + { + if ( IsiOS13OrNewer ) + return UIColor.Label; + + return UIColor.Black; + } + } + + static bool? s_isiOS13OrNewer; + private static bool IsiOS13OrNewer + { + get + { + if ( !s_isiOS13OrNewer.HasValue ) + s_isiOS13OrNewer = UIDevice.CurrentDevice.CheckSystemVersion( 13, 0 ); + + return s_isiOS13OrNewer.Value; + } + } + } + + internal static class FontExtensionss + { + // static readonly string _defaultFontName = UIFont.SystemFontOfSize(12).Name; + // internal static bool IsBold(UIFont font) + // { + // UIFontDescriptor fontDescriptor = font.FontDescriptor; + // UIFontDescriptorSymbolicTraits traits = fontDescriptor.SymbolicTraits; + // return traits.HasFlag(UIFontDescriptorSymbolicTraits.Bold); + // } + + // internal static UIFont Bold(this UIFont font) + // { + // UIFontDescriptor fontDescriptor = font.FontDescriptor; + // UIFontDescriptorSymbolicTraits traits = fontDescriptor.SymbolicTraits; + // traits = traits | UIFontDescriptorSymbolicTraits.Bold; + // UIFontDescriptor boldFontDescriptor = fontDescriptor.CreateWithTraits(traits); + // return UIFont.FromDescriptor(boldFontDescriptor, font.PointSize); + // } + // internal static UIFont Italic(this UIFont self) + // { + // UIFontDescriptor fontDescriptor = self.FontDescriptor; + // UIFontDescriptorSymbolicTraits traits = fontDescriptor.SymbolicTraits; + // traits = traits | UIFontDescriptorSymbolicTraits.Italic; + // UIFontDescriptor boldFontDescriptor = fontDescriptor.CreateWithTraits(traits); + // return UIFont.FromDescriptor(boldFontDescriptor, self.PointSize); + // } + + internal static UIFont WithTraitsOfFont(this UIFont self, UIFont font) + { + UIFontDescriptor fontDescriptor = self.FontDescriptor; + UIFontDescriptorSymbolicTraits traits = fontDescriptor.SymbolicTraits; + traits = traits | font.FontDescriptor.SymbolicTraits; + UIFontDescriptor boldFontDescriptor = fontDescriptor.CreateWithTraits(traits); + return UIFont.FromDescriptor(boldFontDescriptor, self.PointSize); + } + // public static UIFont ToUIFont(this Font self) => ToNativeFont(self); + + // internal static UIFont ToUIFont(this IFontElement element) => ToNativeFont(element); + + // static UIFont _ToNativeFont(string family, float size, FontAttributes attributes) + // { + // var bold = (attributes & FontAttributes.Bold) != 0; + // var italic = (attributes & FontAttributes.Italic) != 0; + + // if (family != null && family != _defaultFontName) + // { + // try + // { + // UIFont result = null; + // if (UIFont.FamilyNames.Contains(family)) + // { + // var descriptor = new UIFontDescriptor().CreateWithFamily(family); + + // if (bold || italic) + // { + // var traits = (UIFontDescriptorSymbolicTraits)0; + // if (bold) + // traits = traits | UIFontDescriptorSymbolicTraits.Bold; + // if (italic) + // traits = traits | UIFontDescriptorSymbolicTraits.Italic; + + // descriptor = descriptor.CreateWithTraits(traits); + // result = UIFont.FromDescriptor(descriptor, size); + // if (result != null) + // return result; + // } + // } + + // var cleansedFont = CleanseFontName(family); + // result = UIFont.FromName(cleansedFont, size); + // if (family.StartsWith(".SFUI", System.StringComparison.InvariantCultureIgnoreCase)) + // { + // var fontWeight = family.Split('-').LastOrDefault(); + + // if (!string.IsNullOrWhiteSpace(fontWeight) && System.Enum.TryParse(fontWeight, true, out var uIFontWeight)) + // { + // result = UIFont.SystemFontOfSize(size, uIFontWeight); + // return result; + // } + + // result = UIFont.SystemFontOfSize(size, UIFontWeight.Regular); + // return result; + // } + // if (result == null) + // result = UIFont.FromName(family, size); + // if (result != null) + // return result; + // } + // catch + // { + // Debug.WriteLine("Could not load font named: {0}", family); + // } + // } + + // if (bold && italic) + // { + // var defaultFont = UIFont.SystemFontOfSize(size); + + // var descriptor = defaultFont.FontDescriptor.CreateWithTraits(UIFontDescriptorSymbolicTraits.Bold | UIFontDescriptorSymbolicTraits.Italic); + // return UIFont.FromDescriptor(descriptor, 0); + // } + + // if (italic) + // return UIFont.ItalicSystemFontOfSize(size); + + // if (bold) + // return UIFont.BoldSystemFontOfSize(size); + + // return UIFont.SystemFontOfSize(size); + // } + + // internal static string CleanseFontName(string fontName) + // { + + // //First check Alias + // var (hasFontAlias, fontPostScriptName) = FontRegistrar.HasFont(fontName); + // if (hasFontAlias) + // return fontPostScriptName; + + // var fontFile = FontFile.FromString(fontName); + + // if (!string.IsNullOrWhiteSpace(fontFile.Extension)) + // { + // var (hasFont, filePath) = FontManager FontRegistrar.HasFont(fontFile.FileNameWithExtension()); + // if (hasFont) + // return filePath ?? fontFile.PostScriptName; + // } + // else + // { + // foreach (var ext in FontFile.Extensions) + // { + + // var formated = fontFile.FileNameWithExtension(ext); + // var (hasFont, filePath) = FontRegistrar.HasFont(formated); + // if (hasFont) + // return filePath; + // } + // } + // return fontFile.PostScriptName; + // } + + // static readonly Dictionary ToUiFont = new Dictionary(); + + // internal static bool IsDefault(this Span self) + // { + // return self.FontFamily == null && self.FontSize == Device.GetNamedSize(NamedSize.Default, typeof(Label), true) && + // self.FontAttributes == FontAttributes.None; + // } + + // static NativeFont ToNativeFont(this IFontElement element) + // { + // var fontFamily = element.FontFamily; + // var fontSize = (float)element.FontSize; + // var fontAttributes = element.FontAttributes; + // return ToNativeFont(fontFamily, fontSize, fontAttributes, _ToNativeFont); + // } + + // static NativeFont ToNativeFont(this Font self) + // { + // var size = (float)self.FontSize; + // if (self.UseNamedSize) + // { + // switch (self.NamedSize) + // { + // case NamedSize.Micro: + // size = 12; + // break; + // case NamedSize.Small: + // size = 14; + // break; + // case NamedSize.Medium: + // size = 17; // as defined by iOS documentation + // break; + // case NamedSize.Large: + // size = 22; + // break; + // default: + // size = 17; + // break; + // } + // } + + // var fontAttributes = self.FontAttributes; + + // return ToNativeFont(self.FontFamily, size, fontAttributes, _ToNativeFont); + // } + + // static NativeFont ToNativeFont(string family, float size, FontAttributes attributes, Func factory) + // { + // var key = new ToNativeFontFontKey(family, size, attributes); + + // lock (ToUiFont) + // { + // NativeFont value; + // if (ToUiFont.TryGetValue(key, out value)) + // return value; + // } + + // var generatedValue = factory(family, size, attributes); + + // lock (ToUiFont) + // { + // NativeFont value; + // if (!ToUiFont.TryGetValue(key, out value)) + // ToUiFont.Add(key, value = generatedValue); + // return value; + // } + // } + + // struct ToNativeFontFontKey + // { + // internal ToNativeFontFontKey(string family, float size, FontAttributes attributes) + // { + // _family = family; + // _size = size; + // _attributes = attributes; + // } + //#pragma warning disable 0414 // these are not called explicitly, but they are used to establish uniqueness. allow it! + // string _family; + // float _size; + // FontAttributes _attributes; + //#pragma warning restore 0414 + // } + } + + internal static class AttributedStringExtensions + { + internal static void SetLineHeight(this NSMutableAttributedString mutableHtmlString, IHtmlLabel element) + { + if (element.LineHeight < 0) + { + return; + } + + using (var lineHeightStyle = new NSMutableParagraphStyle { LineHeightMultiple = (NFloat)element.LineHeight }) + { + mutableHtmlString.AddAttribute(UIStringAttributeKey.ParagraphStyle, lineHeightStyle, new NSRange(0, mutableHtmlString.Length)); + } + } + + internal static void SetLinksStyles(this NSMutableAttributedString mutableHtmlString, IHtmlLabel element) + { + + UIStringAttributes linkAttributes = null; + + if (!element.UnderlineText) + { + linkAttributes ??= new UIStringAttributes(); + linkAttributes.UnderlineStyle = NSUnderlineStyle.None; + }; + if (!element.LinkColor.IsDefault()) + { + linkAttributes ??= new UIStringAttributes(); + linkAttributes.ForegroundColor = element.LinkColor.ToUIColor(); + }; + + mutableHtmlString.EnumerateAttribute(UIStringAttributeKey.Link, new NSRange(0, mutableHtmlString.Length), NSAttributedStringEnumeration.LongestEffectiveRangeNotRequired, + (NSObject value, NSRange range, ref bool stop) => + { + if (value != null && value is NSUrl url) + { + // Applies the style + if (linkAttributes != null) + { + mutableHtmlString.AddAttributes(linkAttributes, range); + } + } + }); + + } + internal static NSMutableAttributedString RemoveTrailingNewLines(this NSAttributedString htmlString) + { + var count = 0; + for (int i = 1; i <= htmlString.Length; i++) + { + if ("\n" != htmlString.Substring(htmlString.Length - i, 1).Value) + break; + + count++; + } + + if (count > 0) + htmlString = htmlString.Substring(0, htmlString.Length - count); + + return new NSMutableAttributedString(htmlString); + } + + internal static NSMutableAttributedString AddCharacterSpacing(this NSAttributedString attributedString, string text, double characterSpacing) + { + if (attributedString == null && characterSpacing == 0) + return null; + + NSMutableAttributedString mutableAttributedString = attributedString as NSMutableAttributedString; + if (attributedString == null || attributedString.Length == 0) + { + mutableAttributedString = text == null ? new NSMutableAttributedString() : new NSMutableAttributedString(text); + } + else + { + mutableAttributedString = new NSMutableAttributedString(attributedString); + } + + AddKerningAdjustment(mutableAttributedString, mutableAttributedString.Value, characterSpacing); + + return mutableAttributedString; + } + internal static bool HasCharacterAdjustment(this NSMutableAttributedString mutableAttributedString) + { + if (mutableAttributedString == null) + return false; + + NSRange removalRange; + var attributes = mutableAttributedString.GetAttributes(0, out removalRange); + + for (uint i = 0; i < attributes.Count; i++) + if (attributes.Keys[i] is NSString nSString && nSString == UIStringAttributeKey.KerningAdjustment) + return true; + + return false; + } + + internal static void AddKerningAdjustment(NSMutableAttributedString mutableAttributedString, string text, double characterSpacing) + { + try + { + if ( !string.IsNullOrEmpty(text) ) + { + if ( characterSpacing == 0 && !mutableAttributedString.HasCharacterAdjustment() ) + return; + + mutableAttributedString.AddAttribute + ( + UIStringAttributeKey.KerningAdjustment, + NSObject.FromObject( characterSpacing ), new NSRange( 0, text.Length - 1 ) + ); + } + } + catch ( Exception e ) + { + Console.WriteLine( e ); + + throw; + } + } + + internal static bool IsHorizontal(this Button.ButtonContentLayout layout) => + layout.Position == Button.ButtonContentLayout.ImagePosition.Left || + layout.Position == Button.ButtonContentLayout.ImagePosition.Right; + } + + internal static class AlignmentExtensions + { + internal static UITextAlignment ToNativeTextAlignment(this TextAlignment alignment, EffectiveFlowDirection flowDirection) + { + var isLtr = (flowDirection & EffectiveFlowDirection.RightToLeft) == EffectiveFlowDirection.RightToLeft; + switch (alignment) + { + case TextAlignment.Center: + return UITextAlignment.Center; + case TextAlignment.End: + if (isLtr) + return UITextAlignment.Right; + else + return UITextAlignment.Left; + default: + if (isLtr) + return UITextAlignment.Left; + else + return UITextAlignment.Right; + } + } + + internal static UIControlContentVerticalAlignment ToNativeTextAlignment(this TextAlignment alignment) + { + switch (alignment) + { + case TextAlignment.Center: + return UIControlContentVerticalAlignment.Center; + case TextAlignment.End: + return UIControlContentVerticalAlignment.Bottom; + case TextAlignment.Start: + return UIControlContentVerticalAlignment.Top; + default: + return UIControlContentVerticalAlignment.Top; + } + } + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Platforms/iOS/HtmlLabelExtensions.cs b/Maui/HtmlLabel/Platforms/iOS/HtmlLabelExtensions.cs new file mode 100644 index 0000000..b222ed5 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/iOS/HtmlLabelExtensions.cs @@ -0,0 +1,117 @@ +using Foundation; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Utilities; +using Microsoft.Maui.Platform; +using UIKit; + +namespace HyperTextLabel.Maui.Platforms.iOS +{ + internal static class HtmlLabelExtensions + { + public static void UpdateText(this MauiLabel view, IHtmlLabel label, IFontManager fontManager) + { + if (string.IsNullOrWhiteSpace(label?.Text)) + { + view.Text = string.Empty; + return; + } + + var uiFont = fontManager.GetFont(label.Font, UIFont.LabelFontSize); + view.Font = uiFont; + + var linkColor = label.LinkColor; + if (!linkColor.IsDefault()) + { + view.TintColor = linkColor.ToPlatform(); + } + var isRtl = AppInfo.RequestedLayoutDirection == LayoutDirection.RightToLeft; + var styledHtml = new RendererHelper(label, label.Text, DevicePlatform.iOS, isRtl).ToString(); + SetText(styledHtml, view, label); + view.SetNeedsDisplay(); + } + + public static void UpdateUnderlineText(this MauiLabel view, IHtmlLabel label) + { + } + + public static void UpdateLinkColor(this MauiLabel view, IHtmlLabel label) + { + } + + public static void UpdateBrowserLaunchOptions(this MauiLabel view, IHtmlLabel label) + { + } + + public static void UpdateAndroidLegacyMode(this MauiLabel view, IHtmlLabel label) + { + } + + public static void UpdateAndroidListIndent(this MauiLabel view, IHtmlLabel label) + { + } + + private static void SetText(string html, MauiLabel view, IHtmlLabel label) + { + // Create HTML data sting + var stringType = new NSAttributedStringDocumentAttributes + { + DocumentType = NSDocumentType.HTML, + StringEncoding = NSStringEncoding.UTF8 + }; + var nsError = new NSError(); + + var htmlData = NSData.FromString(html, NSStringEncoding.Unicode); + + using var htmlString = new NSAttributedString(htmlData, stringType, out _, ref nsError); + var mutableHtmlString = htmlString.RemoveTrailingNewLines(); + + mutableHtmlString.EnumerateAttributes(new NSRange(0, mutableHtmlString.Length), NSAttributedStringEnumeration.None, + (NSDictionary value, NSRange range, ref bool stop) => + { + try + { + var md = new NSMutableDictionary(value); + var font = md[UIStringAttributeKey.Font] as UIFont; + + if (font != null) + { + md[UIStringAttributeKey.Font] = view.Font.WithTraitsOfFont(font); + } + else + { + md[UIStringAttributeKey.Font] = view.Font; + } + + var foregroundColor = md[UIStringAttributeKey.ForegroundColor] as UIColor; + if (foregroundColor == null || foregroundColor.IsEqualToColor(UIColor.Black)) + { + md[UIStringAttributeKey.ForegroundColor] = view.TextColor; + } + + mutableHtmlString.SetAttributes(md, range); + } + catch (Exception e) + { + Console.WriteLine(e); + + throw; + } + }); + + mutableHtmlString.SetLineHeight(label); + mutableHtmlString.SetLinksStyles(label); + view.AttributedText = mutableHtmlString; + } + + //private static bool NavigateToUrl(NSUrl url) + //{ + // if (url == null) + // { + // throw new ArgumentNullException(nameof(url)); + // } + // // Try to handle uri, if it can't be handled, fall back to IOS his own handler. + // return !RendererHelper.HandleUriClick(Element, url.AbsoluteString); + //} + + } +} diff --git a/Maui/HtmlLabel/Platforms/iOS/HtmlLabelHandler.cs b/Maui/HtmlLabel/Platforms/iOS/HtmlLabelHandler.cs new file mode 100644 index 0000000..746505f --- /dev/null +++ b/Maui/HtmlLabel/Platforms/iOS/HtmlLabelHandler.cs @@ -0,0 +1,54 @@ +using Microsoft.Maui.Platform; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Extensions; +using HyperTextLabel.Maui.Platforms.iOS; + +using PlatformView = Microsoft.Maui.Platform.MauiLabel; + +namespace HyperTextLabel.Maui.Handlers +{ + public partial class HtmlLabelHandler : Microsoft.Maui.Handlers.LabelHandler + { + protected override void ConnectHandler(PlatformView platformView) + { + base.ConnectHandler(platformView); + } + + protected override void DisconnectHandler(PlatformView platformView) + { + base.DisconnectHandler(platformView); + } + + public static void MapLabelText(HtmlLabelHandler handler, IHtmlLabel label) + { + var fontManager = handler.GetRequiredService(); + + handler.PlatformView?.UpdateText(label, fontManager); + } + + public static void MapUnderlineText(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateUnderlineText(label); + } + + public static void MapLinkColor(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateLinkColor(label); + } + + public static void MapBrowserLaunchOptions(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateBrowserLaunchOptions(label); + } + + public static void MapAndroidLegacyMode(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidLegacyMode(label); + } + + public static void MapAndroidListIndent(HtmlLabelHandler handler, IHtmlLabel label) + { + handler.PlatformView?.UpdateAndroidListIndent(label); + } + } +} diff --git a/Maui/HtmlLabel/Platforms/iOS/LinkTapHelper.cs b/Maui/HtmlLabel/Platforms/iOS/LinkTapHelper.cs new file mode 100644 index 0000000..a180650 --- /dev/null +++ b/Maui/HtmlLabel/Platforms/iOS/LinkTapHelper.cs @@ -0,0 +1,78 @@ +using CoreGraphics; +using Foundation; +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Utilities; +using System.Runtime.InteropServices; +using UIKit; + +namespace HyperTextLabel.Maui.Platforms.iOS +{ + internal static class LinkTapHelper + { + public static readonly NSString CustomLinkAttribute = new NSString("LabelLink"); + + public static void HandleLinkTap(this UILabel control, IHtmlLabel element) + { + void TapHandler(UITapGestureRecognizer tap) + { + var detectedUrl = DetectTappedUrl(tap, (UILabel)tap.View); + RendererHelper.HandleUriClick(element, detectedUrl); + } + + var tapGesture = new UITapGestureRecognizer(TapHandler); + control.AddGestureRecognizer(tapGesture); + control.UserInteractionEnabled = true; + } + + private static string DetectTappedUrl(UIGestureRecognizer tap, UILabel control) + { + CGRect bounds = control.Bounds; + NSAttributedString attributedText = control.AttributedText; + + // Setup containers + using var textContainer = new NSTextContainer(bounds.Size) + { + LineFragmentPadding = 0, + LineBreakMode = control.LineBreakMode, + MaximumNumberOfLines = (nuint)control.Lines + }; + + using var layoutManager = new NSLayoutManager(); + layoutManager.AddTextContainer(textContainer); + + using var textStorage = new NSTextStorage(); + textStorage.SetString(attributedText); + + using var fontAttributeName = new NSString("NSFont"); + var textRange = new NSRange(0, control.AttributedText.Length); + textStorage.AddAttribute(fontAttributeName, control.Font, textRange); + textStorage.AddLayoutManager(layoutManager); + CGRect textBoundingBox = layoutManager.GetUsedRect(textContainer); + + // Calculate align offset + static NFloat GetAlignOffset(UITextAlignment textAlignment) => textAlignment switch + { + UITextAlignment.Center => 0.5f, + UITextAlignment.Right => 1f, + _ => 0.0f, + }; + NFloat alignmentOffset = GetAlignOffset(control.TextAlignment); + NFloat xOffset = (bounds.Size.Width - textBoundingBox.Size.Width) * alignmentOffset - textBoundingBox.Location.X; + NFloat yOffset = (bounds.Size.Height - textBoundingBox.Size.Height) * alignmentOffset - textBoundingBox.Location.Y; + + // Find tapped character + CGPoint locationOfTouchInLabel = tap.LocationInView(control); + var locationOfTouchInTextContainer = new CGPoint(locationOfTouchInLabel.X - xOffset, locationOfTouchInLabel.Y - yOffset); + var characterIndex = (nint)layoutManager.GetCharacterIndex(locationOfTouchInTextContainer, textContainer); + + if (characterIndex >= attributedText.Length) + { + return null; + } + + // Try to get the URL + NSObject linkAttributeValue = attributedText.GetAttribute(CustomLinkAttribute, characterIndex, out NSRange range); + return linkAttributeValue is NSUrl url ? url.AbsoluteString : null; + } + } +} diff --git a/Maui/HtmlLabel/Platforms/iOS/TextViewDelegate.cs b/Maui/HtmlLabel/Platforms/iOS/TextViewDelegate.cs new file mode 100644 index 0000000..16ce1bc --- /dev/null +++ b/Maui/HtmlLabel/Platforms/iOS/TextViewDelegate.cs @@ -0,0 +1,25 @@ +using Foundation; +using UIKit; + +namespace HyperTextLabel.Maui.Platforms.iOS +{ + internal class TextViewDelegate : UITextViewDelegate + { + private Func _navigateTo; + + public TextViewDelegate(Func navigateTo) + { + _navigateTo = navigateTo; + } + + public override bool ShouldInteractWithUrl(UITextView textView, NSUrl URL, NSRange characterRange) + { + if (_navigateTo != null) + { + return _navigateTo(URL); + } + return true; + } + } + +} diff --git a/Maui/HtmlLabel/Utilities/HttpUtility.cs b/Maui/HtmlLabel/Utilities/HttpUtility.cs new file mode 100644 index 0000000..81969f8 --- /dev/null +++ b/Maui/HtmlLabel/Utilities/HttpUtility.cs @@ -0,0 +1,45 @@ +namespace HyperTextLabel.Maui.Utilities +{ + public static class HttpUtility + { + public static Dictionary> ParseQueryString(this Uri uri, bool decode = true) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (uri.Query.Length == 0 || uri.Query.Length == 1 && uri.Query[0] == '?') + { + return new Dictionary>(); + } + + var query = uri.Query; + if (query[0] == '?') + { + query = query.Substring(1); + } + + return query + .Split('&') + .Select(p => p.Split('=')) + .Select(p => p.Length == 1 ? (p[0], "true") : (p[0], p[1])) + .GroupBy(p => p.Item1.ToUpperInvariant()) + .ToDictionary( + g => g.Key, + g => + { + var values = g.Select(p => p.Item2); + if (decode) + values = values.Select(Uri.UnescapeDataString); + return values.ToList(); + }); + } + + public static string GetFirst(this Dictionary> qParams, string key) => + qParams.Get(key)?.FirstOrDefault(); + + public static List Get(this Dictionary> qParams, string key) => + qParams != null && key != null && qParams.ContainsKey(key.ToUpperInvariant()) ? qParams[key.ToUpperInvariant()] : null; + } +} \ No newline at end of file diff --git a/Maui/HtmlLabel/Utilities/RendererHelper.cs b/Maui/HtmlLabel/Utilities/RendererHelper.cs new file mode 100644 index 0000000..d95d810 --- /dev/null +++ b/Maui/HtmlLabel/Utilities/RendererHelper.cs @@ -0,0 +1,205 @@ +using HyperTextLabel.Maui.Controls; +using HyperTextLabel.Maui.Extensions; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; + +[assembly: InternalsVisibleTo("HyperTextLabel.Maui.Shared.Tests")] +namespace HyperTextLabel.Maui.Utilities +{ + internal class RendererHelper + { + private readonly IHtmlLabel _label; + private readonly DevicePlatform _runtimePlatform; + private readonly bool _isRtl; + private readonly string _text; + private readonly IList> _styles; + private static readonly string[] SupportedProperties = { + Label.TextProperty.PropertyName, + Label.TextColorProperty.PropertyName, + Label.FontAttributesProperty.PropertyName, + Label.FontFamilyProperty.PropertyName, + Label.FontSizeProperty.PropertyName, + Label.LineBreakModeProperty.PropertyName, + Label.HorizontalTextAlignmentProperty.PropertyName, + Label.LineHeightProperty.PropertyName, + Label.PaddingProperty.PropertyName, + HtmlLabel.LinkColorProperty.PropertyName + }; + + public RendererHelper(IHtmlLabel label, string text, DevicePlatform runtimePlatform, bool isRtl) + { + _label = label ?? throw new ArgumentNullException(nameof(label)); + _runtimePlatform = runtimePlatform; + _isRtl = isRtl; + _text = text?.Trim(); + _styles = new List>(); + } + + public void AddFontAttributesStyle(FontAttributes fontAttributes) + { + if (fontAttributes == FontAttributes.Bold) + { + AddStyle("font-weight", "bold"); + } + else if (fontAttributes == FontAttributes.Italic) + { + AddStyle("font-style", "italic"); + } + } + + public void AddFontFamilyStyle(string fontFamily) + { + string GetSystemFont() => _runtimePlatform == DevicePlatform.iOS ? "-apple-system" : + _runtimePlatform == DevicePlatform.Android ? "Roboto" : + _runtimePlatform == DevicePlatform.WinUI ? "Segoe UI" : + "system-ui"; + + var fontFamilyValue = string.IsNullOrWhiteSpace(fontFamily) + ? GetSystemFont() + : fontFamily; + AddStyle("font-family", $"'{fontFamilyValue}'"); + } + + public void AddFontSizeStyle(double fontSize) + { + AddStyle("font-size", $"{fontSize}px"); + } + + public void AddTextColorStyle(Color color) + { + if (color.IsDefault()) + { + return; + } + + var red = (int)(color.Red * 255); + var green = (int)(color.Green * 255); + var blue = (int)(color.Blue * 255); + var alpha = color.Alpha; + var hex = $"#{red:X2}{green:X2}{blue:X2}"; + var rgba = $"rgba({red},{green},{blue},{alpha})"; + AddStyle("color", hex); + AddStyle("color", rgba); + } + + public void AddHorizontalTextAlignStyle(TextAlignment textAlignment) + { + if (textAlignment == TextAlignment.Start) + { + AddStyle("text-align", _isRtl ? "right" : "left"); + } + else if (textAlignment == TextAlignment.Center) + { + AddStyle("text-align", "center"); + } + else if (textAlignment == TextAlignment.End) + { + AddStyle("text-align", _isRtl ? "left" : "right"); + } + } + + public override string ToString() + { + if (string.IsNullOrWhiteSpace(_text)) + { + return null; + } + + AddFontAttributesStyle(_label.FontAttributes); + AddFontFamilyStyle(_label.FontFamily); + AddTextColorStyle(_label.TextColor); + AddHorizontalTextAlignStyle(_label.HorizontalTextAlignment); + AddFontSizeStyle(_label.FontSize); + + var style = GetStyle(); + return $"
{_text}
"; + } + + public string GetStyle() + { + var builder = new StringBuilder(); + + foreach (KeyValuePair style in _styles) + { + _ = builder.Append($"{style.Key}:{style.Value};"); + } + + var css = builder.ToString(); + if (_styles.Any()) + { + css = css.Substring(0, css.Length - 1); + } + + return css; + } + + public static bool RequireProcess(string propertyName) => SupportedProperties.Contains(propertyName); + + /// + /// Handles the Uri for the following types: + /// - Web url + /// - Email + /// - Telephone + /// - SMS + /// - GEO + /// + /// + /// + /// true if the uri has been handled correctly, false if the uri is not handled because of an error + public static bool HandleUriClick(IHtmlLabel label, string url) + { + + if (url == null || !Uri.IsWellFormedUriString(WebUtility.UrlEncode(url), UriKind.RelativeOrAbsolute)) + { + return false; + } + + var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url); + + label.SendNavigating(args); + + if (args.Cancel) + { + // Uri is handled because it is cancled; + return true; + } + bool result = false; + var uri = new Uri(url); + + if (uri.IsHttp()) + { + uri.LaunchBrowser(label.BrowserLaunchOptions); + result = true; + } + else if (uri.IsEmail()) + { + result = uri.LaunchEmail(); + } + else if (uri.IsTel()) + { + result = uri.LaunchTel(); + } + else if (uri.IsSms()) + { + result = uri.LaunchSms(); + } + else if (uri.IsGeo()) + { + result = uri.LaunchMaps(); + } + else + { + result = Launcher.TryOpenAsync(uri).Result; + } + // KWI-FIX What to do if the navigation failed? I assume not to spawn the SendNavigated event or introduce a fail bit on the args + label.SendNavigated(args); + return result; + } + + private void AddStyle(string selector, string value) + { + _styles.Add(new KeyValuePair(selector, value)); + } + } +} diff --git a/Maui/nuget.config b/Maui/nuget.config new file mode 100644 index 0000000..01ef0af --- /dev/null +++ b/Maui/nuget.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file