diff --git a/components/MarkdownTextBlock/OpenSolution.bat b/components/MarkdownTextBlock/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/MarkdownTextBlock/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/MarkdownTextBlock/samples/Assets/MarkdownTextBlock.png b/components/MarkdownTextBlock/samples/Assets/MarkdownTextBlock.png new file mode 100644 index 00000000..8435bcaa Binary files /dev/null and b/components/MarkdownTextBlock/samples/Assets/MarkdownTextBlock.png differ diff --git a/components/MarkdownTextBlock/samples/Dependencies.props b/components/MarkdownTextBlock/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/MarkdownTextBlock/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/MarkdownTextBlock/samples/MarkdownTextBlock.Samples.csproj b/components/MarkdownTextBlock/samples/MarkdownTextBlock.Samples.csproj new file mode 100644 index 00000000..b2dd30a9 --- /dev/null +++ b/components/MarkdownTextBlock/samples/MarkdownTextBlock.Samples.csproj @@ -0,0 +1,16 @@ + + + MarkdownTextBlock + + + + + + + + + + PreserveNewest + + + diff --git a/components/MarkdownTextBlock/samples/MarkdownTextBlock.md b/components/MarkdownTextBlock/samples/MarkdownTextBlock.md new file mode 100644 index 00000000..8932e560 --- /dev/null +++ b/components/MarkdownTextBlock/samples/MarkdownTextBlock.md @@ -0,0 +1,34 @@ +--- +title: MarkdownTextBlock +author: nerocui +description: A control for displaying markdown natively. +keywords: MarkdownTextBlock, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: StatusAndInfo +experimental: true +discussion-id: 0 +issue-id: 0 +icon: Assets/MarkdownTextBlock.png +--- + + + + + + + + + +# MarkdownTextBlock + +MarkdownTextBlock is a evolution of the existing MarkdownTextBlock in the community toolkit. This new implementation uses the popular [Markdig](https://github.com/xoofx/markdig) library for parsing. This solves some long standing bugs and feature gaps in our existing implementation. + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +> [!Sample MarkdownTextBlockCustomSample] diff --git a/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml b/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml new file mode 100644 index 00000000..7173b399 --- /dev/null +++ b/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml.cs b/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml.cs new file mode 100644 index 00000000..01e3127d --- /dev/null +++ b/components/MarkdownTextBlock/samples/MarkdownTextBlockCustomSample.xaml.cs @@ -0,0 +1,611 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +namespace MarkdownTextBlockExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSample(id: nameof(MarkdownTextBlockCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(CommunityToolkit.Labs.WinUI.MarkdownTextBlock)} custom control.")] +public sealed partial class MarkdownTextBlockCustomSample : Page +{ + private MarkdownConfig _config; + private MarkdownConfig _liveConfig; + private string _text; + private const string _markdown = @" +This control was originally written by [Nero Cui](https://github.com/nerocui) for [JitHub](https://github.com/JitHubApp/JitHubV2). The control is powered by the popular [Markdig](https://github.com/xoofx/markdig) markdown parsing library and *almost* supports the full markdown syntax, with a focus on super-efficient parsing and rendering. + +*Note:* For a full list of markdown syntax, see the [official syntax guide](http://daringfireball.net/projects/markdown/syntax). + +  + +**Try it live!** Type in the *unformatted text box*! + +  + +# COMMENTS + +Comments can be added in Markdown, and they won't be rendered to the screen. + +To create a comment, enclose in XML style comment tags: + +><\!-- Comments are now Implemented --> + +There is a Comment below this line. + + +  + +# PARAGRAPHS + +Paragraphs are delimited by a blank line. Simply starting text on a new line won't create a new paragraph; It will remain on the same line in the final, rendered version as the previous line. You need an extra, blank line to start a new paragraph. This is especially important when dealing with quotes and, to a lesser degree, lists. + +You can also add non-paragraph line breaks by ending a line with two spaces. The difference is subtle: + +Paragraph 1, Line 1 +Paragraph 1, Line 2 + +Paragraph 2 + +***** + +# FONT FORMATTING +### Italics + +Text can be displayed in an italic font by surrounding a word or words with either single asterisks (\*) or single underscores (\_). + +For example: + +>This sentence includes \*italic text\*. + +is displayed as: + +>This sentence includes *italic text*. + + +### Bold + +Text can be displayed in a bold font by surrounding a word or words with either double asterisks (\*) or double underscores (\_). + +For example: + +>This sentence includes \*\*bold text\*\*. + +is displayed as: + +>This sentence includes **bold text**. + +### Bold & Italics + +Text can be displayed in a bold font by surrounding a word or words with either triple asterisks (\*) or triple underscores (\_). + +For example: + +>This sentence includes \*\*\*bold & italic text\*\*\*. + +is displayed as: + +>This sentence includes ***bold & italic text***. + +### Strikethrough + +Text can be displayed in a strikethrough font by surrounding a word or words with double tildes (~~). For example: + +>This sentence includes ~ ~strikethrough text~ ~ + +>*(but with no spaces between the tildes; escape sequences [see far below] appear not to work with tildes, so I can't demonstrate the exact usage).* + +is displayed as: + +>This sentence includes ~~strikethrough text~~. + +### Superscript + +Text can be displayed in a superscript font by preceding it with a caret ( ^ ). + +>This sentence includes super^ script + +>*(but with no spaces after the caret; Like strikethrough, the superscript syntax doesn't play nicely with escape sequences).* + +is displayed as: + +>This sentence includes super^script. + +Superscripts can even be nested: just^like^this . + +However, note that the superscript font will be reset by a space. To get around this, you can enclose the text in the superscript with parentheses. The parentheses won't be displayed in the comment, and everything inside of them will be superscripted, regardless of spaces: + +>This sentence^ (has a superscript with multiple words) + +>*Once again, with no space after the caret.* + +is displayed as + +>This sentence^(has a superscript with multiple words) + +### Subscript + +Text can be displayed in a subscript font by preceding it with a caret ( ). + +>This sentence includes \sub\ script + +>This sentence includes subscript. + +### Headers + +Markdown supports 6 levels of headers (some of which don't actually display as headers in reddit): + +# Header 1 + +## Header 2 + +### Header 3 + +#### Header 4 + +##### Header 5 + +###### Header 6 + +...which can be created in a couple of different ways. Level 1 and 2 headers can be created by adding a line of equals signs (=) or dashes (\-), respectively, underneath the header text. + +However, *all* types of headers can be created with a second method. Simply prepend a number of hashes (#) corresponding to the header level you want, so: + +>\# Header 1 + +>\#\# Header 2 + +>\#\#\# Header 3 + +>\#\#\#\# Header 4 + +>\#\#\#\#\# Header 5 + +>\#\#\#\#\#\# Header 6 + +results in: + +># Header 1 + +>## Header 2 + +>### Header 3 + +>#### Header 4 + +>##### Header 5 + +>###### Header 6 + +Note: you can add hashes after the header text to balance out how the source code looks without affecting what is displayed. So: + +>\#\# Header 2 ## + +also produces: + +>## Header 2 ## + + +***** +# LISTS + +Markdown supports two types of lists: ordered and unordered. + +### Unordered Lists + +Prepend each element in the list with either a plus (+), dash (-), or asterisk (*) plus a space. Line openers can be mixed. So + +>\* Item 1 + +>\+ Item 2 + +>\- Item 3 + +results in + +>* Item 1 +>+ Item 2 +>- Item 3 + + + +### Ordered Lists + +Ordered lists work roughly the same way, but you prepend each item in the list with a number plus a period (.) plus a space. The number will increment by 1 starting from the number from the number of the first list item. So + +>7\. Item 1 + +>2\. Item 2 + +>5\. Item 3 + +results in + +>7. Item 1 +>2. Item 2 +>5. Item 3 + +Also, you can nest lists, like so: + +1. Ordered list item 1 + +2. * Bullet 1 in list item 2 + * Bullet 2 in list item 2 + +3. List item 3 + +Note: If your list items consist of multiple paragraphs, you can force each new paragraph to remain in the previous list item by indenting it by one tab or four spaces. So + +>\* This item has multiple paragraphs. +> +>(*four spaces here*)This is the second paragraph +> +>\* Item 2 + +results in: + +>* This item has multiple paragraphs. +> +> This is the second paragraph +>* Item 2 + + +### Task List +You can also use the GitHub style task list. The syntax is the following: + +- [ ] Task 1 +- [x] Task 2 +- [ ] Task 3 + + +Notice how the spaces in my source were stripped out? What if you need to preserve formatting? That brings us to: + +***** + +# CODE BLOCKS AND INLINE CODE + +Inline code is easy. Simply surround any text with backticks (\`), **not to be confused with apostrophes (')**. Anything between the backticks will be rendered in a fixed-width font, and none of the formatting syntax we're exploring will be applied. So + +>Here is some `` ` ``inline code with \*\*formatting\*\*`` ` `` + +is displayed as: + +>Here is some `inline code with **formatting**` + +Note that this is why you should use the normal apostrophe when typing out possessive nouns or contractions. Otherwise you may end up with something like: + +>I couldn`t believe that he didn`t know that! + +Sometimes you need to preserve indentation, too. In those cases, you can create a block code element by starting every line of your code with four spaces (followed by other spaces that will be preserved). You can get results like the following: + + public void main(Strings argv[]){ + System.out.println(""Hello world!""); + } + +Starting with Windows Community Toolkit v1.4, you can also use GitHub code notification by creating a block surrounded by 3x\` (3 backticks). This can also be used with Language Identifiers on the entering backticks, such as: + +\`\`\`csharp + +public static void Main(string[] args) +{ + Console.WriteLine(""Hello world!""); +} + +\`\`\` + +will produce: + +```csharp +public static void Main(string[] args) +{ + Console.WriteLine(""Hello world!""); +} +``` + +***** + +# LINKS + +There are a couple of ways to get HTML links. The easiest is to just paste a valid URL, which will be automatically parsed as a link. Like so: + +>http://en.wikipedia.org + +However, usually you'll want to have text that functions as a link. In that case, include the text inside of square brackets followed by the URL in parentheses. So: + +>\[Wikipedia\]\(http\://en.wikipedia.org). + +results in: + +>[Wikipedia](http://en.wikipedia.org). + +You can also provide tooltip text for links like so: + +>\[Wikipedia\]\(http\://en.wikipedia.org ""tooltip text""\). + +results in: + +>[Wikipedia](http://en.wikipedia.org ""tooltip text""). + +There are other methods of generating links that aren't appropriate for discussion-board style comments. See the [Markdown Syntax](http://daringfireball.net/projects/markdown/syntax#link) if you're interested in more info. + +  + +Relative links are also supported + +>\[Relative Link\]\(/Assets/Photos/Photos.json\) + +results in: + +>[Relative Link](/Assets/Photos/Photos.json) + +  + +>\[Relative Link 2\]\(../Photos/Photos.json\) + +results in: + +>[Relative Link 2](../Photos/Photos.json) + +**Note:** Relative Links has to be Manually Handled in `LinkClicked` Event. + +Custom Scheme's can be added now using `SchemeList` Property. Scheme's should be separated by a comma( , ) + +*Example*: + +If `SchemeList=""companyportal,randomscheme""` then markdown will render + +`companyportal://mycompanyportal.com` to companyportal://mycompanyportal.com + +and + +`randomscheme://www.randomscheme.render` to randomscheme://www.randomscheme.render + +***** + +# Email Links + +Emails can be used as Masked Links. + +>\[Email\]\(email@email.com) + +will be rendered to [Email](email@email.com) + + +***** + +# IMAGES + +To add an image, it is almost like a link. You just need to add a \! before. + +So inline image syntax looks like this: + +>\!\[Helpers Image](https\://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/Microsoft.Toolkit.Uwp.SampleApp/Assets/Helpers.png) + +which renders in: + +![Helpers Image](https://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/Microsoft.Toolkit.Uwp.SampleApp/Assets/Helpers.png) + +Rendering Images is now supported through prefix. use property **UriPrefix** + +  + +Example: if you set **UriPrefix** to **ms-appx://** then + +>\!\[Local Image](/Assets/NotificationAssets/Sunny-Square.png) + +  + +renders in + +![Local Image](/Assets/NotificationAssets/Sunny-Square.png) + +You can also specify image width like this: + +>\!\[SVG logo](https\://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =32) (width is set to 32) + +>\!\[SVG logo](https\://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =x64) (height is set to 64) + +>\!\[SVG logo](https\://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =128x64) (width=128, height=64) + +which renders in: + +![SVG logo](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =32) +![SVG logo](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =x64) +![SVG logo](https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg =128x64) + +MarkdownTextblock supports links wrapped with Images. + +>\[!\[image](https\://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/build/nuget.png)](https\://docs.microsoft.com/windows/uwpcommunitytoolkit/) + +will render into + +[![image](https://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/build/nuget.png)](https://docs.microsoft.com/windows/uwpcommunitytoolkit/) + +and when clicked will go to the Linked Page. + +MarkdownTextBlock also supports Reference based links. + +``` +[![image][1]][2] + +[1]:https://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/build/nuget.png +[2]:https://docs.microsoft.com/windows/uwpcommunitytoolkit/ + +``` + +will render into + +[![image][1]][2] + +[1]:https://raw.githubusercontent.com/CommunityToolkit/WindowsCommunityToolkit/main/build/nuget.png +[2]:https://docs.microsoft.com/windows/uwpcommunitytoolkit/ + +***** + +# BLOCK QUOTES + +You'll probably do a lot of quoting of other redditors. In those cases, you'll want to use block quotes. Simple begin each line you want quoted with a right angle bracket (>). Multiple angle brackets can be used for nested quotes. To cause a new paragraph to be quoted, begin that paragraph with another angle bracket. So the following: + + >Quote1 + + >Quote2.1 + >>Quote2.Nest1.1 + >> + >>Quote2.Nest1.2 + > + >Quote2.3 + + >Quote3.1 + >Quote3.2 + + >Quote4.1 + > + >Quote4.2 + + >Quote5.1 + Quote5.2 + + >Quote6 + + Plain text. + +Is displayed as: + + +>Quote1 + +>Quote2.1 +>>Quote2.Nest1.1 +>> +>>Quote2.Nest1.2 +> +>Quote2.3 + +>Quote3.1 +>Quote3.2 + +>Quote4.1 +> +>Quote4.2 + +>Quote5.1 +Quote5.2 + +>Quote6 + +Plain text. + +***** + +# EMOJIS + +You can use nearly all emojis from this [list](https://gist.github.com/rxaviers/7360908). Text like `:smile:` will display :smile: emoji. + +***** + +# MISCELLANEOUS + +### Yaml Headers + +The parsing of YAML metadata is rendered as a form. For example: + +title|date +:-:|:-: +Windows Community Toolkit|2018/10/17 + +Which is produced with the following markdown: + +>`---` +>`title: Windows Community Toolkit` +>`date: 2018/10/17` +>`---` + +When you use YAML, you should pay attention to: + +* Must be written at the beginning of the document. + +* The start and end are represented by three short horizontal lines respectively. + +* The format should conform to the YAML specification. + + + +### Tables + +Reddit has the ability to represent tabular data in fancy-looking tables. For example: + +some|header|labels +:---|:--:|---: +Left-justified|center-justified|right-justified +a|b|c +d|e|f + +Which is produced with the following markdown: + +>`some|header|labels` +>`:---|:--:|---:` +>`Left-justified|center-justified|right-justified` +>`a|b|c` +>`d|e|f` + +All you need to produce a table is a row of headers separated by ""pipes"" (**|**), a row indicating how to justify the columns, and 1 or more rows of data (again, pipe-separated). + +The only real ""magic"" is in the row between the headers and the data. It should ideally be formed with rows dashes separated by pipes. If you add a colon to the left of the dashes for a column, that column will be left-justified. To the right for right justification, and on both sides for centered data. If there's no colon, it defaults to left-justified. + +Any number of dashes will do, even just one. You can use none at all if you want it to default to left-justified, but it's just easier to see what you're doing if you put a few in there. + +Also note that the pipes (signifying the dividing line between cells) don't have to line up. You just need the same number of them in every row. + +### Escaping special characters + +If you need to display any of the special characters, you can escape that character with a backslash (\\). For example: + +>Escaped \\\*italics\\\* + +results in: + +>Escaped \*italics\* + +###Horizontal rules + +Finally, to create a horizontal rule, create a separate paragraph with 5 or more asterisks (\*). + +>\*\*\*\*\* + +results in + +>***** + +Source: https://www.reddit.com/r/reddit.com/comments/6ewgt/reddit_markdown_primer_or_how_do_you_do_all_that/c03nik6 +"; + + public MarkdownConfig MarkdownConfig + { + get => _config; + set => _config = value; + } + + public MarkdownConfig LiveMarkdownConfig + { + get => _liveConfig; + set => _liveConfig = value; + } + + public string Text + { + get => _text; + set => _text = value; + } + + public MarkdownTextBlockCustomSample() + { + this.InitializeComponent(); + _config = new MarkdownConfig(); + _liveConfig = new MarkdownConfig(); + _text = _markdown; + MarkdownTextBox.Text = "# Hello World\n\n"; + } +} diff --git a/components/MarkdownTextBlock/src/CommunityToolkit.WinUI.Controls.MarkdownTextBlock.csproj b/components/MarkdownTextBlock/src/CommunityToolkit.WinUI.Controls.MarkdownTextBlock.csproj new file mode 100644 index 00000000..1c7ea1ce --- /dev/null +++ b/components/MarkdownTextBlock/src/CommunityToolkit.WinUI.Controls.MarkdownTextBlock.csproj @@ -0,0 +1,47 @@ + + + MarkdownTextBlock + This package contains MarkdownTextBlock. + + + CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns + enable + + + + + + + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MarkdownTextBlock.xaml + + + diff --git a/components/MarkdownTextBlock/src/DefaultSVGRenderer.cs b/components/MarkdownTextBlock/src/DefaultSVGRenderer.cs new file mode 100644 index 00000000..f50ba836 --- /dev/null +++ b/components/MarkdownTextBlock/src/DefaultSVGRenderer.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +internal class DefaultSVGRenderer : ISVGRenderer +{ + public async Task SvgToImage(string svgString) + { + SvgImageSource svgImageSource = new SvgImageSource(); + var image = new Image(); + // Create a MemoryStream object and write the SVG string to it + using (var memoryStream = new MemoryStream()) + using (var streamWriter = new StreamWriter(memoryStream)) + { + await streamWriter.WriteAsync(svgString); + await streamWriter.FlushAsync(); + + // Rewind the MemoryStream + memoryStream.Position = 0; + + // Load the SVG from the MemoryStream + await svgImageSource.SetSourceAsync(memoryStream.AsRandomAccessStream()); + } + + // Set the Source property of the Image control to the SvgImageSource object + image.Source = svgImageSource; + var size = Extensions.GetSvgSize(svgString); + if (size.Width != 0) + { + image.Width = size.Width; + } + if (size.Height != 0) + { + image.Height = size.Height; + } + return image; + } +} diff --git a/components/MarkdownTextBlock/src/Dependencies.props b/components/MarkdownTextBlock/src/Dependencies.props new file mode 100644 index 00000000..358ea283 --- /dev/null +++ b/components/MarkdownTextBlock/src/Dependencies.props @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/MarkdownTextBlock/src/Extensions.cs b/components/MarkdownTextBlock/src/Extensions.cs new file mode 100644 index 00000000..593d8c75 --- /dev/null +++ b/components/MarkdownTextBlock/src/Extensions.cs @@ -0,0 +1,725 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//using ColorCode; +//using ColorCode.Common; +//using ColorCode.Styling; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; +using System.Xml.Linq; +using System.Globalization; +using Windows.UI.ViewManagement; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +public static class Extensions +{ + private const string OneDarkBackground = "#282c34"; + private const string OneDarkPlainText = "#abb2bf"; + + private const string OneDarkXMLDelimiter = "#e06c75"; + private const string OneDarkXMLName = "#d19a66"; + private const string OneDarkXMLAttribute = "#61afef"; + private const string OneDarkXAMLCData = "#98c379"; + private const string OneDarkXMLComment = "#5c6370"; + + private const string OneDarkComment = "#5c6370"; + private const string OneDarkKeyword = "#c678dd"; + private const string OneDarkGray = "#9b9b9b"; + private const string OneDarkNumber = "#d19a66"; + private const string OneDarkClass = "#e5c07b"; + private const string OneDarkString = "#98c379"; + + public const string Blue = "#FF0000FF"; + public const string White = "#FFFFFFFF"; + public const string Black = "#FF000000"; + public const string DullRed = "#FFA31515"; + public const string Yellow = "#FFFFFF00"; + public const string Green = "#FF008000"; + public const string PowderBlue = "#FFB0E0E6"; + public const string Teal = "#FF008080"; + public const string Gray = "#FF808080"; + public const string Navy = "#FF000080"; + public const string OrangeRed = "#FFFF4500"; + public const string Purple = "#FF800080"; + public const string Red = "#FFFF0000"; + public const string MediumTurqoise = "FF48D1CC"; + public const string Magenta = "FFFF00FF"; + public const string OliveDrab = "#FF6B8E23"; + public const string DarkOliveGreen = "#FF556B2F"; + public const string DarkCyan = "#FF008B8B"; + + //public static ILanguage ToLanguage(this FencedCodeBlock fencedCodeBlock) + //{ + // switch (fencedCodeBlock.Info?.ToLower()) + // { + // case "aspx": + // return Languages.Aspx; + // case "aspx - vb": + // return Languages.AspxVb; + // case "asax": + // return Languages.Asax; + // case "ascx": + // return Languages.AspxCs; + // case "ashx": + // case "asmx": + // case "axd": + // return Languages.Ashx; + // case "cs": + // case "csharp": + // case "c#": + // return Languages.CSharp; + // case "xhtml": + // case "html": + // case "hta": + // case "htm": + // case "html.hl": + // case "inc": + // case "xht": + // return Languages.Html; + // case "java": + // case "jav": + // case "jsh": + // return Languages.Java; + // case "js": + // case "node": + // case "_js": + // case "bones": + // case "cjs": + // case "es": + // case "es6": + // case "frag": + // case "gs": + // case "jake": + // case "javascript": + // case "jsb": + // case "jscad": + // case "jsfl": + // case "jslib": + // case "jsm": + // case "jspre": + // case "jss": + // case "jsx": + // case "mjs": + // case "njs": + // case "pac": + // case "sjs": + // case "ssjs": + // case "xsjs": + // case "xsjslib": + // return Languages.JavaScript; + // case "posh": + // case "pwsh": + // case "ps1": + // case "psd1": + // case "psm1": + // return Languages.PowerShell; + // case "sql": + // case "cql": + // case "ddl": + // case "mysql": + // case "prc": + // case "tab": + // case "udf": + // case "viw": + // return Languages.Sql; + // case "vb": + // case "vbhtml": + // case "visual basic": + // case "vbnet": + // case "vb .net": + // case "vb.net": + // return Languages.VbDotNet; + // case "rss": + // case "xsd": + // case "wsdl": + // case "xml": + // case "adml": + // case "admx": + // case "ant": + // case "axaml": + // case "axml": + // case "builds": + // case "ccproj": + // case "ccxml": + // case "clixml": + // case "cproject": + // case "cscfg": + // case "csdef": + // case "csl": + // case "csproj": + // case "ct": + // case "depproj": + // case "dita": + // case "ditamap": + // case "ditaval": + // case "dll.config": + // case "dotsettings": + // case "filters": + // case "fsproj": + // case "fxml": + // case "glade": + // case "gml": + // case "gmx": + // case "grxml": + // case "gst": + // case "hzp": + // case "iml": + // case "ivy": + // case "jelly": + // case "jsproj": + // case "kml": + // case "launch": + // case "mdpolicy": + // case "mjml": + // case "mm": + // case "mod": + // case "mxml": + // case "natvis": + // case "ncl": + // case "ndproj": + // case "nproj": + // case "nuspec": + // case "odd": + // case "osm": + // case "pkgproj": + // case "pluginspec": + // case "proj": + // case "props": + // case "ps1xml": + // case "psc1": + // case "pt": + // case "qhelp": + // case "rdf": + // case "res": + // case "resx": + // case "rs": + // case "sch": + // case "scxml": + // case "sfproj": + // case "shproj": + // case "srdf": + // case "storyboard": + // case "sublime-snippet": + // case "sw": + // case "targets": + // case "tml": + // case "ui": + // case "urdf": + // case "ux": + // case "vbproj": + // case "vcxproj": + // case "vsixmanifest": + // case "vssettings": + // case "vstemplate": + // case "vxml": + // case "wixproj": + // case "workflow": + // case "wsf": + // case "wxi": + // case "wxl": + // case "wxs": + // case "x3d": + // case "xacro": + // case "xaml": + // case "xib": + // case "xlf": + // case "xliff": + // case "xmi": + // case "xml.dist": + // case "xmp": + // case "xproj": + // case "xspec": + // case "xul": + // case "zcml": + // return Languages.Xml; + // case "php": + // case "aw": + // case "ctp": + // case "fcgi": + // case "php3": + // case "php4": + // case "php5": + // case "phps": + // case "phpt": + // return Languages.Php; + // case "css": + // case "scss": + // case "less": + // return Languages.Css; + // case "cpp": + // case "c++": + // case "cc": + // case "cp": + // case "cxx": + // case "h": + // case "h++": + // case "hh": + // case "hpp": + // case "hxx": + // case "inl": + // case "ino": + // case "ipp": + // case "ixx": + // case "re": + // case "tcc": + // case "tpp": + // return Languages.Cpp; + // case "ts": + // case "tsx": + // case "cts": + // case "mts": + // return Languages.Typescript; + // case "fsharp": + // case "fs": + // case "fsi": + // case "fsx": + // return Languages.FSharp; + // case "koka": + // return Languages.Koka; + // case "hs": + // case "hs-boot": + // case "hsc": + // return Languages.Haskell; + // case "pandoc": + // case "md": + // case "livemd": + // case "markdown": + // case "mdown": + // case "mdwn": + // case "mdx": + // case "mkd": + // case "mkdn": + // case "mkdown": + // case "ronn": + // case "scd": + // case "workbook": + // return Languages.Markdown; + // case "fortran": + // case "f": + // case "f77": + // case "for": + // case "fpp": + // return Languages.Fortran; + // case "python": + // case "py": + // case "cgi": + // case "gyp": + // case "gypi": + // case "lmi": + // case "py3": + // case "pyde": + // case "pyi": + // case "pyp": + // case "pyt": + // case "pyw": + // case "rpy": + // case "smk": + // case "spec": + // case "tac": + // case "wsgi": + // case "xpy": + // return Languages.Python; + // case "matlab": + // case "m": + // return Languages.MATLAB; + // default: + // return Languages.JavaScript; + // } + //} + + public static string ToAlphabetical(this int index) + { + var alphabetical = "abcdefghijklmnopqrstuvwxyz"; + var remainder = index; + var stringBuilder = new StringBuilder(); + while (remainder != 0) + { + if (remainder > 26) + { + var newRemainder = remainder % 26; + var i = (remainder - newRemainder) / 26; + stringBuilder.Append(alphabetical[i - 1]); + remainder = newRemainder; + } + else + { + stringBuilder.Append(alphabetical[remainder - 1]); + remainder = 0; + } + } + return stringBuilder.ToString(); + } + + public static TextPointer? GetNextInsertionPosition(this TextPointer position, LogicalDirection logicalDirection) + { + // Check if the current position is already an insertion position + if (position.IsAtInsertionPosition(logicalDirection)) + { + // Return the same position + return position; + } + else + { + // Try to find the next insertion position by moving one symbol forward + TextPointer next = position.GetPositionAtOffset(1, logicalDirection); + // If there is no next position, return null + if (next == null) + { + return null; + } + else + { + // Recursively call this method until an insertion position is found or null is returned + return GetNextInsertionPosition(next, logicalDirection); + } + } + } + + public static bool IsAtInsertionPosition(this TextPointer position, LogicalDirection logicalDirection) + { + // Get the character rect of the current position + Rect currentRect = position.GetCharacterRect(logicalDirection); + // Try to get the next position by moving one symbol forward + TextPointer next = position.GetPositionAtOffset(1, logicalDirection); + // If there is no next position, return false + if (next == null) + { + return false; + } + else + { + // Get the character rect of the next position + Rect nextRect = next.GetCharacterRect(logicalDirection); + // Compare the two rects and return true if they are different + return !currentRect.Equals(nextRect); + } + } + + public static string RemoveImageSize(string? url) + { + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("URL must not be null or empty", nameof(url)); + } + + // Create a regex pattern to match the URL with width and height + var pattern = @"([^)\s]+)\s*=\s*\d+x\d+\s*"; + + // Replace the matched URL with the URL only + var result = Regex.Replace(url, pattern, "$1"); + + return result; + } + + public static Uri GetUri(string? url, string? @base) + { + var validUrl = RemoveImageSize(url); + Uri result; +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + if (Uri.TryCreate(validUrl, UriKind.Absolute, out result)) + { + //the url is already absolute + return result; + } + else if (!string.IsNullOrWhiteSpace(@base)) + { + //the url is relative, so append the base + //trim any trailing "/" from the base and any leading "/" from the url + @base = @base?.TrimEnd('/'); + validUrl = validUrl.TrimStart('/'); + //return the base and the url separated by a single "/" + return new Uri(@base + "/" + validUrl); + } + else + { + //the url is relative to the file system + //add ms-appx + validUrl = validUrl.TrimStart('/'); + return new Uri("ms-appx:///" + validUrl); + } +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + + //public static StyleDictionary GetOneDarkProStyle() + //{ + // return new StyleDictionary + // { + // new ColorCode.Styling.Style(ScopeName.PlainText) + // { + // Foreground = OneDarkPlainText, + // Background = OneDarkBackground, + // ReferenceName = "plainText" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlServerSideScript) + // { + // Background = Yellow, + // ReferenceName = "htmlServerSideScript" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlComment) + // { + // Foreground = OneDarkComment, + // ReferenceName = "htmlComment" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlTagDelimiter) + // { + // Foreground = OneDarkKeyword, + // ReferenceName = "htmlTagDelimiter" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlElementName) + // { + // Foreground = DullRed, + // ReferenceName = "htmlElementName" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlAttributeName) + // { + // Foreground = Red, + // ReferenceName = "htmlAttributeName" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlAttributeValue) + // { + // Foreground = OneDarkKeyword, + // ReferenceName = "htmlAttributeValue" + // }, + // new ColorCode.Styling.Style(ScopeName.HtmlOperator) + // { + // Foreground = OneDarkKeyword, + // ReferenceName = "htmlOperator" + // }, + // new ColorCode.Styling.Style(ScopeName.Comment) + // { + // Foreground = OneDarkComment, + // ReferenceName = "comment" + // }, + // new ColorCode.Styling.Style(ScopeName.XmlDocTag) + // { + // Foreground = OneDarkXMLComment, + // ReferenceName = "xmlDocTag" + // }, + // new ColorCode.Styling.Style(ScopeName.XmlDocComment) + // { + // Foreground = OneDarkXMLComment, + // ReferenceName = "xmlDocComment" + // }, + // new ColorCode.Styling.Style(ScopeName.String) + // { + // Foreground = OneDarkString, + // ReferenceName = "string" + // }, + // new ColorCode.Styling.Style(ScopeName.StringCSharpVerbatim) + // { + // Foreground = OneDarkString, + // ReferenceName = "stringCSharpVerbatim" + // }, + // new ColorCode.Styling.Style(ScopeName.Keyword) + // { + // Foreground = OneDarkKeyword, + // ReferenceName = "keyword" + // }, + // new ColorCode.Styling.Style(ScopeName.PreprocessorKeyword) + // { + // Foreground = OneDarkKeyword, + // ReferenceName = "preprocessorKeyword" + // }, + // new ColorCode.Styling.Style(ScopeName.Number) + // { + // Foreground=OneDarkNumber, + // ReferenceName="number" + // }, + + // new ColorCode.Styling.Style(ScopeName.CssPropertyName) + // { + // Foreground=OneDarkClass, + // ReferenceName="cssPropertyName" + // }, + + // new ColorCode.Styling.Style(ScopeName.CssPropertyValue) + // { + // Foreground=OneDarkString, + // ReferenceName="cssPropertyValue" + // }, + + // new ColorCode.Styling.Style(ScopeName.CssSelector) + // { + // Foreground=OneDarkKeyword, + // ReferenceName="cssSelector" + // }, + + // new ColorCode.Styling.Style(ScopeName.SqlSystemFunction) + // { + // Foreground=OneDarkClass, + // ReferenceName="sqlSystemFunction" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlAttribute) + // { + // Foreground=OneDarkXMLAttribute, + // ReferenceName="xmlAttribute" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlAttributeQuotes) + // { + // Foreground=OneDarkXMLDelimiter, + // ReferenceName="xmlAttributeQuotes" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlAttributeValue) + // { + // Foreground=OneDarkString, + // ReferenceName="xmlAttributeValue" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlCDataSection) + // { + // Foreground=OneDarkXAMLCData, + // ReferenceName="xmlCDataSection" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlComment) + // { + // Foreground=OneDarkXMLComment, + // ReferenceName="xmlComment" + // }, + + // new ColorCode.Styling.Style(ScopeName.XmlDelimiter) + // { + // Foreground=OneDarkXMLDelimiter, + // ReferenceName="xmlDelimiter" + // }, + // new ColorCode.Styling.Style(ScopeName.XmlName) + // { + // Foreground=OneDarkXMLName, + // ReferenceName="xmlName" + // } + // }; + //} + + public static HtmlElementType TagToType(this string tag) + { + switch (tag.ToLower()) + { + case "address": + case "article": + case "aside": + case "details": + case "blockquote": + case "canvas": + case "dd": + case "div": + case "dl": + case "dt": + case "fieldset": + case "figcaption": + case "figure": + case "footer": + case "form": + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + case "header": + case "hr": + case "li": + case "main": + case "nav": + case "noscript": + case "ol": + case "p": + case "pre": + case "section": + case "table": + case "tfoot": + case "ul": return HtmlElementType.Block; + default: return HtmlElementType.Inline; + } + } + + public static bool IsHeading(this string tag) + { + var headings = new List() { "h1", "h2", "h3", "h4", "h5", "h6" }; + return headings.Contains(tag.ToLower()); + } + + public static Size GetSvgSize(string svgString) + { + // Parse the SVG string as an XML document + XDocument svgDocument = XDocument.Parse(svgString); + + // Get the root element of the document +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + XElement svgElement = svgDocument.Root; + + // Get the height and width attributes of the root element +#pragma warning disable CS8602 // Dereference of a possibly null reference. + XAttribute heightAttribute = svgElement.Attribute("height"); +#pragma warning restore CS8602 // Dereference of a possibly null reference. + XAttribute widthAttribute = svgElement.Attribute("width"); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + // Convert the attribute values to double + double.TryParse(heightAttribute?.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out double height); + double.TryParse(widthAttribute?.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out double width); + + // Return the height and width as a tuple + return new(width, height); + } + + public static Size GetMarkdownImageSize(LinkInline link) + { + if (link == null || !link.IsImage) + { + throw new ArgumentException("Link must be an image", nameof(link)); + } + + var url = link.Url; + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("Link must have a valid URL", nameof(link)); + } + + // Try to parse the width and height from the URL + var parts = url?.Split('='); + if (parts?.Length == 2) + { + var dimensions = parts[1].Split('x'); + if (dimensions.Length == 2 && int.TryParse(dimensions[0], out int width) && int.TryParse(dimensions[1], out int height)) + { + return new(width, height); + } + } + + // not using this one as it's seems to be from the HTML renderer + //// Try to parse the width and height from the special attributes + //var attributes = link.GetAttributes(); + //if (attributes != null && attributes.Properties != null) + //{ + // var width = attributes.Properties.FirstOrDefault(p => p.Key == "width")?.Value; + // var height = attributes.Properties.FirstOrDefault(p => p.Key == "height")?.Value; + // if (!string.IsNullOrEmpty(width) && !string.IsNullOrEmpty(height) && int.TryParse(width, out int w) && int.TryParse(height, out int h)) + // { + // return new(w, h); + // } + //} + + // Return default values if no width and height are found + return new(0, 0); + } + + public static SolidColorBrush GetAccentColorBrush() + { + // Create a UISettings object to get the accent color + var uiSettings = new UISettings(); + + // Get the accent color as a Color value + var accentColor = uiSettings.GetColorValue(UIColorType.Accent); + + // Create a SolidColorBrush from the accent color + var accentBrush = new SolidColorBrush(accentColor); + + return accentBrush; + } +} diff --git a/components/MarkdownTextBlock/src/HtmlWriter.cs b/components/MarkdownTextBlock/src/HtmlWriter.cs new file mode 100644 index 00000000..7875c0f0 --- /dev/null +++ b/components/MarkdownTextBlock/src/HtmlWriter.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements.Html; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +internal class HtmlWriter +{ + public static void WriteHtml(WinUIRenderer renderer, HtmlNodeCollection nodes) + { + if (nodes == null || nodes.Count == 0) return; + foreach (var node in nodes) + { + if (node.NodeType == HtmlNodeType.Text) + { + renderer.WriteText(node.InnerText); + } + else if (node.NodeType == HtmlNodeType.Element && node.Name.TagToType() == TextElements.HtmlElementType.Inline) + { + // detect br here + var inlineTagName = node.Name.ToLower(); + if (inlineTagName == "br") + { + renderer.WriteInline(new MyLineBreak()); + } + else if (inlineTagName == "a") + { + IAddChild hyperLink; + if (node.ChildNodes.Any(n => n.Name != "#text")) + { + hyperLink = new MyHyperlinkButton(node, renderer.Config.BaseUrl); + } + else + { + hyperLink = new MyHyperlink(node, renderer.Config.BaseUrl); + } + renderer.Push(hyperLink); + WriteHtml(renderer, node.ChildNodes); + renderer.Pop(); + } + else if (inlineTagName == "img") + { + var image = new MyImage(node, renderer.Config); + renderer.WriteInline(image); + } + else + { + var inline = new MyInline(node); + renderer.Push(inline); + WriteHtml(renderer, node.ChildNodes); + renderer.Pop(); + } + } + else if (node.NodeType == HtmlNodeType.Element && node.Name.TagToType() == TextElements.HtmlElementType.Block) + { + IAddChild block; + var tag = node.Name.ToLower(); + if (tag == "details") + { + block = new MyDetails(node); + node.ChildNodes.Remove(node.ChildNodes.FirstOrDefault(x => x.Name == "summary" || x.Name == "header")); + renderer.Push(block); + WriteHtml(renderer, node.ChildNodes); + } + else if (tag.IsHeading()) + { + var heading = new MyHeading(node, renderer.Config); + renderer.Push(heading); + WriteHtml(renderer, node.ChildNodes); + } + else + { + block = new MyBlock(node); + renderer.Push(block); + WriteHtml(renderer, node.ChildNodes); + } + renderer.Pop(); + } + } + } +} diff --git a/components/MarkdownTextBlock/src/ISVGRenderer.cs b/components/MarkdownTextBlock/src/ISVGRenderer.cs new file mode 100644 index 00000000..c0b68d70 --- /dev/null +++ b/components/MarkdownTextBlock/src/ISVGRenderer.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +public interface ISVGRenderer +{ + Task SvgToImage(string svgString); +} diff --git a/components/MarkdownTextBlock/src/ImageProvider.cs b/components/MarkdownTextBlock/src/ImageProvider.cs new file mode 100644 index 00000000..2c5054f9 --- /dev/null +++ b/components/MarkdownTextBlock/src/ImageProvider.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +public interface IImageProvider +{ + Task GetImage(string url); + bool ShouldUseThisProvider(string url); +} diff --git a/components/MarkdownTextBlock/src/MarkdownConfig.cs b/components/MarkdownTextBlock/src/MarkdownConfig.cs new file mode 100644 index 00000000..0d941b41 --- /dev/null +++ b/components/MarkdownTextBlock/src/MarkdownConfig.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +public record MarkdownConfig +{ + public string? BaseUrl { get; set; } + public IImageProvider? ImageProvider { get; set; } + public ISVGRenderer? SVGRenderer { get; set; } + public MarkdownThemes Themes { get; set; } = MarkdownThemes.Default; + + public static MarkdownConfig Default = new(); +} diff --git a/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml b/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml new file mode 100644 index 00000000..30660e11 --- /dev/null +++ b/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml @@ -0,0 +1,25 @@ + + + + diff --git a/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml.cs b/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml.cs new file mode 100644 index 00000000..b3b98479 --- /dev/null +++ b/components/MarkdownTextBlock/src/MarkdownTextBlock.xaml.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; +using Markdig; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +[TemplatePart(Name = MarkdownContainerName, Type = typeof(Grid))] +public partial class MarkdownTextBlock : Control +{ + private const string MarkdownContainerName = "MarkdownContainer"; + private Grid? _container; + private MarkdownPipeline _pipeline; + private MyFlowDocument _document; + private WinUIRenderer? _renderer; + + private static readonly DependencyProperty ConfigProperty = DependencyProperty.Register( + nameof(Config), + typeof(MarkdownConfig), + typeof(MarkdownTextBlock), + new PropertyMetadata(null, OnConfigChanged) + ); + + private static readonly DependencyProperty TextProperty = DependencyProperty.Register( + nameof(Text), + typeof(string), + typeof(MarkdownTextBlock), + new PropertyMetadata(null, OnTextChanged)); + + public MarkdownConfig Config + { + get => (MarkdownConfig)GetValue(ConfigProperty); + set => SetValue(ConfigProperty, value); + } + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + private static void OnConfigChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MarkdownTextBlock self && e.NewValue != null) + { + self.ApplyConfig(self.Config); + } + } + + private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MarkdownTextBlock self && e.NewValue != null) + { + self.ApplyText(self.Text, true); + } + } + + public MarkdownTextBlock() + { + this.DefaultStyleKey = typeof(MarkdownTextBlock); + _document = new MyFlowDocument(); + _pipeline = new MarkdownPipelineBuilder() + .UseEmphasisExtras() + .UseAutoLinks() + .UseTaskLists() + .UsePipeTables() + .Build(); + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + _container = (Grid)GetTemplateChild(MarkdownContainerName); + _container.Children.Clear(); + _container.Children.Add(_document.RichTextBlock); + Build(); + } + + private void ApplyConfig(MarkdownConfig config) + { + if (_renderer != null) + { + _renderer.Config = config; + } + } + + private void ApplyText(string text, bool rerender) + { + var markdown = Markdown.Parse(text, _pipeline); + if (_renderer != null) + { + if (rerender) + { + _renderer.ReloadDocument(); + } + _renderer.Render(markdown); + } + } + + private void Build() + { + if (Config != null) + { + if (_renderer == null) + { + _renderer = new WinUIRenderer(_document, Config); + } + _pipeline.Setup(_renderer); + ApplyText(Text, false); + } + } +} diff --git a/components/MarkdownTextBlock/src/MarkdownThemes.cs b/components/MarkdownTextBlock/src/MarkdownThemes.cs new file mode 100644 index 00000000..0b1aaf73 --- /dev/null +++ b/components/MarkdownTextBlock/src/MarkdownThemes.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; +#if !WINAPPSDK +using FontWeight = Windows.UI.Text.FontWeight; +using FontWeights = Windows.UI.Text.FontWeights; +#else +using FontWeight = Windows.UI.Text.FontWeight; +using FontWeights = Microsoft.UI.Text.FontWeights; +#endif + +namespace CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns; + +public sealed class MarkdownThemes : DependencyObject +{ + internal static MarkdownThemes Default { get; } = new(); + + public Thickness Padding { get; set; } = new(8); + + public Thickness InternalMargin { get; set; } = new(4); + + public CornerRadius CornerRadius { get; set; } = new(4); + + public double H1FontSize { get; set; } = 22; + + public double H2FontSize { get; set; } = 20; + + public double H3FontSize { get; set; } = 18; + + public double H4FontSize { get; set; } = 16; + + public double H5FontSize { get; set; } = 14; + + public double H6FontSize { get; set; } = 12; + + public Brush HeadingForeground { get; set; } = Extensions.GetAccentColorBrush(); + + public FontWeight H1FontWeight { get; set; } = FontWeights.Bold; + + public FontWeight H2FontWeight { get; set; } = FontWeights.Normal; + + public FontWeight H3FontWeight { get; set; } = FontWeights.Normal; + + public FontWeight H4FontWeight { get; set;} = FontWeights.Normal; + + public FontWeight H5FontWeight { get; set; } = FontWeights.Normal; + + public FontWeight H6FontWeight { get; set; } = FontWeights.Normal; + + public Brush InlineCodeBackground { get; set; } = (Brush)Application.Current.Resources["ExpanderHeaderBackground"]; + + public Brush InlineCodeBorderBrush { get; set; } = new SolidColorBrush(Colors.Gray); + + public Thickness InlineCodeBorderThickness { get; set; } = new (1); + + public CornerRadius InlineCodeCornerRadius { get; set; } = new (2); + + public Thickness InlineCodePadding { get; set; } = new(0); + + public double InlineCodeFontSize { get; set; } = 10; + + public FontWeight InlineCodeFontWeight { get; set; } = FontWeights.Normal; +} diff --git a/components/MarkdownTextBlock/src/MultiTarget.props b/components/MarkdownTextBlock/src/MultiTarget.props new file mode 100644 index 00000000..18f6c7c9 --- /dev/null +++ b/components/MarkdownTextBlock/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk; + + diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/CodeBlockRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/CodeBlockRenderer.cs new file mode 100644 index 00000000..1dd58819 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/CodeBlockRenderer.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class CodeBlockRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, CodeBlock obj) + { + var code = new MyCodeBlock(obj, renderer.Config); + renderer.Push(code); + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TableRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TableRenderer.cs new file mode 100644 index 00000000..91063829 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TableRenderer.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.Tables; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Extensions; + +public class TableRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, Table table) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (table == null) throw new ArgumentNullException(nameof(table)); + + var myTable = new MyTable(table); + + renderer.Push(myTable); + + for (var rowIndex = 0; rowIndex < table.Count; rowIndex++) + { + var rowObj = table[rowIndex]; + var row = (TableRow)rowObj; + + for (var i = 0; i < row.Count; i++) + { + var cellObj = row[i]; + var cell = (TableCell)cellObj; + var textAlignment = TextAlignment.Left; + + var columnIndex = i; + + if (table.ColumnDefinitions.Count > 0) + { + columnIndex = cell.ColumnIndex < 0 || cell.ColumnIndex >= table.ColumnDefinitions.Count + ? i + : cell.ColumnIndex; + columnIndex = columnIndex >= table.ColumnDefinitions.Count ? table.ColumnDefinitions.Count - 1 : columnIndex; + var alignment = table.ColumnDefinitions[columnIndex].Alignment; + textAlignment = alignment switch + { + TableColumnAlign.Center => TextAlignment.Center, + TableColumnAlign.Left => TextAlignment.Left, + TableColumnAlign.Right => TextAlignment.Right, + _ => TextAlignment.Left, + }; + } + + var myCell = new MyTableCell(cell, textAlignment, row.IsHeader, columnIndex, rowIndex); + + renderer.Push(myCell); + renderer.Write(cell); + renderer.Pop(); + } + } + + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TaskListRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TaskListRenderer.cs new file mode 100644 index 00000000..f8c0e11b --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Extensions/TaskListRenderer.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.TaskLists; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Extensions; + +internal class TaskListRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, TaskList taskList) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (taskList == null) throw new ArgumentNullException(nameof(taskList)); + + var checkBox = new MyTaskListCheckBox(taskList); + renderer.WriteInline(checkBox); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HeadingRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HeadingRenderer.cs new file mode 100644 index 00000000..fe789912 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HeadingRenderer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class HeadingRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, HeadingBlock obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var paragraph = new MyHeading(obj, renderer.Config); + renderer.Push(paragraph); + renderer.WriteLeafInline(obj); + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HtmlBlockRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HtmlBlockRenderer.cs new file mode 100644 index 00000000..1d44bde5 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/HtmlBlockRenderer.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class HtmlBlockRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, HtmlBlock obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var stringBuilder = new StringBuilder(); + foreach (var line in obj.Lines.Lines) + { + var lineText = line.Slice.ToString().Trim(); + if (String.IsNullOrWhiteSpace(lineText)) + { + continue; + } + stringBuilder.AppendLine(lineText); + } + + var html = Regex.Replace(stringBuilder.ToString(), @"\t|\n|\r", "", RegexOptions.Compiled); + html = Regex.Replace(html, @" ", " ", RegexOptions.Compiled); + var doc = new HtmlDocument(); + doc.LoadHtml(html); + HtmlWriter.WriteHtml(renderer, doc.DocumentNode.ChildNodes); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/AutoLinkInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/AutoLinkInlineRenderer.cs new file mode 100644 index 00000000..4e9d01f0 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/AutoLinkInlineRenderer.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class AutoLinkInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, AutolinkInline link) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (link == null) throw new ArgumentNullException(nameof(link)); + + var url = link.Url; + if (link.IsEmail) + { + url = "mailto:" + url; + } + + if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute)) + { + url = "#"; + } + + var autolink = new MyAutolinkInline(link); + + renderer.Push(autolink); + + renderer.WriteText(link.Url); + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/CodeInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/CodeInlineRenderer.cs new file mode 100644 index 00000000..d95d8634 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/CodeInlineRenderer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class CodeInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, CodeInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + renderer.WriteInline(new MyInlineCode(obj, renderer.Config)); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/ContainerInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/ContainerInlineRenderer.cs new file mode 100644 index 00000000..47a807c2 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/ContainerInlineRenderer.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class ContainerInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, ContainerInline obj) + { + foreach (var inline in obj) + { + renderer.Write(inline); + } + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/DelimiterInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/DelimiterInlineRenderer.cs new file mode 100644 index 00000000..6d06376a --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/DelimiterInlineRenderer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class DelimiterInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, DelimiterInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + // delimiter's children are emphasized text, we don't need to explicitly render them + // Just need to render the children of the delimiter, I think.. + //renderer.WriteText(obj.ToLiteral()); + renderer.WriteChildren(obj); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/EmphasisInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/EmphasisInlineRenderer.cs new file mode 100644 index 00000000..ff82ff20 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/EmphasisInlineRenderer.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class EmphasisInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, EmphasisInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + MyEmphasisInline? span = null; + + switch (obj.DelimiterChar) + { + case '*': + case '_': + span = new MyEmphasisInline(obj); + if (obj.DelimiterCount == 2) { span.SetBold(); } else { span.SetItalic(); } + break; + case '~': + span = new MyEmphasisInline(obj); + if (obj.DelimiterCount == 2) { span.SetStrikeThrough(); } else { span.SetSubscript(); } + break; + case '^': + span = new MyEmphasisInline(obj); + span.SetSuperscript(); + break; + } + + if (span != null) + { + renderer.Push(span); + renderer.WriteChildren(obj); + renderer.Pop(); + } + else + { + renderer.WriteChildren(obj); + } + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlEntityInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlEntityInlineRenderer.cs new file mode 100644 index 00000000..5f7a55a8 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlEntityInlineRenderer.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class HtmlEntityInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, HtmlEntityInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var transcoded = obj.Transcoded; + renderer.WriteText(ref transcoded); + // todo: wtf is this? + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlInlineRenderer.cs new file mode 100644 index 00000000..503c39df --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/HtmlInlineRenderer.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class HtmlInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, HtmlInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var html = obj.Tag; + var doc = new HtmlDocument(); + doc.LoadHtml(html); + HtmlWriter.WriteHtml(renderer, doc.DocumentNode.ChildNodes); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LineBreakInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LineBreakInlineRenderer.cs new file mode 100644 index 00000000..79e57a3f --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LineBreakInlineRenderer.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class LineBreakInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, LineBreakInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + if (obj.IsHard) + { + renderer.WriteInline(new MyLineBreak()); + } + else + { + // Soft line break. + renderer.WriteText(" "); + } + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LinkInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LinkInlineRenderer.cs new file mode 100644 index 00000000..fa05d400 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LinkInlineRenderer.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class LinkInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, LinkInline link) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (link == null) throw new ArgumentNullException(nameof(link)); + + var url = link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url; + + if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute)) + { + url = "#"; + } + + if (link.IsImage) + { + var image = new MyImage(link, CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Extensions.GetUri(url, renderer.Config.BaseUrl), renderer.Config); + renderer.WriteInline(image); + } + else + { + if (link.FirstChild is LinkInline linkInlineChild && linkInlineChild.IsImage) + { + renderer.Push(new MyHyperlinkButton(link, renderer.Config.BaseUrl)); + } + else + { + var hyperlink = new MyHyperlink(link, renderer.Config.BaseUrl); + + renderer.Push(hyperlink); + } + + renderer.WriteChildren(link); + renderer.Pop(); + } + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LiteralInlineRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LiteralInlineRenderer.cs new file mode 100644 index 00000000..e6777867 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/Inlines/LiteralInlineRenderer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; + +internal class LiteralInlineRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, LiteralInline obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + if (obj.Content.IsEmpty) + return; + + renderer.WriteText(ref obj.Content); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ListRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ListRenderer.cs new file mode 100644 index 00000000..aa8034bb --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ListRenderer.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class ListRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, ListBlock listBlock) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (listBlock == null) throw new ArgumentNullException(nameof(listBlock)); + + var list = new MyList(listBlock); + + renderer.Push(list); + + foreach (var item in listBlock) + { + var listItemBlock = (ListItemBlock)item; + var listItem = new MyBlockContainer(listItemBlock); + renderer.Push(listItem); + renderer.WriteChildren(listItemBlock); + renderer.Pop(); + } + + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ParagraphRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ParagraphRenderer.cs new file mode 100644 index 00000000..b4f9f20f --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ParagraphRenderer.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class ParagraphRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, ParagraphBlock obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var paragraph = new MyParagraph(obj); + // set style + renderer.Push(paragraph); + renderer.WriteLeafInline(obj); + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/QuoteBlockRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/QuoteBlockRenderer.cs new file mode 100644 index 00000000..71d42927 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/QuoteBlockRenderer.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class QuoteBlockRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, QuoteBlock obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var quote = new MyQuote(obj); + + renderer.Push(quote); + renderer.WriteChildren(obj); + renderer.Pop(); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ThematicBreakRenderer.cs b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ThematicBreakRenderer.cs new file mode 100644 index 00000000..9ee2356b --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/ObjectRenderers/ThematicBreakRenderer.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; + +internal class ThematicBreakRenderer : UWPObjectRenderer +{ + protected override void Write(WinUIRenderer renderer, ThematicBreakBlock obj) + { + if (renderer == null) throw new ArgumentNullException(nameof(renderer)); + if (obj == null) throw new ArgumentNullException(nameof(obj)); + + var thematicBreak = new MyThematicBreak(obj); + + renderer.WriteBlock(thematicBreak); + } +} diff --git a/components/MarkdownTextBlock/src/Renderers/UWPObjectRenderer.cs b/components/MarkdownTextBlock/src/Renderers/UWPObjectRenderer.cs new file mode 100644 index 00000000..792cdecd --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/UWPObjectRenderer.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Renderers; +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers; + +public abstract class UWPObjectRenderer : MarkdownObjectRenderer + where TObject : MarkdownObject +{ +} diff --git a/components/MarkdownTextBlock/src/Renderers/WinUIRenderer.cs b/components/MarkdownTextBlock/src/Renderers/WinUIRenderer.cs new file mode 100644 index 00000000..af4d6cb0 --- /dev/null +++ b/components/MarkdownTextBlock/src/Renderers/WinUIRenderer.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Inlines; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers.ObjectRenderers.Extensions; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; +using Markdig.Renderers; +using Markdig.Syntax; +using Markdig.Helpers; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.Renderers; + +public class WinUIRenderer : RendererBase +{ + private readonly Stack _stack = new Stack(); + private char[] _buffer; + private MarkdownConfig _config = MarkdownConfig.Default; + public MyFlowDocument FlowDocument { get; private set; } + public MarkdownConfig Config + { + get => _config; + set => _config = value; + } + + public WinUIRenderer(MyFlowDocument document, MarkdownConfig config) + { + _buffer = new char[1024]; + Config = config; + FlowDocument = document; + // set style + _stack.Push(FlowDocument); + LoadOverridenRenderers(); + } + + private void LoadOverridenRenderers() + { + LoadRenderers(); + } + + public override object Render(MarkdownObject markdownObject) + { + Write(markdownObject); + return FlowDocument ?? new(); + } + + public void ReloadDocument() + { + _stack.Clear(); + FlowDocument.RichTextBlock.Blocks.Clear(); + _stack.Push(FlowDocument); + LoadOverridenRenderers(); + } + + public void WriteLeafInline(LeafBlock leafBlock) + { + if (leafBlock == null || leafBlock.Inline == null) throw new ArgumentNullException(nameof(leafBlock)); + var inline = (Markdig.Syntax.Inlines.Inline)leafBlock.Inline; + while (inline != null) + { + Write(inline); + inline = inline.NextSibling; + } + } + + public void WriteLeafRawLines(LeafBlock leafBlock) + { + if (leafBlock == null) throw new ArgumentNullException(nameof(leafBlock)); + if (leafBlock.Lines.Lines != null) + { + var lines = leafBlock.Lines; + var slices = lines.Lines; + for (var i = 0; i < lines.Count; i++) + { + if (i != 0) + WriteInline(new MyLineBreak()); + + WriteText(ref slices[i].Slice); + } + } + } + + public void Push(IAddChild child) + { + _stack.Push(child); + } + + public void Pop() + { + var popped = _stack.Pop(); + _stack.Peek().AddChild(popped); + } + + public void WriteBlock(IAddChild obj) + { + _stack.Peek().AddChild(obj); + } + + public void WriteInline(IAddChild inline) + { + AddInline(_stack.Peek(), inline); + } + + public void WriteText(ref StringSlice slice) + { + if (slice.Start > slice.End) + return; + + WriteText(slice.Text, slice.Start, slice.Length); + } + + public void WriteText(string? text) + { + WriteInline(new MyInlineText(text ?? "")); + } + + public void WriteText(string? text, int offset, int length) + { + if (text == null) + return; + + if (offset == 0 && text.Length == length) + { + WriteText(text); + } + else + { + if (length > _buffer.Length) + { + _buffer = text.ToCharArray(); + WriteText(new string(_buffer, offset, length)); + } + else + { + text.CopyTo(offset, _buffer, 0, length); + WriteText(new string(_buffer, 0, length)); + } + } + } + + private static void AddInline(IAddChild parent, IAddChild inline) + { + parent.AddChild(inline); + } + + protected virtual void LoadRenderers() + { + // Default block renderers + ObjectRenderers.Add(new CodeBlockRenderer()); + ObjectRenderers.Add(new ListRenderer()); + ObjectRenderers.Add(new HeadingRenderer()); + ObjectRenderers.Add(new ParagraphRenderer()); + ObjectRenderers.Add(new QuoteBlockRenderer()); + ObjectRenderers.Add(new ThematicBreakRenderer()); + ObjectRenderers.Add(new HtmlBlockRenderer()); + + // Default inline renderers + ObjectRenderers.Add(new AutoLinkInlineRenderer()); + ObjectRenderers.Add(new CodeInlineRenderer()); + ObjectRenderers.Add(new DelimiterInlineRenderer()); + ObjectRenderers.Add(new EmphasisInlineRenderer()); + ObjectRenderers.Add(new HtmlEntityInlineRenderer()); + ObjectRenderers.Add(new LineBreakInlineRenderer()); + ObjectRenderers.Add(new LinkInlineRenderer()); + ObjectRenderers.Add(new LiteralInlineRenderer()); + ObjectRenderers.Add(new ContainerInlineRenderer()); + + // Extension renderers + ObjectRenderers.Add(new TableRenderer()); + ObjectRenderers.Add(new TaskListRenderer()); + ObjectRenderers.Add(new HtmlInlineRenderer()); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/Html/MyBlock.cs b/components/MarkdownTextBlock/src/TextElements/Html/MyBlock.cs new file mode 100644 index 00000000..4cdc8ba2 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/Html/MyBlock.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Windows.UI.Text; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements.Html; + +internal class MyBlock : IAddChild +{ + private HtmlNode _htmlNode; + private Paragraph _paragraph; + private List _richTextBlocks; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyBlock(HtmlNode block) + { + _htmlNode = block; + var align = _htmlNode.GetAttributeValue("align", "left"); + _richTextBlocks = new List(); + _paragraph = new Paragraph(); + _paragraph.TextAlignment = align switch + { + "left" => TextAlignment.Left, + "right" => TextAlignment.Right, + "center" => TextAlignment.Center, + "justify" => TextAlignment.Justify, + _ => TextAlignment.Left, + }; + StyleBlock(); + } + + public void AddChild(IAddChild child) + { + if (child.TextElement is Block blockChild) + { + _paragraph.Inlines.Add(new LineBreak()); + var inlineUIContainer = new InlineUIContainer(); + var richTextBlock = new RichTextBlock(); + richTextBlock.Blocks.Add(blockChild); + inlineUIContainer.Child = richTextBlock; + _richTextBlocks.Add(richTextBlock); + _paragraph.Inlines.Add(inlineUIContainer); + _paragraph.Inlines.Add(new LineBreak()); + } + else if (child.TextElement is Inline inlineChild) + { + _paragraph.Inlines.Add(inlineChild); + } + } + + private void StyleBlock() + { + switch (_htmlNode.Name.ToLower()) + { + case "address": + _paragraph.FontStyle = FontStyle.Italic; + foreach (var richTextBlock in _richTextBlocks) + { + richTextBlock.FontStyle = FontStyle.Italic; + } + //_flowDocument.RichTextBlock.Style = (Windows.UI.Xaml.Style)Windows.UI.Xaml.Application.Current.Resources["AddressBlockStyle"]; + break; + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/Html/MyDetails.cs b/components/MarkdownTextBlock/src/TextElements/Html/MyDetails.cs new file mode 100644 index 00000000..34faf6c3 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/Html/MyDetails.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Microsoft.UI.Xaml.Controls; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements.Html; + +// block +internal class MyDetails : IAddChild +{ + private HtmlNode _htmlNode; + private InlineUIContainer _inlineUIContainer; + private Expander _expander; + private MyFlowDocument _flowDocument; + private Paragraph _paragraph; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyDetails(HtmlNode details) + { + _htmlNode = details; + + var header = _htmlNode.ChildNodes + .FirstOrDefault( + x => x.Name == "summary" || + x.Name == "header"); + + _inlineUIContainer = new InlineUIContainer(); + _expander = new Expander(); + _expander.HorizontalAlignment = HorizontalAlignment.Stretch; + _flowDocument = new MyFlowDocument(details); + _flowDocument.RichTextBlock.HorizontalAlignment = HorizontalAlignment.Stretch; + _expander.Content = _flowDocument.RichTextBlock; + var headerBlock = new TextBlock() + { + Text = header?.InnerText + }; + headerBlock.HorizontalAlignment = HorizontalAlignment.Stretch; + _expander.Header = headerBlock; + _inlineUIContainer.Child = _expander; + _paragraph = new Paragraph(); + _paragraph.Inlines.Add(_inlineUIContainer); + } + + public void AddChild(IAddChild child) + { + _flowDocument.AddChild(child); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/Html/MyInline.cs b/components/MarkdownTextBlock/src/TextElements/Html/MyInline.cs new file mode 100644 index 00000000..7ede16d4 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/Html/MyInline.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements.Html; + +internal class MyInline : IAddChild +{ + private HtmlNode _htmlNode; + private Paragraph _paragraph; + private InlineUIContainer _inlineUIContainer; + private RichTextBlock _richTextBlock; + + public TextElement TextElement + { + get => _inlineUIContainer; + } + + public MyInline(HtmlNode inline) + { + _htmlNode = inline; + _paragraph = new Paragraph(); + _inlineUIContainer = new InlineUIContainer(); + _richTextBlock = new RichTextBlock(); + _richTextBlock.Blocks.Add(_paragraph); + + _richTextBlock.HorizontalAlignment = HorizontalAlignment.Stretch; + _inlineUIContainer.Child = _richTextBlock; + } + + public void AddChild(IAddChild child) + { + if (child.TextElement is Inline inlineChild) + { + _paragraph.Inlines.Add(inlineChild); + } + // we shouldn't support rendering block in inline + // but if we want to support it, we can do it like this: + //else if (child.TextElement is Block blockChild) + //{ + // _richTextBlock.Blocks.Add(blockChild); + // // if we add a new block to an inline container, + // // if the next child is inline, it needs to be added after the block + // // so we add a new paragraph. This way the next time + // // AddChild is called, it's added to the new paragraph + // _paragraph = new Paragraph(); + // _richTextBlock.Blocks.Add(_paragraph); + //} + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/HtmlElementType.cs b/components/MarkdownTextBlock/src/TextElements/HtmlElementType.cs new file mode 100644 index 00000000..1621cf1a --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/HtmlElementType.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +public enum HtmlElementType +{ + Block, + Inline, +} diff --git a/components/MarkdownTextBlock/src/TextElements/IAddChild.cs b/components/MarkdownTextBlock/src/TextElements/IAddChild.cs new file mode 100644 index 00000000..6b43654c --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/IAddChild.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +public interface IAddChild +{ + TextElement TextElement { get; } + void AddChild(IAddChild child); +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyAutolinkInline.cs b/components/MarkdownTextBlock/src/TextElements/MyAutolinkInline.cs new file mode 100644 index 00000000..bdea36ea --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyAutolinkInline.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyAutolinkInline : IAddChild +{ + private AutolinkInline _autoLinkInline; + + public TextElement TextElement { get; private set; } + + public MyAutolinkInline(AutolinkInline autoLinkInline) + { + _autoLinkInline = autoLinkInline; + TextElement = new Hyperlink() + { + NavigateUri = new Uri(autoLinkInline.Url), + }; + } + + + public void AddChild(IAddChild child) + { + try + { + var text = (MyInlineText)child; + ((Hyperlink)TextElement).Inlines.Add((Run)text.TextElement); + } + catch (Exception ex) + { + throw new Exception("Error adding child to MyAutolinkInline", ex); + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyBlockContainer.cs b/components/MarkdownTextBlock/src/TextElements/MyBlockContainer.cs new file mode 100644 index 00000000..35be495a --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyBlockContainer.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyBlockContainer : IAddChild +{ + private ContainerBlock _containerBlock; + private InlineUIContainer _inlineUIContainer; + private MyFlowDocument _flowDocument; + private Paragraph _paragraph; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyBlockContainer(ContainerBlock containerBlock) + { + _containerBlock = containerBlock; + _inlineUIContainer = new InlineUIContainer(); + _flowDocument = new MyFlowDocument(containerBlock); + _inlineUIContainer.Child = _flowDocument.RichTextBlock; + _paragraph = new Paragraph(); + _paragraph.Inlines.Add(_inlineUIContainer); + } + + public void AddChild(IAddChild child) + { + _flowDocument.AddChild(child); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyCodeBlock.cs b/components/MarkdownTextBlock/src/TextElements/MyCodeBlock.cs new file mode 100644 index 00000000..862c7de1 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyCodeBlock.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyCodeBlock : IAddChild +{ + private CodeBlock _codeBlock; + private Paragraph _paragraph; + private MarkdownConfig _config; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyCodeBlock(CodeBlock codeBlock, MarkdownConfig config) + { + _codeBlock = codeBlock; + _config = config; + _paragraph = new Paragraph(); + var container = new InlineUIContainer(); + var border = new Border(); + border.Background = (Brush)Application.Current.Resources["ExpanderHeaderBackground"]; + border.Padding = _config.Themes.Padding; + border.Margin = _config.Themes.InternalMargin; + border.CornerRadius = _config.Themes.CornerRadius; + var richTextBlock = new RichTextBlock(); + + if (codeBlock is FencedCodeBlock fencedCodeBlock) + { +//#if !WINAPPSDK +// var formatter = new ColorCode.RichTextBlockFormatter(Extensions.GetOneDarkProStyle()); +//#else +// var formatter = new ColorCode.RichTextBlockFormatter(Extensions.GetOneDarkProStyle()); +//#endif + var stringBuilder = new StringBuilder(); + + // go through all the lines backwards and only add the lines to a stack if we have encountered the first non-empty line + var lines = fencedCodeBlock.Lines.Lines; + var stack = new Stack(); + var encounteredFirstNonEmptyLine = false; + if (lines != null) + { + for (var i = lines.Length - 1; i >= 0; i--) + { + var line = lines[i]; + if (String.IsNullOrWhiteSpace(line.ToString()) && !encounteredFirstNonEmptyLine) + { + continue; + } + + encounteredFirstNonEmptyLine = true; + stack.Push(line.ToString()); + } + + // append all the lines in the stack to the string builder + while (stack.Count > 0) + { + stringBuilder.AppendLine(stack.Pop()); + } + } + + //formatter.FormatRichTextBlock(stringBuilder.ToString(), fencedCodeBlock.ToLanguage(), richTextBlock); + } + else + { + foreach (var line in codeBlock.Lines.Lines) + { + var paragraph = new Paragraph(); + var lineString = line.ToString(); + if (!String.IsNullOrWhiteSpace(lineString)) + { + paragraph.Inlines.Add(new Run() { Text = lineString }); + } + richTextBlock.Blocks.Add(paragraph); + } + } + border.Child = richTextBlock; + container.Child = border; + _paragraph.Inlines.Add(container); + } + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyEmphasisInline.cs b/components/MarkdownTextBlock/src/TextElements/MyEmphasisInline.cs new file mode 100644 index 00000000..ef30998f --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyEmphasisInline.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using Windows.UI.Text; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyEmphasisInline : IAddChild +{ + private Span _span; + private EmphasisInline _markdownObject; + + private bool _isBold; + private bool _isItalic; + private bool _isStrikeThrough; + + public TextElement TextElement + { + get => _span; + } + + public MyEmphasisInline(EmphasisInline emphasisInline) + { + _span = new Span(); + _markdownObject = emphasisInline; + } + + public void AddChild(IAddChild child) + { + try + { + if (child is MyInlineText inlineText) + { + _span.Inlines.Add((Run)inlineText.TextElement); + } + else if (child is MyEmphasisInline emphasisInline) + { + if (emphasisInline._isBold) { SetBold(); } + if (emphasisInline._isItalic) { SetItalic(); } + if (emphasisInline._isStrikeThrough) { SetStrikeThrough(); } + _span.Inlines.Add(emphasisInline._span); + } + } + catch (Exception ex) + { + throw new Exception($"Error in {nameof(MyEmphasisInline)}.{nameof(AddChild)}: {ex.Message}"); + } + } + + public void SetBold() + { + _span.FontWeight = FontWeights.Bold; + _isBold = true; + } + + public void SetItalic() + { + _span.FontStyle = FontStyle.Italic; + _isItalic = true; + } + + public void SetStrikeThrough() + { + _span.TextDecorations = TextDecorations.Strikethrough; + _isStrikeThrough = true; + } + + public void SetSubscript() + { + _span.SetValue(Typography.VariantsProperty, FontVariants.Subscript); + } + + public void SetSuperscript() + { + _span.SetValue(Typography.VariantsProperty, FontVariants.Superscript); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyFlowDocument.cs b/components/MarkdownTextBlock/src/TextElements/MyFlowDocument.cs new file mode 100644 index 00000000..cebf08be --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyFlowDocument.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax; +#if !WINAPPSDK +using Block = Windows.UI.Xaml.Documents.Block; +using Inline = Windows.UI.Xaml.Documents.Inline; +#else +using Block = Microsoft.UI.Xaml.Documents.Block; +using Inline = Microsoft.UI.Xaml.Documents.Inline; +#endif + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +public class MyFlowDocument : IAddChild +{ + private HtmlNode? _htmlNode; + private RichTextBlock _richTextBlock = new RichTextBlock(); + private MarkdownObject? _markdownObject; + + // useless property + public TextElement TextElement { get; set; } = new Run(); + // + + public RichTextBlock RichTextBlock + { + get => _richTextBlock; + set => _richTextBlock = value; + } + + public bool IsHtml => _htmlNode != null; + + public MyFlowDocument() + { + } + + public MyFlowDocument(MarkdownObject markdownObject) + { + _markdownObject = markdownObject; + } + + public MyFlowDocument(HtmlNode node) + { + _htmlNode = node; + } + + public void AddChild(IAddChild child) + { + TextElement element = child.TextElement; + if (element != null) + { + if (element is Block block) + { + _richTextBlock.Blocks.Add(block); + } + else if (element is Inline inline) + { + var paragraph = new Paragraph(); + paragraph.Inlines.Add(inline); + _richTextBlock.Blocks.Add(paragraph); + } + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyHeading.cs b/components/MarkdownTextBlock/src/TextElements/MyHeading.cs new file mode 100644 index 00000000..7e12be87 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyHeading.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyHeading : IAddChild +{ + private Paragraph _paragraph; + private HeadingBlock? _headingBlock; + private HtmlNode? _htmlNode; + private MarkdownConfig _config; + + public bool IsHtml => _htmlNode != null; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyHeading(HeadingBlock headingBlock, MarkdownConfig config) + { + _headingBlock = headingBlock; + _paragraph = new Paragraph(); + _config = config; + + var level = headingBlock.Level; + _paragraph.FontSize = level switch + { + 1 => _config.Themes.H1FontSize, + 2 => _config.Themes.H2FontSize, + 3 => _config.Themes.H3FontSize, + 4 => _config.Themes.H4FontSize, + 5 => _config.Themes.H5FontSize, + _ => _config.Themes.H6FontSize, + }; + _paragraph.Foreground = _config.Themes.HeadingForeground; + _paragraph.FontWeight = level switch + { + 1 => _config.Themes.H1FontWeight, + 2 => _config.Themes.H2FontWeight, + 3 => _config.Themes.H3FontWeight, + 4 => _config.Themes.H4FontWeight, + 5 => _config.Themes.H5FontWeight, + _ => _config.Themes.H6FontWeight, + }; + } + + public MyHeading(HtmlNode htmlNode, MarkdownConfig config) + { + _htmlNode = htmlNode; + _paragraph = new Paragraph(); + _config = config; + + var align = _htmlNode.GetAttributeValue("align", "left"); + _paragraph.TextAlignment = align switch + { + "left" => TextAlignment.Left, + "right" => TextAlignment.Right, + "center" => TextAlignment.Center, + "justify" => TextAlignment.Justify, + _ => TextAlignment.Left, + }; + + var level = int.Parse(htmlNode.Name.Substring(1)); + _paragraph.FontSize = level switch + { + 1 => _config.Themes.H1FontSize, + 2 => _config.Themes.H2FontSize, + 3 => _config.Themes.H3FontSize, + 4 => _config.Themes.H4FontSize, + 5 => _config.Themes.H5FontSize, + _ => _config.Themes.H6FontSize, + }; + _paragraph.Foreground = _config.Themes.HeadingForeground; + _paragraph.FontWeight = level switch + { + 1 => _config.Themes.H1FontWeight, + 2 => _config.Themes.H2FontWeight, + 3 => _config.Themes.H3FontWeight, + 4 => _config.Themes.H4FontWeight, + 5 => _config.Themes.H5FontWeight, + _ => _config.Themes.H6FontWeight, + }; + } + + public void AddChild(IAddChild child) + { + if (child.TextElement is Inline inlineChild) + { + _paragraph.Inlines.Add(inlineChild); + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyHyperlink.cs b/components/MarkdownTextBlock/src/TextElements/MyHyperlink.cs new file mode 100644 index 00000000..dba8a0f2 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyHyperlink.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyHyperlink : IAddChild +{ + private Hyperlink _hyperlink; + private LinkInline? _linkInline; + private HtmlNode? _htmlNode; + private string? _baseUrl; + + public bool IsHtml => _htmlNode != null; + + public TextElement TextElement + { + get => _hyperlink; + } + + public MyHyperlink(LinkInline linkInline, string? baseUrl) + { + _baseUrl = baseUrl; + var url = linkInline.GetDynamicUrl != null ? linkInline.GetDynamicUrl() ?? linkInline.Url : linkInline.Url; + _linkInline = linkInline; + _hyperlink = new Hyperlink() + { + NavigateUri = Extensions.GetUri(url, baseUrl), + }; + } + + public MyHyperlink(HtmlNode htmlNode, string? baseUrl) + { + _baseUrl = baseUrl; + var url = htmlNode.GetAttributeValue("href", "#"); + _htmlNode = htmlNode; + _hyperlink = new Hyperlink() + { + NavigateUri = Extensions.GetUri(url, baseUrl), + }; + } + + public void AddChild(IAddChild child) + { +#if !WINAPPSDK + if (child.TextElement is Windows.UI.Xaml.Documents.Inline inlineChild) + { + try + { + _hyperlink.Inlines.Add(inlineChild); + // TODO: Add support for click handler + } + catch { } + } +#else + if (child.TextElement is Microsoft.UI.Xaml.Documents.Inline inlineChild) + { + try + { + _hyperlink.Inlines.Add(inlineChild); + // TODO: Add support for click handler + } + catch { } + } +#endif + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyHyperlinkButton.cs b/components/MarkdownTextBlock/src/TextElements/MyHyperlinkButton.cs new file mode 100644 index 00000000..dbc91ca7 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyHyperlinkButton.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using HtmlAgilityPack; +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyHyperlinkButton : IAddChild +{ + private HyperlinkButton? _hyperLinkButton; + private InlineUIContainer _inlineUIContainer = new InlineUIContainer(); + private MyFlowDocument? _flowDoc; + private string? _baseUrl; + private LinkInline? _linkInline; + private HtmlNode? _htmlNode; + + public bool IsHtml => _htmlNode != null; + + public TextElement TextElement + { + get => _inlineUIContainer; + } + + public MyHyperlinkButton(LinkInline linkInline, string? baseUrl) + { + _baseUrl = baseUrl; + var url = linkInline.GetDynamicUrl != null ? linkInline.GetDynamicUrl() ?? linkInline.Url : linkInline.Url; + _linkInline = linkInline; + Init(url, baseUrl); + } + + public MyHyperlinkButton(HtmlNode htmlNode, string? baseUrl) + { + _baseUrl = baseUrl; + _htmlNode = htmlNode; + var url = htmlNode.GetAttributeValue("href", "#"); + Init(url, baseUrl); + } + + private void Init(string? url, string? baseUrl) + { + _hyperLinkButton = new HyperlinkButton() + { + NavigateUri = Extensions.GetUri(url, baseUrl), + }; + _hyperLinkButton.Padding = new Thickness(0); + _hyperLinkButton.Margin = new Thickness(0); + if (IsHtml && _htmlNode != null) + { + _flowDoc = new MyFlowDocument(_htmlNode); + } + else if (_linkInline != null) + { + _flowDoc = new MyFlowDocument(_linkInline); + } + _inlineUIContainer.Child = _hyperLinkButton; + _hyperLinkButton.Content = _flowDoc?.RichTextBlock; + } + + public void AddChild(IAddChild child) + { + _flowDoc?.AddChild(child); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyImage.cs b/components/MarkdownTextBlock/src/TextElements/MyImage.cs new file mode 100644 index 00000000..56866d00 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyImage.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; +using HtmlAgilityPack; +using System.Globalization; +using Windows.Storage.Streams; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyImage : IAddChild +{ + private InlineUIContainer _container = new InlineUIContainer(); + private LinkInline? _linkInline; + private Image _image = new Image(); + private Uri _uri; + private HtmlNode? _htmlNode; + private IImageProvider? _imageProvider; + private ISVGRenderer _svgRenderer; + private double _precedentWidth; + private double _precedentHeight; + private bool _loaded; + + public TextElement TextElement + { + get => _container; + } + + public MyImage(LinkInline linkInline, Uri uri, MarkdownConfig config) + { + _linkInline = linkInline; + _uri = uri; + _imageProvider = config.ImageProvider; + _svgRenderer = config.SVGRenderer == null ? new DefaultSVGRenderer() : config.SVGRenderer; + Init(); + var size = Extensions.GetMarkdownImageSize(linkInline); + if (size.Width != 0) + { + _precedentWidth = size.Width; + } + if (size.Height != 0) + { + _precedentHeight = size.Height; + } + } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public MyImage(HtmlNode htmlNode, MarkdownConfig? config) +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { +#pragma warning disable CS8601 // Possible null reference assignment. + Uri.TryCreate(htmlNode.GetAttributeValue("src", "#"), UriKind.RelativeOrAbsolute, out _uri); +#pragma warning restore CS8601 // Possible null reference assignment. + _htmlNode = htmlNode; + _imageProvider = config?.ImageProvider; + _svgRenderer = config?.SVGRenderer == null ? new DefaultSVGRenderer() : config.SVGRenderer; + Init(); + int.TryParse( + htmlNode.GetAttributeValue("width", "0"), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var width + ); + int.TryParse( + htmlNode.GetAttributeValue("height", "0"), + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var height + ); + if (width > 0) + { + _precedentWidth = width; + } + if (height > 0) + { + _precedentHeight = height; + } + } + + private void Init() + { + _image.Loaded += LoadImage; + _container.Child = _image; + } + + private async void LoadImage(object sender, RoutedEventArgs e) + { + if (_loaded) return; + try + { + if (_imageProvider != null && _imageProvider.ShouldUseThisProvider(_uri.AbsoluteUri)) + { + _image = await _imageProvider.GetImage(_uri.AbsoluteUri); + _container.Child = _image; + } + else + { + HttpClient client = new HttpClient(); + + // Download data from URL + HttpResponseMessage response = await client.GetAsync(_uri); + + + // Get the Content-Type header +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. +#pragma warning disable CS8602 // Dereference of a possibly null reference. + string contentType = response.Content.Headers.ContentType.MediaType; +#pragma warning restore CS8602 // Dereference of a possibly null reference. +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + if (contentType == "image/svg+xml") + { + var svgString = await response.Content.ReadAsStringAsync(); + var resImage = await _svgRenderer.SvgToImage(svgString); + if (resImage != null) + { + _image = resImage; + _container.Child = _image; + } + } + else + { + byte[] data = await response.Content.ReadAsByteArrayAsync(); + // Create a BitmapImage for other supported formats + BitmapImage bitmap = new BitmapImage(); + using (InMemoryRandomAccessStream stream = new InMemoryRandomAccessStream()) + { + // Write the data to the stream + await stream.WriteAsync(data.AsBuffer()); + stream.Seek(0); + + // Set the source of the BitmapImage + await bitmap.SetSourceAsync(stream); + } + _image.Source = bitmap; + _image.Width = bitmap.PixelWidth == 0 ? bitmap.DecodePixelWidth : bitmap.PixelWidth; + _image.Height = bitmap.PixelHeight == 0 ? bitmap.DecodePixelHeight : bitmap.PixelHeight; + + } + + _loaded = true; + } + + if (_precedentWidth != 0) + { + _image.Width = _precedentWidth; + } + if (_precedentHeight != 0) + { + _image.Height = _precedentHeight; + } + } + catch (Exception) { } + } + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyInlineCode.cs b/components/MarkdownTextBlock/src/TextElements/MyInlineCode.cs new file mode 100644 index 00000000..8e97d596 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyInlineCode.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax.Inlines; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyInlineCode : IAddChild +{ + private CodeInline _codeInline; + private InlineUIContainer _inlineContainer; + private MarkdownConfig _config; + + public TextElement TextElement + { + get => _inlineContainer; + } + + public MyInlineCode(CodeInline codeInline, MarkdownConfig config) + { + _codeInline = codeInline; + _config = config; + _inlineContainer = new InlineUIContainer(); + var border = new Border(); + border.VerticalAlignment = VerticalAlignment.Bottom; + border.Background = _config.Themes.InlineCodeBackground; + border.BorderBrush = _config.Themes.InlineCodeBorderBrush; + border.BorderThickness = _config.Themes.InlineCodeBorderThickness; + border.CornerRadius = _config.Themes.InlineCodeCornerRadius; + border.Padding = _config.Themes.InlineCodePadding; + CompositeTransform3D transform = new CompositeTransform3D(); + transform.TranslateY = 4; + border.Transform3D = transform; + var textBlock = new TextBlock(); + textBlock.FontSize = _config.Themes.InlineCodeFontSize; + textBlock.FontWeight = _config.Themes.InlineCodeFontWeight; + textBlock.Text = codeInline.Content.ToString(); + border.Child = textBlock; + _inlineContainer.Child = border; + } + + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyInlineText.cs b/components/MarkdownTextBlock/src/TextElements/MyInlineText.cs new file mode 100644 index 00000000..3718e937 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyInlineText.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyInlineText : IAddChild +{ + private Run _run; + + public TextElement TextElement + { + get => _run; + } + + public MyInlineText(string text) + { + _run = new Run() + { + Text = text + }; + } + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyLineBreak.cs b/components/MarkdownTextBlock/src/TextElements/MyLineBreak.cs new file mode 100644 index 00000000..8e404472 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyLineBreak.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyLineBreak : IAddChild +{ + private LineBreak _lineBreak; + + public TextElement TextElement + { + get => _lineBreak; + } + + public MyLineBreak() + { + _lineBreak = new LineBreak(); + } + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyList.cs b/components/MarkdownTextBlock/src/TextElements/MyList.cs new file mode 100644 index 00000000..fc381008 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyList.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; +using RomanNumerals; +using System.Globalization; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyList : IAddChild +{ + private Paragraph _paragraph; + private InlineUIContainer _container; + private StackPanel _stackPanel; + private ListBlock _listBlock; + private BulletType _bulletType; + private bool _isOrdered; + private int _startIndex = 1; + private int _index = 1; + private const string _dot = "• "; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyList(ListBlock listBlock) + { + _paragraph = new Paragraph(); + _container = new InlineUIContainer(); + _stackPanel = new StackPanel(); + _listBlock = listBlock; + + if (listBlock.IsOrdered) + { + _isOrdered = true; + _bulletType = ToBulletType(listBlock.BulletType); + + if (listBlock.OrderedStart != null && (listBlock.DefaultOrderedStart != listBlock.OrderedStart)) + { + _startIndex = int.Parse(listBlock.OrderedStart, NumberFormatInfo.InvariantInfo); + _index = _startIndex; + } + } + + _stackPanel.Orientation = Orientation.Vertical; + _container.Child = _stackPanel; + _paragraph.Inlines.Add(_container); + } + + public void AddChild(IAddChild child) + { + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) }); + grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); + string bullet; + if (_isOrdered) + { + bullet = _bulletType switch + { + BulletType.Number => $"{_index}. ", + BulletType.LowerAlpha => $"{_index.ToAlphabetical()}. ", + BulletType.UpperAlpha => $"{_index.ToAlphabetical().ToUpper()}. ", + BulletType.LowerRoman => $"{_index.ToRomanNumerals().ToLower()} ", + BulletType.UpperRoman => $"{_index.ToRomanNumerals().ToUpper()} ", + BulletType.Circle => _dot, + _ => _dot + }; + _index++; + } + else + { + bullet = _dot; + } + var textBlock = new TextBlock() + { + Text = bullet, + }; + textBlock.SetValue(Grid.ColumnProperty, 0); + textBlock.VerticalAlignment = VerticalAlignment.Top; + grid.Children.Add(textBlock); + var flowDoc = new MyFlowDocument(); + flowDoc.AddChild(child); + + flowDoc.RichTextBlock.SetValue(Grid.ColumnProperty, 1); + flowDoc.RichTextBlock.Padding = new Thickness(0); + flowDoc.RichTextBlock.VerticalAlignment = VerticalAlignment.Top; + grid.Children.Add(flowDoc.RichTextBlock); + + _stackPanel.Children.Add(grid); + } + + private BulletType ToBulletType(char bullet) + { + /// Gets or sets the type of the bullet (e.g: '1', 'a', 'A', 'i', 'I'). + switch (bullet) + { + case '1': + return BulletType.Number; + case 'a': + return BulletType.LowerAlpha; + case 'A': + return BulletType.UpperAlpha; + case 'i': + return BulletType.LowerRoman; + case 'I': + return BulletType.UpperRoman; + default: + return BulletType.Circle; + } + } +} + +internal enum BulletType +{ + Circle, + Number, + LowerAlpha, + UpperAlpha, + LowerRoman, + UpperRoman +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyParagraph.cs b/components/MarkdownTextBlock/src/TextElements/MyParagraph.cs new file mode 100644 index 00000000..c3b47c0e --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyParagraph.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyParagraph : IAddChild +{ + private ParagraphBlock _paragraphBlock; + private Paragraph _paragraph; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyParagraph(ParagraphBlock paragraphBlock) + { + _paragraphBlock = paragraphBlock; + _paragraph = new Paragraph(); + } + + public void AddChild(IAddChild child) + { + if (child.TextElement is Inline inlineChild) + { + _paragraph.Inlines.Add(inlineChild); + } +#if !WINAPPSDK + else if (child.TextElement is Windows.UI.Xaml.Documents.Block blockChild) +#else + else if (child.TextElement is Microsoft.UI.Xaml.Documents.Block blockChild) +#endif + { + var inlineUIContainer = new InlineUIContainer(); + var richTextBlock = new RichTextBlock(); + richTextBlock.TextWrapping = TextWrapping.Wrap; + richTextBlock.Blocks.Add(blockChild); + inlineUIContainer.Child = richTextBlock; + _paragraph.Inlines.Add(inlineUIContainer); + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyQuote.cs b/components/MarkdownTextBlock/src/TextElements/MyQuote.cs new file mode 100644 index 00000000..caf1abc8 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyQuote.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyQuote : IAddChild +{ + private Paragraph _paragraph; + private MyFlowDocument _flowDocument; + private QuoteBlock _quoteBlock; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyQuote(QuoteBlock quoteBlock) + { + _quoteBlock = quoteBlock; + _paragraph = new Paragraph(); + + _flowDocument = new MyFlowDocument(quoteBlock); + var inlineUIContainer = new InlineUIContainer(); + + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) }); + grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) }); + + var bar = new Grid(); + bar.Width = 4; + bar.Background = new SolidColorBrush(Colors.Gray); + bar.SetValue(Grid.ColumnProperty, 0); + bar.VerticalAlignment = VerticalAlignment.Stretch; + bar.Margin = new Thickness(0, 0, 4, 0); + grid.Children.Add(bar); + + var rightGrid = new Grid(); + rightGrid.Padding = new Thickness(4); + rightGrid.Children.Add(_flowDocument.RichTextBlock); + + rightGrid.SetValue(Grid.ColumnProperty, 1); + grid.Children.Add(rightGrid); + grid.Margin = new Thickness(0, 2, 0, 2); + + inlineUIContainer.Child = grid; + + _paragraph.Inlines.Add(inlineUIContainer); + } + + public void AddChild(IAddChild child) + { + _flowDocument.AddChild(child); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyTable.cs b/components/MarkdownTextBlock/src/TextElements/MyTable.cs new file mode 100644 index 00000000..815361c8 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyTable.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.Tables; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyTable : IAddChild +{ + private Table _table; + private Paragraph _paragraph; + private MyTableUIElement _tableElement; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyTable(Table table) + { + _table = table; + _paragraph = new Paragraph(); + var row = table.FirstOrDefault() as TableRow; + var column = row == null ? 0 : row.Count; + + _tableElement = new MyTableUIElement + ( + column, + table.Count, + 1, + new SolidColorBrush(Colors.Gray) + ); + + var inlineUIContainer = new InlineUIContainer(); + inlineUIContainer.Child = _tableElement; + _paragraph.Inlines.Add(inlineUIContainer); + } + + public void AddChild(IAddChild child) + { + if (child is MyTableCell cellChild) + { + var cell = cellChild.Container; + + Grid.SetColumn(cell, cellChild.ColumnIndex); + Grid.SetRow(cell, cellChild.RowIndex); + Grid.SetColumnSpan(cell, cellChild.ColumnSpan); + Grid.SetRowSpan(cell, cellChild.RowSpan); + + _tableElement.Children.Add(cell); + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyTableCell.cs b/components/MarkdownTextBlock/src/TextElements/MyTableCell.cs new file mode 100644 index 00000000..d6793b62 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyTableCell.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.Tables; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyTableCell : IAddChild +{ + private TableCell _tableCell; + private Paragraph _paragraph = new Paragraph(); + private MyFlowDocument _flowDocument; + private bool _isHeader; + private int _columnIndex; + private int _rowIndex; + private Grid _container; + + public TextElement TextElement + { + get => _paragraph; + } + + public Grid Container + { + get => _container; + } + + public int ColumnSpan + { + get => _tableCell.ColumnSpan; + } + + public int RowSpan + { + get => _tableCell.RowSpan; + } + + public int ColumnIndex + { + get => _columnIndex; + } + + public int RowIndex + { + get => _rowIndex; + } + + public MyTableCell(TableCell tableCell, TextAlignment textAlignment, bool isHeader, int columnIndex, int rowIndex) + { + _isHeader = isHeader; + _tableCell = tableCell; + _columnIndex = columnIndex; + _rowIndex = rowIndex; + _container = new Grid(); + + _flowDocument = new MyFlowDocument(tableCell); + _flowDocument.RichTextBlock.TextWrapping = TextWrapping.Wrap; + _flowDocument.RichTextBlock.TextAlignment = textAlignment; + _flowDocument.RichTextBlock.HorizontalTextAlignment = textAlignment; + _flowDocument.RichTextBlock.HorizontalAlignment = textAlignment switch + { + TextAlignment.Left => HorizontalAlignment.Left, + TextAlignment.Center => HorizontalAlignment.Center, + TextAlignment.Right => HorizontalAlignment.Right, + _ => HorizontalAlignment.Left, + }; + + _container.Padding = new Thickness(4); + if (_isHeader) + { + _flowDocument.RichTextBlock.FontWeight = FontWeights.Bold; + } + _flowDocument.RichTextBlock.HorizontalAlignment = textAlignment switch + { + TextAlignment.Left => HorizontalAlignment.Left, + TextAlignment.Center => HorizontalAlignment.Center, + TextAlignment.Right => HorizontalAlignment.Right, + _ => HorizontalAlignment.Left, + }; + _container.Children.Add(_flowDocument.RichTextBlock); + } + + public void AddChild(IAddChild child) + { + _flowDocument.AddChild(child); + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyTableRow.cs b/components/MarkdownTextBlock/src/TextElements/MyTableRow.cs new file mode 100644 index 00000000..4574ef8c --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyTableRow.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.Tables; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyTableRow : IAddChild +{ + private TableRow _tableRow; + private StackPanel _stackPanel; + private Paragraph _paragraph; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyTableRow(TableRow tableRow) + { + _tableRow = tableRow; + _paragraph = new Paragraph(); + + _stackPanel = new StackPanel(); + _stackPanel.Orientation = Orientation.Horizontal; + var inlineUIContainer = new InlineUIContainer(); + inlineUIContainer.Child = _stackPanel; + _paragraph.Inlines.Add(inlineUIContainer); + } + + public void AddChild(IAddChild child) + { + if (child is MyTableCell cellChild) + { + var richTextBlock = new RichTextBlock(); + richTextBlock.Blocks.Add((Paragraph)cellChild.TextElement); + _stackPanel.Children.Add(richTextBlock); + } + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyTableUIElement.cs b/components/MarkdownTextBlock/src/TextElements/MyTableUIElement.cs new file mode 100644 index 00000000..dfe6883d --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyTableUIElement.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyTableUIElement : Panel +{ + private readonly int _columnCount; + private readonly int _rowCount; + private readonly double _borderThickness; + private double[]? _columnWidths; + private double[]? _rowHeights; + + public MyTableUIElement(int columnCount, int rowCount, double borderThickness, Brush borderBrush) + { + _columnCount = columnCount; + _rowCount = rowCount; + _borderThickness = borderThickness; + for (int col = 0; col < columnCount + 1; col++) + { + Children.Add(new Rectangle { Fill = borderBrush }); + } + + for (int row = 0; row < rowCount + 1; row++) + { + Children.Add(new Rectangle { Fill = borderBrush }); + } + } + + // Helper method to enumerate FrameworkElements instead of UIElements. + private IEnumerable ContentChildren + { + get + { + for (int i = _columnCount + _rowCount + 2; i < Children.Count; i++) + { + yield return (FrameworkElement)Children[i]; + } + } + } + + // Helper method to get table vertical edges. + private IEnumerable VerticalLines + { + get + { + for (int i = 0; i < _columnCount + 1; i++) + { + yield return (Rectangle)Children[i]; + } + } + } + + // Helper method to get table horizontal edges. + private IEnumerable HorizontalLines + { + get + { + for (int i = _columnCount + 1; i < _columnCount + _rowCount + 2; i++) + { + yield return (Rectangle)Children[i]; + } + } + } + + protected override Size MeasureOverride(Size availableSize) + { + // Measure the width of each column, with no horizontal width restrictions. + var naturalColumnWidths = new double[_columnCount]; + foreach (var child in ContentChildren) + { + var columnIndex = Grid.GetColumn(child); + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + naturalColumnWidths[columnIndex] = Math.Max(naturalColumnWidths[columnIndex], child.DesiredSize.Width); + } + + // Now figure out the actual column widths. + var remainingContentWidth = availableSize.Width - ((_columnCount + 1) * _borderThickness); + _columnWidths = new double[_columnCount]; + int remainingColumnCount = _columnCount; + while (remainingColumnCount > 0) + { + // Calculate the fair width of all columns. + double fairWidth = Math.Max(0, remainingContentWidth / remainingColumnCount); + + // Are there any columns less than that? If so, they get what they are asking for. + bool recalculationNeeded = false; + for (int i = 0; i < _columnCount; i++) + { + if (_columnWidths[i] == 0 && naturalColumnWidths[i] < fairWidth) + { + _columnWidths[i] = naturalColumnWidths[i]; + remainingColumnCount--; + remainingContentWidth -= _columnWidths[i]; + recalculationNeeded = true; + } + } + + // If there are no columns less than the fair width, every remaining column gets that width. + if (recalculationNeeded == false) + { + for (int i = 0; i < _columnCount; i++) + { + if (_columnWidths[i] == 0) + { + _columnWidths[i] = fairWidth; + } + } + + break; + } + } + + // TODO: we can skip this step if none of the column widths changed, and just re-use + // the row heights we obtained earlier. + + // Now measure row heights. + _rowHeights = new double[_rowCount]; + foreach (var child in ContentChildren) + { + var columnIndex = Grid.GetColumn(child); + var rowIndex = Grid.GetRow(child); + child.Measure(new Size(_columnWidths[columnIndex], double.PositiveInfinity)); + _rowHeights[rowIndex] = Math.Max(_rowHeights[rowIndex], child.DesiredSize.Height); + } + + return new Size( + _columnWidths.Sum() + (_borderThickness * (_columnCount + 1)), + _rowHeights.Sum() + ((_rowCount + 1) * _borderThickness)); + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_columnWidths == null || _rowHeights == null) + { + throw new InvalidOperationException("Expected Measure to be called first."); + } + + // Arrange content. + foreach (var child in ContentChildren) + { + var columnIndex = Grid.GetColumn(child); + var rowIndex = Grid.GetRow(child); + + var rect = new Rect(_borderThickness, 0, 0, 0); + + for (int col = 0; col < columnIndex; col++) + { + rect.X += _borderThickness + _columnWidths[col]; + } + + rect.Y = _borderThickness; + for (int row = 0; row < rowIndex; row++) + { + rect.Y += _borderThickness + _rowHeights[row]; + } + + rect.Width = _columnWidths[columnIndex]; + rect.Height = _rowHeights[rowIndex]; + child.Arrange(rect); + } + + // Arrange vertical border elements. + { + int colIndex = 0; + double x = 0; + foreach (var borderLine in VerticalLines) + { + borderLine.Arrange(new Rect(x, 0, _borderThickness, finalSize.Height)); + if (colIndex >= _columnWidths.Length) + { + break; + } + + x += _borderThickness + _columnWidths[colIndex]; + colIndex++; + } + } + + // Arrange horizontal border elements. + { + int rowIndex = 0; + double y = 0; + foreach (var borderLine in HorizontalLines) + { + borderLine.Arrange(new Rect(0, y, finalSize.Width, _borderThickness)); + if (rowIndex >= _rowHeights.Length) + { + break; + } + + y += _borderThickness + _rowHeights[rowIndex]; + rowIndex++; + } + } + + return finalSize; + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyTaskListCheckBox.cs b/components/MarkdownTextBlock/src/TextElements/MyTaskListCheckBox.cs new file mode 100644 index 00000000..1eba2a62 --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyTaskListCheckBox.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Extensions.TaskLists; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyTaskListCheckBox : IAddChild +{ + private TaskList _taskList; + public TextElement TextElement { get; private set; } + + public MyTaskListCheckBox(TaskList taskList) + { + _taskList = taskList; + var grid = new Grid(); + CompositeTransform3D transform = new CompositeTransform3D(); + transform.TranslateY = 2; + grid.Transform3D = transform; + grid.Width = 16; + grid.Height = 16; + grid.Margin = new Thickness(2, 0, 2, 0); + grid.BorderThickness = new Thickness(1); + grid.BorderBrush = new SolidColorBrush(Colors.Gray); + FontIcon icon = new FontIcon(); + icon.FontSize = 16; + icon.HorizontalAlignment = HorizontalAlignment.Center; + icon.VerticalAlignment = VerticalAlignment.Center; + icon.Glyph = "\uE73E"; + grid.Children.Add(taskList.Checked ? icon : new TextBlock()); + grid.Padding = new Thickness(0); + grid.CornerRadius = new CornerRadius(2); + var inlineUIContainer = new InlineUIContainer(); + inlineUIContainer.Child = grid; + TextElement = inlineUIContainer; + } + + public void AddChild(IAddChild child) + { + } +} diff --git a/components/MarkdownTextBlock/src/TextElements/MyThematicBreak.cs b/components/MarkdownTextBlock/src/TextElements/MyThematicBreak.cs new file mode 100644 index 00000000..c1f945ca --- /dev/null +++ b/components/MarkdownTextBlock/src/TextElements/MyThematicBreak.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Markdig.Syntax; + +namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements; + +internal class MyThematicBreak : IAddChild +{ + private ThematicBreakBlock _thematicBreakBlock; + private Paragraph _paragraph; + + public TextElement TextElement + { + get => _paragraph; + } + + public MyThematicBreak(ThematicBreakBlock thematicBreakBlock) + { + _thematicBreakBlock = thematicBreakBlock; + _paragraph = new Paragraph(); + + var inlineUIContainer = new InlineUIContainer(); + var border = new Border(); + border.Width = 500; + border.BorderThickness = new Thickness(1); + border.Margin = new Thickness(0, 4, 0, 4); + border.BorderBrush = new SolidColorBrush(Colors.Gray); + border.Height = 1; + border.HorizontalAlignment = HorizontalAlignment.Stretch; + inlineUIContainer.Child = border; + _paragraph.Inlines.Add(inlineUIContainer); + } + + public void AddChild(IAddChild child) {} +} diff --git a/components/MarkdownTextBlock/src/Themes/Generic.xaml b/components/MarkdownTextBlock/src/Themes/Generic.xaml new file mode 100644 index 00000000..c16c591a --- /dev/null +++ b/components/MarkdownTextBlock/src/Themes/Generic.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestClass.cs b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestClass.cs new file mode 100644 index 00000000..19eef0a7 --- /dev/null +++ b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestClass.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.Labs.WinUI.MarkdownTextBlock; + +namespace MarkdownTextBlockExperiment.Tests; + +[TestClass] +public partial class ExampleMarkdownTextBlockTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(MarkdownTextBlock).Assembly; + var type = assembly.GetType(typeof(MarkdownTextBlock).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find MarkdownTextBlock type."); + Assert.AreEqual(typeof(MarkdownTextBlock), type, "Type of MarkdownTextBlock does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new MarkdownTextBlock(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleMarkdownTextBlockTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("MarkdownTextBlockControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleMarkdownTextBlockTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new MarkdownTextBlock(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new MarkdownTextBlock(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new MarkdownTextBlock(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml new file mode 100644 index 00000000..832636a1 --- /dev/null +++ b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml.cs b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml.cs new file mode 100644 index 00000000..e14ae117 --- /dev/null +++ b/components/MarkdownTextBlock/tests/ExampleMarkdownTextBlockTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MarkdownTextBlockExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleMarkdownTextBlockTestPage : Page +{ + public ExampleMarkdownTextBlockTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.projitems b/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.projitems new file mode 100644 index 00000000..0ef9adee --- /dev/null +++ b/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 6F0FA793-7CF0-41F9-B7D9-260039B62C8A + + + MarkdownTextBlockExperiment.Tests + + + + + ExampleMarkdownTextBlockTestPage.xaml + + + + + Designer + MSBuild:Compile + + + diff --git a/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.shproj b/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.shproj new file mode 100644 index 00000000..ad69f244 --- /dev/null +++ b/components/MarkdownTextBlock/tests/MarkdownTextBlock.Tests.shproj @@ -0,0 +1,13 @@ + + + + 6F0FA793-7CF0-41F9-B7D9-260039B62C8A + 14.0 + + + + + + + +