Repository: whistyun/Markdown.Avalonia Branch: master Commit: d71a34d8de3c Files: 422 Total size: 1.3 MB Directory structure: gitextract_t8vp1hh_/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── ColorDocument.Avalonia/ │ ├── ClassNames.cs │ ├── ColorDocument.Avalonia.csproj │ ├── DocumentElement.cs │ ├── DocumentElements/ │ │ ├── BlockquoteElement.cs │ │ ├── CTextBlockElement.cs │ │ ├── DocumentRootElement.cs │ │ ├── HeaderElement.cs │ │ ├── ListBlockElement.cs │ │ ├── ListItemElement.cs │ │ ├── NoteBlockElement.cs │ │ ├── PlainCodeBlockElement.cs │ │ ├── TableBlockElement.cs │ │ ├── TableCellElement.cs │ │ ├── TextBlockElement.cs │ │ ├── TextMarkerStyle.cs │ │ └── UnBlockElement.cs │ ├── EnumerableExt.cs │ ├── NumberToOrder.cs │ ├── RegionUtil.cs │ ├── SelectionList.cs │ └── SelectionUtil.cs ├── ColorTextBlock.Avalonia/ │ ├── CBold.cs │ ├── CCode.cs │ ├── CHyperlink.cs │ ├── CImage.cs │ ├── CInline.cs │ ├── CInlineUIContainer.cs │ ├── CItalic.cs │ ├── CLineBreak.cs │ ├── CRun.cs │ ├── CSpan.cs │ ├── CStrikethrough.cs │ ├── CTextBlock.cs │ ├── CTextBlockAutomationPeer.cs │ ├── CUnderline.cs │ ├── ColorTextBlock.Avalonia.csproj │ ├── Fonts/ │ │ └── FontFamilyCollector.cs │ ├── Geometies/ │ │ ├── CGeometry.cs │ │ ├── DecoratorGeometry.cs │ │ ├── DummyGeometryForControl.cs │ │ ├── ImageGeometry.cs │ │ ├── LineBreakMarkGeometry.cs │ │ ├── TextGeometry.cs │ │ └── TextLineGeometry.cs │ ├── StringToRunConverter.cs │ ├── TextPointer.cs │ └── TextVerticalAlignment.cs ├── LICENSE.txt ├── Markdown.Avalonia/ │ ├── Markdown.Avalonia.csproj │ ├── MarkdownScrollViewer.cs │ ├── MdAvPlugins.cs │ └── Properties/ │ └── AssemblyInfo.cs ├── Markdown.Avalonia.Html/ │ ├── Core/ │ │ ├── Parsers/ │ │ │ ├── ButtonParser.cs │ │ │ ├── CodeBlockParser.cs │ │ │ ├── CommentParser.cs │ │ │ ├── DetailsParser.cs │ │ │ ├── HorizontalRuleParser.cs │ │ │ ├── ITagParser.cs │ │ │ ├── ImageParser.cs │ │ │ ├── InputParser.cs │ │ │ ├── OrderListParser.cs │ │ │ ├── ProgressParser.cs │ │ │ ├── TagIgnoreParser.cs │ │ │ ├── TextAreaParser.cs │ │ │ ├── TextNodeParser.cs │ │ │ ├── TypicalBlockParser.cs │ │ │ ├── TypicalBlockParser.tsv │ │ │ ├── TypicalInlineParser.cs │ │ │ ├── TypicalInlineParser.tsv │ │ │ ├── TypicalParseInfo.cs │ │ │ └── UnorderListParser.cs │ │ ├── Parsers.MarkdigExtensions/ │ │ │ ├── FigureParser.cs │ │ │ └── GridTableParser.cs │ │ ├── ReplaceManager.cs │ │ ├── Tags.cs │ │ ├── TextRange.cs │ │ ├── UnknownTagException.cs │ │ ├── UnknownTagsOption.cs │ │ └── Utils/ │ │ ├── DocUtils.cs │ │ ├── EnumerableExt.cs │ │ ├── HtmlUtils.cs │ │ ├── Length.cs │ │ ├── NodeCollectionExt.cs │ │ └── StringExt.cs │ ├── HtmlBlockParser.cs │ ├── HtmlInlineParser.cs │ ├── HtmlPlugin.cs │ ├── Markdown.Avalonia.Html.csproj │ ├── SimpleHtmlUtils.cs │ └── Tables/ │ ├── AutoScaleColumnDefinitions.cs │ ├── Table.cs │ └── TableCell.cs ├── Markdown.Avalonia.Svg/ │ ├── Markdown.Avalonia.Svg.csproj │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── SvgFormat.cs │ ├── SvgImageResolver.cs │ └── VectorImage.cs ├── Markdown.Avalonia.SyntaxHigh/ │ ├── Alias.cs │ ├── CodeBlockElement.cs │ ├── CodePad.cs │ ├── Extensions/ │ │ ├── HSV.cs │ │ ├── HighlightWrapper.cs │ │ ├── MixHighlightingBrush.cs │ │ └── SyntaxHighlightWrapperExtension.cs │ ├── Markdown.Avalonia.SyntaxHigh.csproj │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── StyleCollections/ │ │ ├── AppendixOfDefaultTheme.axaml │ │ ├── AppendixOfDefaultTheme.axaml.cs │ │ ├── AppendixOfFluentAvalonia.axaml │ │ ├── AppendixOfFluentAvalonia.axaml.cs │ │ ├── AppendixOfFluentTheme.axaml │ │ └── AppendixOfFluentTheme.axaml.cs │ ├── StyleEdit.cs │ ├── SyntaxHighlightProvider.cs │ ├── SyntaxHiglight.cs │ ├── SyntaxOverride.cs │ └── ThemeDetector.cs ├── Markdown.Avalonia.Tight/ │ ├── CascadeDictionary.cs │ ├── ChatAISetup.cs │ ├── ContainerSwitcher.cs │ ├── Controls/ │ │ ├── AutoScaleColumnDefinitions.cs │ │ └── Rule.cs │ ├── CopyMode.cs │ ├── EmojiTable.cs │ ├── EmojiTable.txt │ ├── EngineUpg.cs │ ├── Extensions/ │ │ ├── AlphaExtension.cs │ │ ├── ComplementaryExtension.cs │ │ ├── DivideColorExtension.cs │ │ └── MultiplyExtension.cs │ ├── Header.cs │ ├── HeaderScrolled.cs │ ├── HeaderScrolledEventArgs.cs │ ├── IMarkdownEngine.cs │ ├── IMarkdownEngine2.cs │ ├── IMarkdownEngineBase.cs │ ├── Markdown.Avalonia.Tight.csproj │ ├── Markdown.cs │ ├── MarkdownScrollViewer.cs │ ├── MarkdownStyle.cs │ ├── MdAvPlugins.cs │ ├── Parsers/ │ │ ├── BlockParser.cs │ │ ├── BlockParser2.cs │ │ ├── BlockParserExt.cs │ │ ├── Builtin/ │ │ │ ├── AbstractHeaderParser.cs │ │ │ ├── AbstractHorizontalParser.cs │ │ │ ├── AbstractListParser.cs │ │ │ ├── AtxHeaderParser.cs │ │ │ ├── BlockquotesParser.cs │ │ │ ├── CommonHorizontalParser.cs │ │ │ ├── CommonListParser.cs │ │ │ ├── ContainerBlockParser.cs │ │ │ ├── ExtHorizontalParser.cs │ │ │ ├── ExtListParser.cs │ │ │ ├── FencedCodeBlockParser.cs │ │ │ ├── IndentCodeBlockParser.cs │ │ │ ├── NoteParser.cs │ │ │ ├── SetextHeaderParser.cs │ │ │ └── TableParser.cs │ │ ├── InlineParser.cs │ │ └── ParseStatus.cs │ ├── Plugins/ │ │ ├── BlockOverride2.cs │ │ ├── IBlockOverrider.cs │ │ ├── IMdAvPlugin.cs │ │ ├── IStyleEditor.cs │ │ └── SetupInfo.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── StyleCollections/ │ │ ├── INamedStyle.cs │ │ ├── MarkdownStyleDefaultTheme.axaml │ │ ├── MarkdownStyleDefaultTheme.axaml.cs │ │ ├── MarkdownStyleFluentAvalonia.axaml │ │ ├── MarkdownStyleFluentAvalonia.axaml.cs │ │ ├── MarkdownStyleFluentTheme.axaml │ │ ├── MarkdownStyleFluentTheme.axaml.cs │ │ ├── MarkdownStyleGithubLike.axaml │ │ ├── MarkdownStyleGithubLike.axaml.cs │ │ ├── MarkdownStyleStandard.axaml │ │ └── MarkdownStyleStandard.axaml.cs │ ├── Tables/ │ │ ├── ITable.cs │ │ ├── ITableCell.cs │ │ ├── TextileTable.cs │ │ └── TextileTableCell.cs │ ├── TextMarkerStyle.cs │ └── Utils/ │ ├── DefaultBitmapLoader.cs │ ├── DefaultHyperlinkCommand.cs │ ├── DefaultPathResolver.cs │ ├── Helper.cs │ ├── IBitmapLoader.cs │ ├── IContainerBlockHandler.cs │ ├── IImageResolver.cs │ ├── IPathResolver.cs │ ├── InterassemblyUtil.cs │ ├── TextUtil.cs │ └── ThemeDetector.cs ├── Markdown.Avalonia.props ├── Markdown.Avalonia.sln ├── Packages.ps1 ├── README.md ├── benchmark/ │ └── MdAvBench/ │ ├── Apps/ │ │ ├── App.axaml │ │ └── App.axaml.cs │ ├── BenchmarkOfCTextBlock.cs │ ├── MdAvBench.csproj │ ├── Program.cs │ ├── StringLogger.cs │ └── Xamls/ │ ├── CTextBlockData.axaml │ ├── CTextBlockData.axaml.cs │ ├── TextBlockData.axaml │ └── TextBlockData.axaml.cs ├── demos/ │ ├── Markdown.AvaloniaDemo/ │ │ ├── App.axaml │ │ ├── App.axaml.cs │ │ ├── Assets/ │ │ │ ├── AppendingStyles.axaml │ │ │ ├── Pegasus-Mode.xshd │ │ │ └── XamlTemplate.txt │ │ ├── DynamicStyleBehavior.cs │ │ ├── MainWindow.md │ │ ├── Markdown.AvaloniaDemo.csproj │ │ ├── MyConverter.cs │ │ ├── Program.cs │ │ ├── ViewLocator.cs │ │ ├── ViewModels/ │ │ │ ├── MainWindowViewModel.cs │ │ │ └── ViewModelBase.cs │ │ ├── Views/ │ │ │ ├── MainWindow.axaml │ │ │ └── MainWindow.axaml.cs │ │ └── nuget.config │ ├── Markdown.AvaloniaFluentAvaloniaDemo/ │ │ ├── App.axaml │ │ ├── App.axaml.cs │ │ ├── Assets/ │ │ │ ├── AppendingStyles.axaml │ │ │ └── XamlTemplate.txt │ │ ├── Assets2/ │ │ │ └── MainWindow.md │ │ ├── DynamicStyleBehavior.cs │ │ ├── MainWindow.md │ │ ├── Markdown.AvaloniaFluentAvaloniaDemo.csproj │ │ ├── MyConverter.cs │ │ ├── Program.cs │ │ ├── ViewLocator.cs │ │ ├── ViewModels/ │ │ │ ├── MainWindowViewModel.cs │ │ │ └── ViewModelBase.cs │ │ ├── Views/ │ │ │ ├── MainWindow.axaml │ │ │ └── MainWindow.axaml.cs │ │ └── nuget.config │ └── Markdown.AvaloniaFluentDemo/ │ ├── App.axaml │ ├── App.axaml.cs │ ├── Assets/ │ │ ├── AppendingStyles.axaml │ │ └── XamlTemplate.txt │ ├── Assets2/ │ │ └── MainWindow.md │ ├── DynamicStyleBehavior.cs │ ├── MainWindow.md │ ├── Markdown.AvaloniaFluentDemo.csproj │ ├── MyConverter.cs │ ├── Program.cs │ ├── ViewLocator.cs │ ├── ViewModels/ │ │ ├── MainWindowViewModel.cs │ │ └── ViewModelBase.cs │ ├── Views/ │ │ ├── MainWindow.axaml │ │ └── MainWindow.axaml.cs │ └── nuget.config ├── example/ │ ├── CustomStyle/ │ │ ├── CustomStyle/ │ │ │ ├── App.axaml │ │ │ ├── App.axaml.cs │ │ │ ├── CustomStyle.csproj │ │ │ ├── MainWindow.axaml │ │ │ ├── MainWindow.axaml.cs │ │ │ ├── Program.cs │ │ │ ├── SetStyles.axaml │ │ │ ├── SetStyles.axaml.cs │ │ │ ├── UseEmbeddedStyle.axaml │ │ │ ├── UseEmbeddedStyle.axaml.cs │ │ │ └── nuget.config │ │ └── CustomStyle.sln │ └── HowToUse/ │ ├── HowToUse/ │ │ ├── App.axaml │ │ ├── App.axaml.cs │ │ ├── HowToUse.csproj │ │ ├── MainWindow.axaml │ │ ├── MainWindow.axaml.cs │ │ ├── Program.cs │ │ ├── UseBinding.axaml │ │ ├── UseBinding.axaml.cs │ │ ├── UseBindingViewModel.cs │ │ ├── WriteMarkdownInXaml.axaml │ │ ├── WriteMarkdownInXaml.axaml.cs │ │ └── nuget.config │ └── HowToUse.sln ├── key.snk ├── pack_readme/ │ ├── Markdown.Avalonia.Html.md │ ├── Markdown.Avalonia.Svg.md │ ├── Markdown.Avalonia.SyntaxHigh.md │ ├── Markdown.Avalonia.Tight.md │ └── Markdown.Avalonia.md ├── packs/ │ ├── ColorTextBlock.Avalonia.11.0.0-d2.nupkg │ ├── Markdown.Avalonia.11.0.0-d2.nupkg │ ├── Markdown.Avalonia.Html.11.0.0-d2.nupkg │ ├── Markdown.Avalonia.Svg.11.0.0-d2.nupkg │ ├── Markdown.Avalonia.SyntaxHigh.11.0.0-d2.nupkg │ └── Markdown.Avalonia.Tight.11.0.0-d2.nupkg └── tests/ ├── UnitTest.Base/ │ ├── Apps/ │ │ ├── App.axaml │ │ └── App.axaml.cs │ ├── ChangeOutputPathNamer.cs │ ├── UnitTest.Base.csproj │ ├── UnitTestBase.cs │ └── Utils/ │ ├── BrokenXamlWriter.cs │ ├── RunOnUIAttribute.cs │ ├── TextUtil.cs │ └── Util.cs ├── UnitTest.CTxt/ │ ├── .gitignore │ ├── Assets/ │ │ └── Fonts/ │ │ └── license.html │ ├── Texts/ │ │ └── MainWindow.md │ ├── UnitTest.CTxt.csproj │ ├── UnitTestCTxt.cs │ ├── Utils/ │ │ ├── ApprovalImageWriter.cs │ │ └── ImageFileApprover.cs │ └── Xamls/ │ ├── Test1.axaml │ ├── Test1.axaml.cs │ ├── Test2.axaml │ ├── Test2.axaml.cs │ ├── Test3.axaml │ ├── Test3.axaml.cs │ ├── Test4.axaml │ ├── Test4.axaml.cs │ ├── Test5.axaml │ ├── Test5.axaml.cs │ ├── Test6.axaml │ ├── Test6.axaml.cs │ ├── Test7.axaml │ ├── Test7.axaml.cs │ ├── Test99.axaml │ └── Test99.axaml.cs ├── UnitTest.Md/ │ ├── .gitignore │ ├── Out/ │ │ ├── UnitTestMd.Transform_givenBlockqoute_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenCodes_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenContainer_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenEmoji.approved.txt │ │ ├── UnitTestMd.Transform_givenHorizontalRules_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenImages_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenLinksInline_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenLists1_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenLists2_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenLists3_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenMixing2_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenMixing_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenTables1_generatesExpectedResult.approved.txt │ │ ├── UnitTestMd.Transform_givenTest1_generatesExpectedResult.approved.txt │ │ └── UnitTestMd.Transform_givenTextStyles_generatesExpectedResult.approved.txt │ ├── Texts/ │ │ ├── Blockquite.md │ │ ├── Codes.md │ │ ├── ContainerBlock.md │ │ ├── Emoji.md │ │ ├── HorizontalRules.md │ │ ├── Images.md │ │ ├── Links_inline_style.md │ │ ├── Lists1.md │ │ ├── Lists2.md │ │ ├── Lists3.md │ │ ├── Mixing.md │ │ ├── Mixing2.md │ │ ├── Tables.md │ │ ├── Test1.md │ │ └── Text_style.md │ ├── UnitTest.Md.csproj │ └── UnitTestMd.cs ├── UnitTest.MdHtml/ │ ├── .gitignore │ ├── Out/ │ │ ├── UnitTest.Button.approved.txt │ │ ├── UnitTest.CodeBlock.approved.txt │ │ ├── UnitTest.Details.approved.txt │ │ ├── UnitTest.InlineCode.approved.txt │ │ ├── UnitTest.Input.approved.txt │ │ ├── UnitTest.List.approved.txt │ │ ├── UnitTest.Mixing.approved.txt │ │ ├── UnitTest.Progres.approved.txt │ │ ├── UnitTest.Table.approved.txt │ │ ├── UnitTest.TypicalBlock.approved.txt │ │ └── UnitTest.TypicalInline.approved.txt │ ├── Texts/ │ │ ├── Button.html │ │ ├── CodeBlock.html │ │ ├── Details.html │ │ ├── InlineCode.html │ │ ├── Input.html │ │ ├── List.html │ │ ├── Mixing.html │ │ ├── Progres.html │ │ ├── Table.html │ │ ├── TypicalBlock.html │ │ └── TypicalInline.html │ ├── UnitTest.MdHtml.csproj │ ├── UnitTest.cs │ └── Utils.cs ├── UnitTest.MdSyntax/ │ ├── .gitignore │ ├── Out/ │ │ ├── UnitTestMdSyntax.Transform_givenBlockqoute_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenCodes_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenContainer_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenEmoji.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenHorizontalRules_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenImages_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenLinksInline_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenLists1_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenLists2_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenLists3_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenMixing2_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenMixing_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenTables1_generatesExpectedResult.approved.txt │ │ ├── UnitTestMdSyntax.Transform_givenTest1_generatesExpectedResult.approved.txt │ │ └── UnitTestMdSyntax.Transform_givenTextStyles_generatesExpectedResult.approved.txt │ ├── Texts/ │ │ ├── Blockquite.md │ │ ├── Codes.md │ │ ├── ContainerBlock.md │ │ ├── Emoji.md │ │ ├── HorizontalRules.md │ │ ├── Images.md │ │ ├── Links_inline_style.md │ │ ├── Lists1.md │ │ ├── Lists2.md │ │ ├── Lists3.md │ │ ├── Mixing.md │ │ ├── Mixing2.md │ │ ├── Tables.md │ │ ├── Test1.md │ │ └── Text_style.md │ ├── UnitTest.MdSyntax.csproj │ ├── UnitTestConverter.cs │ └── UnitTestMdSyntax.cs └── UnitTest.props ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp *.sln merge=union *.csproj merge=union *.vbproj merge=union *.fsproj merge=union *.dbproj merge=union # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .github/workflows/build.yml ================================================ name: .NET on: pull_request: branches: [ master ] workflow_dispatch: jobs: build: runs-on: windows-latest strategy: matrix: versions: [ 12.0.0, 12.0.1 ] steps: - uses: actions/checkout@v2 with: submodules: 'true' - name: Clean run: dotnet clean env: AVA_VER: ${{ matrix.versions }} - name: Restore run: dotnet restore env: AVA_VER: ${{ matrix.versions }} - name: Build run: dotnet build --no-restore env: AVA_VER: ${{ matrix.versions }} - name: Test run: dotnet test --no-build --verbosity normal env: AVA_VER: ${{ matrix.versions }} ================================================ FILE: .gitignore ================================================ ################# ## Eclipse ################# *.pydevproject .project .metadata bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .classpath .settings/ .loadpath # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # PDT-specific .buildpath ################# ## Visual Studio ################# ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. .vs packages # User-specific files *.suo *.user *.sln.docstates # Build results [Dd]ebug/ [Rr]elease/ *_i.c *_p.c *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.vspscc .builds *.dotCover ## TODO: If you have NuGet Package Restore enabled, uncomment this #packages/ # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf # Visual Studio profiler *.psess *.vsp # ReSharper is a .NET coding add-in _ReSharper* # Installshield output folder [Ee]xpress # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish # Others [Bb]in [Oo]bj sql TestResults *.Cache ClientBin stylecop.* ~$* *.dbmdl Generated_Code #added for RIA/Silverlight projects # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML ############ ## Windows ############ # Windows image file caches Thumbs.db # Folder config file Desktop.ini ############# ## Python ############# *.py[co] # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg # Mac crap .DS_Store ================================================ FILE: ColorDocument.Avalonia/ClassNames.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ColorDocument.Avalonia { public static class ClassNames { public const string Heading1Class = "Heading1"; public const string Heading2Class = "Heading2"; public const string Heading3Class = "Heading3"; public const string Heading4Class = "Heading4"; public const string Heading5Class = "Heading5"; public const string Heading6Class = "Heading6"; public const string CodeBlockClass = "CodeBlock"; public const string ContainerBlockClass = "ContainerBlock"; public const string NoContainerClass = "NoContainer"; public const string BlockquoteClass = "Blockquote"; public const string NoteClass = "Note"; public const string ParagraphClass = "Paragraph"; public const string TableClass = "Table"; public const string TableHeaderClass = "TableHeader"; public const string TableFirstRowClass = "FirstTableRow"; public const string TableRowOddClass = "OddTableRow"; public const string TableRowEvenClass = "EvenTableRow"; public const string TableLastRowClass = "LastTableRow"; public const string TableFooterClass = "TableFooter"; public const string ListClass = "List"; public const string ListMarkerClass = "ListMarker"; } } ================================================ FILE: ColorDocument.Avalonia/ColorDocument.Avalonia.csproj ================================================  $(PackageTargetFrameworks) $(PackageVersion) Bevan Arps(original); whistyun ColorDocument.Avalonia $(PackageVersion) whistyun Copyright (c) 2024 whistyun https://github.com/whistyun/Markdown.Avalonia/tree/master/ColorDocument.Avalonia/ 9 MIT enable ================================================ FILE: ColorDocument.Avalonia/DocumentElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia { public abstract class DocumentElement { private ISelectionRenderHelper? _helper; public abstract Control Control { get; } public abstract IEnumerable Children { get; } public ISelectionRenderHelper? Helper { get => _helper; set { _helper = value; foreach (var child in Children) child.Helper = value; } } public Rect GetRect(Layoutable anchor) => Control.GetRectInDoc(anchor).GetValueOrDefault(); public abstract void Select(Point from, Point to); public abstract void UnSelect(); public virtual string GetSelectedText() { var builder = new StringBuilder(); ConstructSelectedText(builder); return builder.ToString(); } public abstract void ConstructSelectedText(StringBuilder stringBuilder); } public interface ISelectionRenderHelper { void Register(Control control); void Unregister(Control control); } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/BlockquoteElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { /// /// The document element for expression of blockquote. /// // 引用を表現するためのドキュメント要素 public class BlockquoteElement : DocumentElement { private Lazy _block; private EnumerableEx _children; private SelectionList? _prevSelection; public override Control Control => _block.Value; public override IEnumerable Children => _children; public BlockquoteElement(IEnumerable child) { _block = new Lazy(Create); _children = child.ToEnumerable(); } private Border Create() { var panel = new StackPanel(); panel.Orientation = Orientation.Vertical; panel.Classes.Add(ClassNames.BlockquoteClass); foreach (var child in Children) panel.Children.Add(child.Control); var border = new Border(); border.Classes.Add(ClassNames.BlockquoteClass); border.Child = panel; return border; } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectVertical(Control, _children, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var child in _children) child.UnSelect(); } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; var preLen = builder.Length; foreach (var para in _prevSelection) { para.ConstructSelectedText(builder); if (preLen == builder.Length) continue; if (builder[builder.Length - 1] != '\n') builder.Append('\n'); } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/CTextBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class CTextBlockElement : DocumentElement { private Lazy _text; public string Text => _text.Value.Text; public override Control Control => _text.Value; public override IEnumerable Children => Array.Empty(); public CTextBlockElement(IEnumerable inlines) { _text = new Lazy(() => { var text = new CTextBlock(); foreach (var inline in inlines) text.Content.Add(inline); return text; }); } public CTextBlockElement(IEnumerable inlines, string appendClass) { _text = new Lazy(() => { var text = new CTextBlock(); foreach (var inline in inlines) text.Content.Add(inline); text.Classes.Add(appendClass); return text; }); } public CTextBlockElement(IEnumerable inlines, string appendClass, TextAlignment alignment) { _text = new Lazy(() => { var text = new CTextBlock(); foreach (var inline in inlines) text.Content.Add(inline); text.TextAlignment = alignment; text.Classes.Add(appendClass); return text; }); } public override void Select(Point from, Point to) { var text = _text.Value; var fromPoint = text.CalcuatePointerFrom(from.X, from.Y); var toPoint = text.CalcuatePointerFrom(to.X, to.Y); text.Select(fromPoint, toPoint); } public override void UnSelect() { _text.Value.ClearSelection(); } public override void ConstructSelectedText(StringBuilder builder) { builder.Append(_text.Value.GetSelectedText()); } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/DocumentRootElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { /// /// The top document element. /// public class DocumentRootElement : DocumentElement { private Lazy _block; private EnumerableEx _children; private SelectionList? _prevSelection; public override Control Control => _block.Value; public override IEnumerable Children => _children; public DocumentRootElement(IEnumerable child) { _block = new Lazy(Create); _children = child.ToEnumerable(); } private StackPanel Create() { var panel = new StackPanel(); panel.Orientation = Orientation.Vertical; foreach (var child in _children) panel.Children.Add(child.Control); return panel; } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectVertical(Control, _children, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var child in _children) child.UnSelect(); } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; var preLen = builder.Length; foreach (var para in _prevSelection) { para.ConstructSelectedText(builder); if (preLen == builder.Length) continue; if (builder[builder.Length - 1] != '\n') builder.Append('\n'); } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/HeaderElement.cs ================================================ using ColorTextBlock.Avalonia; using System.Collections.Generic; namespace ColorDocument.Avalonia.DocumentElements { public class HeaderElement : CTextBlockElement { public int Level { get; } public HeaderElement(IEnumerable inlines, int level) : base(inlines, level switch { 1 => ClassNames.Heading1Class, 2 => ClassNames.Heading2Class, 3 => ClassNames.Heading3Class, 4 => ClassNames.Heading4Class, 5 => ClassNames.Heading5Class, _ => ClassNames.Heading6Class, }) { Level = level switch { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, _ => 6, }; } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/ListBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class ListBlockElement : DocumentElement { private Lazy _control; private EnumerableEx _items; private SelectionList? _prevSelection; public override Control Control => _control.Value; public override IEnumerable Children => _items; public ListBlockElement(TextMarkerStyle marker, IEnumerable items) { _control = new Lazy(() => CreateList(marker)); _items = items.ToEnumerable(); } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectVertical(Control, _items, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var c in _items) c.UnSelect(); } private Grid CreateList(TextMarkerStyle marker) { var grid = new Grid(); grid.Classes.Add(ClassNames.ListClass); grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); grid.ColumnDefinitions.Add(new ColumnDefinition()); int index = 0; foreach (var item in _items) { var markerTxt = new CTextBlock(marker.CreateMakerText(index)); var itemCtrl = item.Control; item.MarkerText = markerTxt.Text; // adjust baseline if (FindFirstFrom(itemCtrl) is { } controlTxt) markerTxt.ObserveBaseHeightOf(controlTxt); grid.RowDefinitions.Add(new RowDefinition()); markerTxt.TextAlignment = TextAlignment.Right; markerTxt.TextWrapping = TextWrapping.NoWrap; markerTxt.Classes.Add(ClassNames.ListMarkerClass); Grid.SetRow(markerTxt, index); Grid.SetColumn(markerTxt, 0); grid.Children.Add(markerTxt); Grid.SetRow(itemCtrl, index); Grid.SetColumn(itemCtrl, 1); grid.Children.Add(itemCtrl); ++index; } return grid; static CTextBlock? FindFirstFrom(Control ctrl) { if (ctrl is Panel pnl) { foreach (var chld in pnl.Children) { var res = FindFirstFrom(chld); if (res != null) return res; } } if (ctrl is CTextBlock ctxt) { return ctxt; } return null; } } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; foreach (var para in _prevSelection.Cast()) { builder.Append(para.MarkerText).Append(' '); var listElmTxt = para.GetSelectedText().Replace("\r\n", "\n").Replace('\r', '\n'); builder.Append(listElmTxt); if (!listElmTxt.EndsWith("\n")) builder.Append('\n'); } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/ListItemElement.cs ================================================ using Avalonia; using Avalonia.Controls; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class ListItemElement : DocumentElement { private Lazy _panel; private EnumerableEx _elements; private SelectionList? _prevSelection; internal string MarkerText { get; set; } public override Control Control => _panel.Value; public override IEnumerable Children => _elements; public ListItemElement(IEnumerable contents) { _elements = contents.ToEnumerable(); _panel = new Lazy(() => { var panel = new StackPanel(); foreach (var content in _elements) panel.Children.Add(content.Control); return panel; }); } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectVertical(Control, _elements, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var c in _elements) c.UnSelect(); } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; var preLen = builder.Length; foreach (var para in _prevSelection) { para.ConstructSelectedText(builder); if (preLen == builder.Length) continue; if (builder[builder.Length - 1] != '\n') builder.Append('\n'); } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/NoteBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class NoteBlockElement : DocumentElement { private CTextBlockElement _child; private TextAlignment? _indiAlignment; private Lazy _block; public override Control Control => _block.Value; public override IEnumerable Children => new[] { _child }; public NoteBlockElement(IEnumerable content, TextAlignment? indiAlignment) { _child = new CTextBlockElement(content); _indiAlignment = indiAlignment; _block = new Lazy(Create); } private Border Create() { var note = (CTextBlock)_child.Control; note.Classes.Add(ClassNames.NoteClass); if (_indiAlignment.HasValue) { note.TextAlignment = _indiAlignment.Value; } var result = new Border(); result.Classes.Add(ClassNames.NoteClass); result.Child = note; return result; } public override void Select(Point from, Point to) => _child.Select(from, to); public override void UnSelect() { _child.UnSelect(); } public override void ConstructSelectedText(StringBuilder builder) { _child.ConstructSelectedText(builder); } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/PlainCodeBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Media; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class PlainCodeBlockElement : DocumentElement { private string _code; private Lazy _border; public override Control Control => _border.Value; public override IEnumerable Children => Array.Empty(); public PlainCodeBlockElement(string code) { _code = code; _border = new Lazy(CreateBlock); } public override void Select(Point from, Point to) { } public override void UnSelect() { } public Border CreateBlock() { var ctxt = new TextBlock() { Text = _code, TextWrapping = TextWrapping.NoWrap }; ctxt.Classes.Add(ClassNames.CodeBlockClass); var scrl = new ScrollViewer(); scrl.Classes.Add(ClassNames.CodeBlockClass); scrl.Content = ctxt; scrl.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; var result = new Border(); result.Classes.Add(ClassNames.CodeBlockClass); result.Child = scrl; return result; } public override void ConstructSelectedText(StringBuilder stringBuilder) { stringBuilder.Append(_code); } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/TableBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class TableBlockElement : DocumentElement { private Lazy _table; private TableCellElement[][] _head; private TableCellElement[][] _body; private TableCellElement[][] _foot; private bool _autoAdjust; private EnumerableEx _all; private SelectionList? _prevSelection; public override Control Control => _table.Value; public override IEnumerable Children => _all; public TableBlockElement( IEnumerable> thead, IEnumerable> tbody, IEnumerable> tfoot, bool autoAdjust) : this( thead.Select(ln => ln.ToArray()).ToArray(), tbody.Select(ln => ln.ToArray()).ToArray(), tfoot.Select(ln => ln.ToArray()).ToArray(), autoAdjust) { } public TableBlockElement( TableCellElement[][] thead, TableCellElement[][] tbody, TableCellElement[][] tfoot, bool autoAdjust) { _head = thead; _body = tbody; _foot = tfoot; _all = _head.SelectMany(l => l) .Concat(_body.SelectMany(l => l)) .Concat(_foot.SelectMany(l => l)) .ToEnumerable(); _autoAdjust = autoAdjust; _table = new Lazy(CreateTable); } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectGrid(Control, _all, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var child in _all) child.UnSelect(); } private Border CreateTable() { var rowInfs = new List(); CreateRows(rowInfs, _head, ClassNames.TableHeaderClass); CreateRows(rowInfs, _body); CreateRows(rowInfs, _foot, ClassNames.TableFooterClass); int maxColCnt = rowInfs.Max(r => r.ColumnCount); if (_autoAdjust) { for (int i = 0; i < rowInfs.Count; ++i) { var rowInf = rowInfs[i]; while (rowInf.ColumnCount < maxColCnt) { var cellCtrl = new Border(); Grid.SetRow(cellCtrl, i); Grid.SetColumn(cellCtrl, rowInf.ColumnCount); rowInf.Cells.Add(cellCtrl); rowInf.ColumnCount++; } } } var grid = new Grid(); grid.Classes.Add(ClassNames.TableClass); grid.RowDefinitions.AddRange(Enumerable.Range(0, rowInfs.Count).Select(_ => new RowDefinition())); grid.ColumnDefinitions.AddRange(Enumerable.Range(0, maxColCnt).Select(_ => new ColumnDefinition())); foreach (var rowInf in rowInfs) { foreach (var cell in rowInf.Cells) cell.Classes.AddRange(rowInf.Classes); grid.Children.AddRange(rowInf.Cells); } var border = new Border(); border.Classes.Add(ClassNames.TableClass); border.Child = grid; //var grid = new Grid(); //grid.Classes.Add(ClassNames.TableClass); //var border = new Border(); //border.Classes.Add(ClassNames.TableClass); //border.Child = grid; //int rowOffset = 0; // //int hRowOffset = rowOffset; //List hInfs = SetupRow(grid, _head, ref rowOffset, ClassNames.TableHeaderClass); //int bRowOffset = rowOffset; //List bInfs = SetupRow(grid, _body, ref rowOffset); //int fRowOffset = rowOffset; //List fInfs = SetupRow(grid, _foot, ref rowOffset, ClassNames.TableFooterClass); // //int colCnt = hInfs.Concat(bInfs).Concat(fInfs).Max(i => i.ColumnCount); // //if (_autoAdjust) //{ // AdjustRow(grid, hInfs, hRowOffset, colCnt); // AdjustRow(grid, bInfs, bRowOffset, colCnt); // AdjustRow(grid, fInfs, fRowOffset, colCnt); //} // //foreach (var _ in Enumerable.Range(0, colCnt)) //{ // grid.ColumnDefinitions.Add(new ColumnDefinition()); //} return border; } private static List SetupRow( Grid grid, TableCellElement[][] rows, ref int gridRowIdx, string? classNm = null) { // The list of multi-row cells. // Key: Column index where the target cell is located. var multiRowsAtColIdx = new Dictionary(); var rowInfs = new List(); var maxColCount = 0; int startRowInSection = gridRowIdx; for (var i = 0; i < rows.Length; ++gridRowIdx) { var row = rows[i]; grid.RowDefinitions.Add(new RowDefinition()); // Set up classes for cell in this row. string[] classes; if (classNm is not null) classes = new[] { classNm }; else { var rowIdxInSection = gridRowIdx - startRowInSection; if (rowIdxInSection == 0) { if (i == rows.Length - 1) classes = new[] { ClassNames.TableRowOddClass, ClassNames.TableFirstRowClass, ClassNames.TableLastRowClass }; else classes = new[] { ClassNames.TableRowOddClass, ClassNames.TableFirstRowClass }; } else { var oddOrEven = rowIdxInSection % 2 == 0 ? ClassNames.TableRowOddClass : ClassNames.TableRowEvenClass; if (i == rows.Length - 1) classes = new[] { oddOrEven, ClassNames.TableLastRowClass }; else classes = new[] { oddOrEven }; } } var rowspansColOffset = multiRowsAtColIdx.Sum(e => e.Value.ColSpan); /* * In this row, is space exists to insert cell? * * eg. has space * __________________________________ * | 2x1 cell | 1x1 cell | 1x1 cell | * -> | |‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾| * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * * eg. has no space: multi-rows occupy all space in this row. * __________________________________ * | 2x1 cell | 2x2 cell | * -> | | | * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * */ if (rowspansColOffset == 0 || rowspansColOffset < maxColCount) { int colIdx = 0; foreach (var cell in row) { while (multiRowsAtColIdx.TryGetValue(colIdx, out var span)) { colIdx += span.ColSpan; } var cellCtrl = cell.Control; cell.Row = gridRowIdx; cell.Column = colIdx; Grid.SetRow(cellCtrl, gridRowIdx); Grid.SetColumn(cellCtrl, colIdx); if (cell.RowSpan > 1) Grid.SetRowSpan(cellCtrl, cell.RowSpan); if (cell.ColSpan > 1) Grid.SetColumnSpan(cellCtrl, cell.ColSpan); cellCtrl.Classes.AddRange(classes); grid.Children.Add(cellCtrl); if (cell.RowSpan > 1) { multiRowsAtColIdx[colIdx] = new MdSpan(cell.RowSpan, cell.ColSpan); } colIdx += cell.ColSpan; } rowInfs.Add(new RowInfo(classes, colIdx)); if (maxColCount < colIdx) maxColCount = colIdx; ++i; } else { rowInfs.Add(new RowInfo(classes, rowspansColOffset)); } // Removes multi-row cells, 複数行にまたがるセルの削除(必要なら) foreach (var spanEntry in multiRowsAtColIdx.ToArray()) { if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } } // if any multirow is left, insert an empty row. while (multiRowsAtColIdx.Count > 0) { grid.RowDefinitions.Add(new RowDefinition()); var colOffset = 0; foreach (var spanEntry in multiRowsAtColIdx.OrderBy(tpl => tpl.Key)) { while (colOffset < spanEntry.Key) { var cellCtrl = new Border(); Grid.SetRow(cellCtrl, gridRowIdx); Grid.SetColumn(cellCtrl, colOffset); grid.Children.Add(cellCtrl); colOffset++; } colOffset += spanEntry.Value.ColSpan; if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } rowInfs.Add(new RowInfo(Array.Empty(), colOffset)); gridRowIdx++; } return rowInfs; } private static void AdjustRow(Grid grid, List rowInfs, int rowOffset, int colCnt) { for (var rowIdx = 0; rowIdx < rowInfs.Count; ++rowIdx) { var rowInf = rowInfs[rowIdx]; for (var colIdx = rowInf.ColumnCount; colIdx < colCnt; ++colIdx) { var cellCtrl = new Border(); Grid.SetRow(cellCtrl, rowIdx + rowOffset); Grid.SetColumn(cellCtrl, colIdx); cellCtrl.Classes.AddRange(rowInf.Classes); grid.Children.Insert(SearchInsPos(grid.Children, cellCtrl), cellCtrl); } } int SearchInsPos(IList list, Control tgt) { int min = 0, max = list.Count; var tgtRow = Grid.GetRow(tgt); var tgtCol = Grid.GetColumn(tgt); int mid = 0; while (min < max) { mid = (min + max) / 2; var ctrl = list[mid]; var ctrlRow = Grid.GetRow(ctrl); var ctrlCol = Grid.GetColumn(ctrl); if (tgtRow < ctrlRow || (tgtRow == ctrlRow && tgtCol < ctrlCol)) { max = mid - 1; } else if (tgtRow > ctrlRow || (tgtRow == ctrlRow && tgtCol > ctrlCol)) { min = mid + 1; } else break; } for (var i = Math.Min(Math.Min(Math.Min(min, max), mid), list.Count); i < list.Count; ++i) { var ctrl = list[i]; var ctrlRow = Grid.GetRow(ctrl); var ctrlCol = Grid.GetColumn(ctrl); if (tgtRow < ctrlRow || (tgtRow == ctrlRow && tgtCol < ctrlCol)) { return i; } } return list.Count; } } private void CreateRows(List rowInfs, TableCellElement[][] rows, string? classNm = null) { // The list of multi-row cells. // Key: Column index where the target cell is located. var multiRowsAtColIdx = new Dictionary(); var maxColCount = 0; int detailsRowIdx = 0; for (int i = 0; i < rows.Length;) { var rinf = new RowInf(); SetupClass(rinf, detailsRowIdx++, classNm); var rowspansColOffset = multiRowsAtColIdx.Sum(e => e.Value.ColSpan); /* * In this row, is space exists to insert cell? * * eg. has space * __________________________________ * | 2x1 cell | 1x1 cell | 1x1 cell | * -> | |‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾| * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * * eg. has no space: multi-rows occupy all space in this row. * __________________________________ * | 2x1 cell | 2x2 cell | * -> | | | * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * */ if (rowspansColOffset == 0 || rowspansColOffset < maxColCount) { int colIdx = 0; foreach (var cell in rows[i]) { while (multiRowsAtColIdx.TryGetValue(colIdx, out var span)) { colIdx += span.ColSpan; } var cellCtrl = cell.Control; cell.Row = rowInfs.Count; cell.Column = colIdx; Grid.SetRow(cellCtrl, rowInfs.Count); Grid.SetColumn(cellCtrl, colIdx); if (cell.RowSpan > 1) Grid.SetRowSpan(cellCtrl, cell.RowSpan); if (cell.ColSpan > 1) Grid.SetColumnSpan(cellCtrl, cell.ColSpan); rinf.Cells.Add(cellCtrl); if (cell.RowSpan > 1) { multiRowsAtColIdx[colIdx] = new MdSpan(cell.RowSpan, cell.ColSpan); } colIdx += cell.ColSpan; } foreach (var left in multiRowsAtColIdx.Where(tpl => tpl.Key >= colIdx) .OrderBy(tpl => tpl.Key)) { while (colIdx < left.Key) { var cellCtrl = new Border(); Grid.SetRow(cellCtrl, rowInfs.Count); Grid.SetColumn(cellCtrl, colIdx); rinf.Cells.Add(cellCtrl); ++colIdx; } colIdx += left.Value.ColSpan; } rinf.ColumnCount = colIdx; if (maxColCount < colIdx) maxColCount = colIdx; ++i; } else { rinf.ColumnCount = rowspansColOffset; } rowInfs.Add(rinf); // Removes multi-row cells, 複数行にまたがるセルの削除(必要なら) foreach (var spanEntry in multiRowsAtColIdx.ToArray()) { if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } } // if any multirow is left, insert an empty row. while (multiRowsAtColIdx.Count > 0) { var rinf = new RowInf(); SetupClass(rinf, detailsRowIdx++, classNm); var colIdx = 0; foreach (var spanEntry in multiRowsAtColIdx.OrderBy(tpl => tpl.Key)) { while (colIdx < spanEntry.Key) { var cellCtrl = new Border(); Grid.SetRow(cellCtrl, rowInfs.Count); Grid.SetColumn(cellCtrl, colIdx); rinf.Cells.Add(cellCtrl); colIdx++; } colIdx += spanEntry.Value.ColSpan; if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } rinf.ColumnCount = colIdx; rowInfs.Add(rinf); } if (classNm is null) { rowInfs.Last().Classes.Add(ClassNames.TableLastRowClass); } static void SetupClass(RowInf rinf, int rowIndex, string? classNm) { if (classNm is not null) rinf.Classes.Add(classNm); else if (rowIndex == 0) rinf.Classes.AddRange(new[] { ClassNames.TableRowOddClass, ClassNames.TableFirstRowClass }); else if (rowIndex % 2 == 0) rinf.Classes.Add(ClassNames.TableRowOddClass); else rinf.Classes.Add(ClassNames.TableRowEvenClass); } } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; string[,] cellTxt = new string[ _all.Max(c => c.Row + c.RowSpan), _all.Max(c => c.Column + c.ColSpan) ]; foreach (var para in _prevSelection.Cast()) { cellTxt[para.Row, para.Column] = para.GetSelectedText().TrimEnd().Replace("\r\n", "\r").Replace('\n', '\r'); } for (int i = 0; i < cellTxt.GetLength(0); i++) { var preLen = builder.Length; for (int j = 0; j < cellTxt.GetLength(1); j++) { builder.Append(cellTxt[i, j] ?? ""); builder.Append("\t"); } if (builder.Length - preLen == 0) continue; if (builder[builder.Length - 1] != '\n') builder.Append('\n'); } } class MdSpan { public int Life { get; set; } public int ColSpan { get; } public MdSpan(int l, int c) { Life = l; ColSpan = c; } } class RowInf { public List Classes { get; } = new List(5); public List Cells { get; } = new List(); public int ColumnCount; } class RowInfo { public string[] Classes { get; } public int ColumnCount { get; } public RowInfo(string[] classes, int colCount) { Classes = classes; ColumnCount = colCount; } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/TableCellElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class TableCellElement : DocumentElement { private readonly Lazy _control; private readonly EnumerableEx _items; private SelectionList? _prevSelection; internal int Row { get; set; } internal int Column { get; set; } public int RowSpan { set; get; } public int ColSpan { set; get; } public TextAlignment? Horizontal { set; get; } public VerticalAlignment? Vertical { set; get; } public override Control Control => _control.Value; public override IEnumerable Children => _items; public TableCellElement(DocumentElement cell) { _items = new[] { cell }.ToEnumerable(); _control = new Lazy(CreateCell); } public TableCellElement(IEnumerable cells) { _items = cells.ToEnumerable(); _control = new Lazy(CreateCell); } public override void Select(Point from, Point to) { var selection = SelectionUtil.SelectVertical(Control, _items, from, to); if (_prevSelection is not null) { foreach (var ps in _prevSelection) { if (!selection.Any(cs => ReferenceEquals(cs, ps))) { ps.UnSelect(); } } } _prevSelection = selection; } public override void UnSelect() { foreach (var child in _items) child.UnSelect(); } private Border CreateCell() { if (_items.Count == 1) { return new Border() { Child = Setup(_items[0].Control) }; } else { var pnl = new StackPanel() { Orientation = Orientation.Vertical }; foreach (var cnt in _items) pnl.Children.Add(Setup(cnt.Control)); return new Border() { Child = pnl }; } } private Control Setup(Control control) { if (Horizontal.HasValue) { control.SetCurrentValue(TextBlock.TextAlignmentProperty, Horizontal.Value); switch (Horizontal.Value) { case TextAlignment.Left: control.HorizontalAlignment = HorizontalAlignment.Left; break; case TextAlignment.Right: control.HorizontalAlignment = HorizontalAlignment.Right; break; case TextAlignment.Center: control.HorizontalAlignment = HorizontalAlignment.Center; break; } } return control; } public override void ConstructSelectedText(StringBuilder builder) { if (_prevSelection is null) return; var preLen = builder.Length; foreach (var para in _prevSelection) { para.ConstructSelectedText(builder); if (preLen == builder.Length) continue; if (builder[builder.Length - 1] != '\n') builder.Append('\n'); } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/TextBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Media; using System; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class TextBlockElement : DocumentElement { private Lazy _text; public string? Text => _text.Value.Text; public override Control Control => _text.Value; public override IEnumerable Children => Array.Empty(); public TextBlockElement(IEnumerable inlines) { _text = new Lazy(() => { var text = new TextBlock(); if (text.Inlines is null) { text.Inlines = new InlineCollection(); } text.Inlines.AddRange(inlines); return text; }); } public TextBlockElement(IEnumerable inlines, string appendClass) { _text = new Lazy(() => { var text = new TextBlock(); if (text.Inlines is null) { text.Inlines = new InlineCollection(); } text.Inlines.AddRange(inlines); text.Classes.Add(appendClass); return text; }); } public TextBlockElement(IEnumerable inlines, string appendClass, TextAlignment alignment) { _text = new Lazy(() => { var text = new TextBlock(); if (text.Inlines is null) { text.Inlines = new InlineCollection(); } text.Inlines.AddRange(inlines); text.TextAlignment = alignment; text.Classes.Add(appendClass); return text; }); } public override void Select(Point from, Point to) { } public override void UnSelect() { } public override void ConstructSelectedText(StringBuilder stringBuilder) { } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/TextMarkerStyle.cs ================================================ using Markdown.Avalonia; using System; namespace ColorDocument.Avalonia.DocumentElements { public enum TextMarkerStyle { Box, Circle, Decimal, Disc, LowerLatin, LowerRoman, UpperLatin, UpperRoman, Square, } public static class MarkdownStyleExt { public static string CreateMakerText(this TextMarkerStyle textMarker, int index) { switch (textMarker) { default: throw new InvalidOperationException("sorry library manager forget to modify about listmerker."); case TextMarkerStyle.Disc: return "•"; case TextMarkerStyle.Box: return "▪"; case TextMarkerStyle.Circle: return "○"; case TextMarkerStyle.Square: return "❏"; case TextMarkerStyle.Decimal: return (index + 1).ToString() + "."; case TextMarkerStyle.LowerLatin: return NumberToOrder.ToLatin(index + 1).ToLower() + "."; case TextMarkerStyle.UpperLatin: return NumberToOrder.ToLatin(index + 1) + "."; case TextMarkerStyle.LowerRoman: return NumberToOrder.ToRoman(index + 1).ToLower() + "."; case TextMarkerStyle.UpperRoman: return NumberToOrder.ToRoman(index + 1) + "."; } } } } ================================================ FILE: ColorDocument.Avalonia/DocumentElements/UnBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using System; using System.Collections.Generic; using System.Text; namespace ColorDocument.Avalonia.DocumentElements { public class UnBlockElement : DocumentElement { private Control _control; public override Control Control => _control; public override IEnumerable Children => Array.Empty(); public UnBlockElement(Control control) { _control = control; } public override void Select(Point from, Point to) { } public override void UnSelect() { } public override void ConstructSelectedText(StringBuilder stringBuilder) { } } } ================================================ FILE: ColorDocument.Avalonia/EnumerableExt.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ColorDocument.Avalonia { internal static class EnumerableExt { public static EnumerableEx ToEnumerable(this IEnumerable enumerable) { if (enumerable is List list) return new EnumerableExLst(list); else if (enumerable is T[] array) return new EnumerableExAry(array); return new EnumerableExLzy(enumerable); } } internal abstract class EnumerableEx : IEnumerable { public abstract int Count { get; } public abstract T this[int idx] { get; } public abstract IEnumerator GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } internal class EnumerableExLzy : EnumerableEx { private Lazy _lzy; public EnumerableExLzy(IEnumerable enm) { _lzy = new Lazy(() => enm.ToArray()); } public override int Count => _lzy.Value.Length; public override T this[int idx] { get => _lzy.Value[idx]; } public override IEnumerator GetEnumerator() => ((ICollection)_lzy.Value).GetEnumerator(); } internal class EnumerableExAry : EnumerableEx { private T[] _array; public EnumerableExAry(T[] array) { _array = array; } public override int Count => _array.Length; public override T this[int idx] { get => _array[idx]; } public override IEnumerator GetEnumerator() => ((ICollection)_array).GetEnumerator(); } internal class EnumerableExLst : EnumerableEx { private IList _list; public EnumerableExLst(IList array) { _list = array; } public override int Count => _list.Count; public override T this[int idx] { get => _list[idx]; } public override IEnumerator GetEnumerator() => _list.GetEnumerator(); } } ================================================ FILE: ColorDocument.Avalonia/NumberToOrder.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia { static class NumberToOrder { public static string ToRoman(int number) { // roman can treat between 1 and 3999 if (number < 0 || number >= 4000) return number.ToString(); if (number >= 1000) return "M" + ToRoman(number - 1000); if (number >= 900) return "CM" + ToRoman(number - 900); if (number >= 500) return "D" + ToRoman(number - 500); if (number >= 400) return "CD" + ToRoman(number - 400); if (number >= 100) return "C" + ToRoman(number - 100); if (number >= 90) return "XC" + ToRoman(number - 90); if (number >= 50) return "L" + ToRoman(number - 50); if (number >= 40) return "XL" + ToRoman(number - 40); if (number >= 10) return "X" + ToRoman(number - 10); if (number >= 9) return "IX" + ToRoman(number - 9); if (number >= 5) return "V" + ToRoman(number - 5); if (number >= 4) return "IV" + ToRoman(number - 4); if (number >= 1) return "I" + ToRoman(number - 1); if (number == 0) return ""; throw new ArgumentOutOfRangeException("something bad happened"); } public static string ToLatin(int number) { var buff = new StringBuilder(); while (number > 0) { var mod = (number - 1) % 26; buff.Insert(0, (char)(mod + 'A')); number = (number - mod) / 26; } return buff.ToString(); } } } ================================================ FILE: ColorDocument.Avalonia/RegionUtil.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; namespace ColorDocument.Avalonia { internal static class RegionUtil { public static Rect? GetRectInDoc(this Control control, Layoutable anchor) { if (!LayoutInformation.GetPreviousArrangeBounds(control).HasValue) return null; double driftX = 0; double driftY = 0; StyledElement? c; for (c = control.Parent; c is not null && c is Layoutable layoutable && !ReferenceEquals(anchor, layoutable); c = c.Parent) { driftX += layoutable.Bounds.X; driftY += layoutable.Bounds.Y; } return new Rect( control.Bounds.X + driftX, control.Bounds.Y + driftY, control.Bounds.Width, control.Bounds.Height); } public static EnumerableEx GetRectInDoc(this EnumerableEx controls, Layoutable anchor) where T : DocumentElement { var rs = new DocumentElementWithBound[controls.Count]; for (var i = 0; i < rs.Length; ++i) { var doc = controls[i]; var rect = doc.Control.GetRectInDoc(anchor); if (rect.HasValue) { rs[i] = new DocumentElementWithBound(doc, rect.Value); } } return new EnumerableExAry(rs); } } internal struct DocumentElementWithBound { public DocumentElement Element { get; } public Rect Rect { get; } public DocumentElementWithBound(DocumentElement c, Rect r) { Element = c; Rect = r; } } } ================================================ FILE: ColorDocument.Avalonia/SelectionList.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ColorDocument.Avalonia { public class SelectionList : IList { private SelectDirection _direction; private SelectRange _range; private IList _elements; public SelectionList(SelectDirection direction, SelectRange range, IList elements) { _direction = direction; _range = range; _elements = elements; } public SelectDirection Direction => _direction; public DocumentElement this[int index] { get => _elements[index]; set => throw new InvalidOperationException(); } public int Count => _elements.Count; public bool IsReadOnly => true; public void Add(DocumentElement item) => throw new InvalidOperationException(); public void Clear() => throw new InvalidOperationException(); public bool Contains(DocumentElement item) => _elements.Contains(item); public void CopyTo(DocumentElement[] array, int arrayIndex) => _elements.CopyTo(array, arrayIndex); public IEnumerator GetEnumerator() => _elements.GetEnumerator(); public int IndexOf(DocumentElement item) => _elements.IndexOf(item); public void Insert(int index, DocumentElement item) => throw new InvalidOperationException(); public bool Remove(DocumentElement item) => throw new InvalidOperationException(); public void RemoveAt(int index) => throw new InvalidOperationException(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } public enum SelectDirection { Forward, Backward } public enum SelectRange { Part = 0b0001, Begin = 0b0011, End = 0b0101, Fill = 0b0111, } } ================================================ FILE: ColorDocument.Avalonia/SelectionUtil.cs ================================================ using Avalonia; using Avalonia.Layout; using System; using System.Collections.Generic; using System.Linq; namespace ColorDocument.Avalonia { internal static class SelectionUtil { public static SelectionList SelectVertical(Layoutable anchor, EnumerableEx elements, Point from, Point to) where T : DocumentElement { var c = elements.GetRectInDoc(anchor); int fp = ComputeIdxVertical(c, from); int tp = ComputeIdxVertical(c, to); var list = Select(c, from, to, fp, tp); SelectRange rng = (Math.Min(fp, tp) == 0 ? SelectRange.Begin : SelectRange.Part) | (Math.Max(fp, tp) == elements.Count - 1 ? SelectRange.End : SelectRange.Part); if (fp < tp) { return new SelectionList(SelectDirection.Forward, rng, list); } else { list.Reverse(); return new SelectionList(SelectDirection.Backward, rng, list); } } public static SelectionList SelectGrid(Layoutable anchor, EnumerableEx elements, Point from, Point to) where T : DocumentElement { var c = elements.GetRectInDoc(anchor); int fp = ComputeIdxGrid(c, from); int tp = ComputeIdxGrid(c, to); var list = Select(c, from, to, fp, tp); SelectRange rng = (Math.Min(fp, tp) == 0 ? SelectRange.Begin : SelectRange.Part) | (Math.Max(fp, tp) == elements.Count - 1 ? SelectRange.End : SelectRange.Part); if (fp < tp) { return new SelectionList(SelectDirection.Forward, rng, list); } else { list.Reverse(); return new SelectionList(SelectDirection.Backward, rng, list); } } static int ComputeIdxVertical(EnumerableEx elements, Point pnt) { if (pnt.X <= 0 && pnt.Y <= 0) return 0; if (pnt.X == Double.PositiveInfinity && pnt.Y == Double.PositiveInfinity) return elements.Count - 1; foreach ((var c, int i) in elements.Select((value, index) => (value, index))) { var bounds = c.Rect; if (pnt.Y < bounds.Bottom) return i; } return elements.Count - 1; } static int ComputeIdxGrid(EnumerableEx elements, Point pnt) { if (pnt.X <= 0 && pnt.Y <= 0) return 0; if (pnt.X == Double.PositiveInfinity && pnt.Y == Double.PositiveInfinity) return elements.Count - 1; double prevLeft = double.NegativeInfinity; int verticalLastHit = -1; foreach ((var c, int i) in elements.Select((value, index) => (value, index))) { var bounds = c.Rect; if (pnt.Y < bounds.Bottom) { if (pnt.X < bounds.Right) return i; if (bounds.Left < prevLeft) return verticalLastHit; prevLeft = bounds.Left; verticalLastHit = i; } } return elements.Count - 1; } private static List Select(EnumerableEx elements, Point from, Point to, int fp, int tp) { var list = new List(); if (fp < tp) { var workF = from; var workT = new Point(Double.PositiveInfinity, Double.PositiveInfinity); for (var i = fp; i <= tp; ++i) { if (i == tp) { workT = to; } var element = elements[i].Element; var rect = elements[i].Rect; element.Select( new Point(workF.X - rect.X, workF.Y - rect.Y), new Point(workT.X - rect.X, workT.Y - rect.Y)); list.Add(element); workF = new Point(0, 0); } } else if (tp < fp) { var workF = from; var workT = new Point(0, 0); for (var i = fp; i >= tp; --i) { if (i == tp) { workT = to; } var element = elements[i].Element; var rect = elements[i].Rect; element.Select( new Point(workF.X - rect.X, workF.Y - rect.Y), new Point(workT.X - rect.X, workT.Y - rect.Y)); list.Add(element); workF = new Point(Double.PositiveInfinity, Double.PositiveInfinity); } } else { var element = elements[tp].Element; var rect = elements[tp].Rect; element.Select( new Point(from.X - rect.X, from.Y - rect.Y), new Point(to.X - rect.X, to.Y - rect.Y)); list.Add(element); } return list; } } } ================================================ FILE: ColorTextBlock.Avalonia/CBold.cs ================================================ using System.Collections.Generic; using Weight = Avalonia.Media.FontWeight; namespace ColorTextBlock.Avalonia { /// /// Bold decoration /// public class CBold : CSpan { public CBold() { } public CBold(IEnumerable inlines) : base(inlines) { FontWeight = Weight.Bold; } } } ================================================ FILE: ColorTextBlock.Avalonia/CCode.cs ================================================ using System.Collections.Generic; using Avalonia.Media; using Avalonia; using ColorTextBlock.Avalonia.Fonts; namespace ColorTextBlock.Avalonia { /// /// Monospace decoration /// public class CCode : CSpan { /// /// Monospace font family used for code display. /// /// public static readonly StyledProperty MonospaceFontFamilyProperty = AvaloniaProperty.Register( nameof(MonospaceFontFamily), defaultValue: FontFamilyCollector.TryGetMonospace() ?? FontFamily.Default, inherits: true); public CCode() { var obsvr = this.GetBindingObservable(MonospaceFontFamilyProperty); Bind(FontFamilyProperty, obsvr); } public CCode(IEnumerable inlines) : base(inlines) { var obsvr = this.GetBindingObservable(MonospaceFontFamilyProperty); Bind(FontFamilyProperty, obsvr); } /// /// Monospace font family used for code display. /// public FontFamily MonospaceFontFamily { get { return GetValue(MonospaceFontFamilyProperty); } set { SetValue(MonospaceFontFamilyProperty, value); } } } } ================================================ FILE: ColorTextBlock.Avalonia/CHyperlink.cs ================================================ using Avalonia; using Avalonia.Input; using Avalonia.Media; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Linq; namespace ColorTextBlock.Avalonia { /// /// Hyperlink decoration /// public class CHyperlink : CSpan { /// /// Background brush during mouse hover /// /// public static readonly StyledProperty HoverBackgroundProperty = AvaloniaProperty.Register(nameof(Foreground)); /// /// Foreground brush during mouse hover /// /// public static readonly StyledProperty HoverForegroundProperty = AvaloniaProperty.Register(nameof(Foreground)); /// /// Background brush during mouse hover /// public IBrush? HoverBackground { get { return GetValue(HoverBackgroundProperty); } set { SetValue(HoverBackgroundProperty, value); } } /// /// Foreground brush during mouse hover /// public IBrush? HoverForeground { get { return GetValue(HoverForegroundProperty); } set { SetValue(HoverForegroundProperty, value); } } /// /// Link click action /// public Action? Command { get; set; } /// /// Link click action parameter /// public string? CommandParameter { get; set; } public CHyperlink() { } public CHyperlink(IEnumerable inlines) : base(inlines) { } protected override IEnumerable MeasureOverride( double entireWidth, double remainWidth) { var metrics = base.MeasureOverride( entireWidth, remainWidth); foreach (CGeometry metry in metrics) { metry.OnClick = ctrl => Command?.Invoke(CommandParameter ?? string.Empty); metry.OnMousePressed = ctrl => { PseudoClasses.Add(":pressed"); }; metry.OnMouseReleased = ctrl => { PseudoClasses.Remove(":pressed"); }; metry.OnMouseEnter = ctrl => { PseudoClasses.Add(":pointerover"); PseudoClasses.Add(":hover"); try { ctrl.Cursor = new Cursor(StandardCursorType.Hand); } catch { /*I cannot assume Cursor.ctor doesn't throw an exception.*/ } IEnumerable tmetries = (metry is DecoratorGeometry d) ? d.Targets.OfType() : (metry is TextGeometry t) ? new[] { t } : new TextGeometry[0]; if (tmetries != null) { foreach (var tmetry in tmetries) { tmetry.TemporaryForeground = HoverForeground; tmetry.TemporaryBackground = HoverBackground; } RequestRender(); } }; metry.OnMouseLeave = ctrl => { PseudoClasses.Remove(":pointerover"); PseudoClasses.Remove(":hover"); ctrl.Cursor = Cursor.Default; IEnumerable tmetries = (metry is DecoratorGeometry d) ? d.Targets.OfType() : (metry is TextGeometry t) ? new[] { t } : new TextGeometry[0]; if (tmetries != null) { foreach (var tmetry in tmetries) { tmetry.TemporaryForeground = null; tmetry.TemporaryBackground = null; } RequestRender(); } }; yield return metry; } } } } ================================================ FILE: ColorTextBlock.Avalonia/CImage.cs ================================================ using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Avalonia.Media; namespace ColorTextBlock.Avalonia { /// /// Displays an image /// public class CImage : CInline { public static readonly StyledProperty LayoutWidthProperty = AvaloniaProperty.Register(nameof(LayoutWidth)); public static readonly StyledProperty LayoutHeightProperty = AvaloniaProperty.Register(nameof(LayoutHeight)); public static readonly StyledProperty RelativeWidthProperty = AvaloniaProperty.Register(nameof(RelativeWidth)); /// /// Determine wheither image auto fitting or protrude outside Control /// when image is too width to be rendered in control. /// If you set 'true', Image is fitted to control width. /// public static readonly StyledProperty FittingWhenProtrudeProperty = AvaloniaProperty.Register(nameof(FittingWhenProtrude), defaultValue: true); /// /// Save aspect ratio if one of or set. /// public static readonly StyledProperty SaveAspectRatioProperty = AvaloniaProperty.Register(nameof(SaveAspectRatio)); public double? LayoutWidth { get { return GetValue(LayoutWidthProperty); } set { SetValue(LayoutWidthProperty, value); } } public double? LayoutHeight { get { return GetValue(LayoutHeightProperty); } set { SetValue(LayoutHeightProperty, value); } } public double? RelativeWidth { get { return GetValue(RelativeWidthProperty); } set { SetValue(RelativeWidthProperty, value); } } public bool FittingWhenProtrude { get { return GetValue(FittingWhenProtrudeProperty); } set { SetValue(FittingWhenProtrudeProperty, value); } } public bool SaveAspectRatio { get => GetValue(SaveAspectRatioProperty); set => SetValue(SaveAspectRatioProperty, value); } public Task? Task { get; } private IImage WhenError { get; } public IImage? Image { private set; get; } public CImage(Task task, IImage whenError) { if (task is null) throw new NullReferenceException(nameof(task)); if (whenError is null) throw new NullReferenceException(nameof(whenError)); this.Task = task; this.WhenError = whenError; } public CImage(IImage image) { if (image is null) throw new NullReferenceException(nameof(image)); this.WhenError = this.Image = image; } protected override IEnumerable MeasureOverride( double entireWidth, double remainWidth) { if (Image is null) { if (Task is null) { Image = WhenError; } else if ( Task.Status == TaskStatus.RanToCompletion || Task.Status == TaskStatus.Faulted || Task.Status == TaskStatus.Canceled) { Image = Task.IsFaulted ? WhenError : Task.Result ?? WhenError; } else { Image = new WriteableBitmap( new PixelSize(1, 1), new Vector(96, 96), PixelFormat.Rgb565, AlphaFormat.Premul); Thread.MemoryBarrier(); System.Threading.Tasks.Task.Run(() => { Task.Wait(); Dispatcher.UIThread.InvokeAsync(() => { Image = Task.IsFaulted ? WhenError : Task.Result ?? WhenError; RequestMeasure(); }); }); } } double imageWidth = Image.Size.Width; double imageHeight = Image.Size.Height; if (RelativeWidth.HasValue) { var aspect = imageHeight / imageWidth; imageWidth = RelativeWidth.Value * entireWidth; imageHeight = aspect * imageWidth; } if (LayoutWidth.HasValue) { imageWidth = LayoutWidth.Value; if (SaveAspectRatio && !LayoutHeight.HasValue) { var aspect = Image.Size.Height / Image.Size.Width; imageHeight = aspect * imageWidth; } } if (LayoutHeight.HasValue) { imageHeight = LayoutHeight.Value; if (SaveAspectRatio && !LayoutWidth.HasValue) { var aspect = Image.Size.Width / Image.Size.Height; imageWidth = aspect * imageHeight; } } if (imageWidth > remainWidth) { if (entireWidth != remainWidth) { yield return new LineBreakMarkGeometry(this); } if (FittingWhenProtrude && imageWidth > entireWidth) { var aspect = imageHeight / imageWidth; imageWidth = entireWidth; imageHeight = aspect * imageWidth; } } yield return new ImageGeometry(this, Image, imageWidth, imageHeight, TextVerticalAlignment); } public override string AsString() => " $$Image$$ "; } } ================================================ FILE: ColorTextBlock.Avalonia/CInline.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; using ColorTextBlock.Avalonia.Geometries; using System.Collections.Generic; using System.ComponentModel; namespace ColorTextBlock.Avalonia { /// /// The base class for representing a text element. /// // テキスト要素を表現するための基底のクラス [TypeConverter(typeof(StringToRunConverter))] public abstract class CInline : StyledElement { /// /// The brush of background. /// /// public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), inherits: true); /// /// The brush of the text element. /// /// public static readonly StyledProperty ForegroundProperty = TextBlock.ForegroundProperty.AddOwner(); /// /// The font family of the text element /// /// public static readonly StyledProperty FontFamilyProperty = TextBlock.FontFamilyProperty.AddOwner(); /// /// The font weight of the text element /// /// public static readonly StyledProperty FontWeightProperty = TextBlock.FontWeightProperty.AddOwner(); /// /// The font stretch of the text element /// /// public static readonly StyledProperty FontStretchProperty = TextBlock.FontStretchProperty.AddOwner(); /// /// The font size of the text element /// /// public static readonly StyledProperty FontSizeProperty = TextBlock.FontSizeProperty.AddOwner(); /// /// The font style of the text element /// /// public static readonly StyledProperty FontStyleProperty = TextBlock.FontStyleProperty.AddOwner(); /// /// Use to indicate the vertical position of text within line. /// For example, it is used to align text to the top or to the bottom. /// /// // テキストを上揃えで描画するか下揃えで描画するか指定します。 public static readonly StyledProperty TextVerticalAlignmentProperty = CTextBlock.TextVerticalAlignmentProperty.AddOwner(); /// /// Indicates whether the text element is underlined. /// If this property value is true, the text element is underlined. /// /// public static readonly StyledProperty IsUnderlineProperty = AvaloniaProperty.Register(nameof(IsUnderline), inherits: true); /// /// Indicates whether the text element is strikethrough. /// If the value of this property is true, the text element is strikethrough. /// /// public static readonly StyledProperty IsStrikethroughProperty = AvaloniaProperty.Register(nameof(IsStrikethrough), inherits: true); /// /// The brush of background. /// public IBrush? Background { get { return GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } } /// /// The brush of the text element. /// public IBrush? Foreground { get { return GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } } /// /// The font family of the text element /// public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } /// /// The font size of the text element /// public double FontSize { get { return GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } /// /// The font stretch of the text element /// public FontStyle FontStyle { get { return GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } /// /// The font weight of the text element /// public FontWeight FontWeight { get { return GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } /// /// The font stretch of the text element /// public FontStretch FontStretch { get { return GetValue(FontStretchProperty); } set { SetValue(FontStretchProperty, value); } } /// /// Typeface of the text element /// public Typeface Typeface { get; private set; } /// /// Indicates whether the text element is underlined. /// If this property value is true, the text element is underlined. /// public bool IsUnderline { get { return GetValue(IsUnderlineProperty); } set { SetValue(IsUnderlineProperty, value); } } /// /// Indicates whether the text element is strikethrough. /// If the value of this property is true, the text element is strikethrough. /// public bool IsStrikethrough { get { return GetValue(IsStrikethroughProperty); } set { SetValue(IsStrikethroughProperty, value); } } /// /// Use to indicate the vertical position of text within line. /// For example, it is used to align text to the top or to the bottom. /// public TextVerticalAlignment TextVerticalAlignment { get { return GetValue(TextVerticalAlignmentProperty); } set { SetValue(TextVerticalAlignmentProperty, value); } } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) { case nameof(Background): case nameof(Foreground): case nameof(IsUnderline): case nameof(IsStrikethrough): RequestRender(); break; case nameof(FontFamily): case nameof(FontSize): case nameof(FontStyle): case nameof(FontWeight): case nameof(FontStretch): Typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); goto case nameof(TextVerticalAlignment); case nameof(TextVerticalAlignment): RequestMeasure(); break; } } protected void RequestMeasure() { if (Parent is CInline cline) { cline.RequestMeasure(); } else if (Parent is CTextBlock ctxt) { ctxt.OnMeasureSourceChanged(); } else if (Parent is Layoutable layout) { layout.InvalidateMeasure(); } } protected void RequestRender() { try { if (Parent is CInline cline) { cline.RequestRender(); } else if (Parent is Layoutable layout) { layout.InvalidateVisual(); } } catch { // An error occured sometimes with FluentAvalonia. } } internal IEnumerable Measure(double entireWidth, double remainWidth) { Typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); /* * This is Imitation of Layoutable.MeasureCore. * If parent style is changed, StyledElement.InvalidedStyles is called. * This method clear all applied styles, * so we should reapply style after style change. */ ApplyStyling(); return MeasureOverride(entireWidth, remainWidth); } protected abstract IEnumerable MeasureOverride( double entireWidth, double remainWidth); /// /// Returns the string that this instance displays. /// /// // この要素が表示する文字を返します。 public abstract string AsString(); } } ================================================ FILE: ColorTextBlock.Avalonia/CInlineUIContainer.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia.Geometies; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Text; namespace ColorTextBlock.Avalonia { /// /// Places a control as an inline element. /// public class CInlineUIContainer : CInline { /// /// A displayed control /// public Control? Content { get; set; } internal DummyGeometryForControl? Indicator { get; private set; } public CInlineUIContainer(Control content) { Content = content; } protected override IEnumerable MeasureOverride(double entireWidth, double remainWidth) { if (Content is null) { Indicator = null; return new CGeometry[0]; } Content.Measure(new Size(remainWidth, Double.PositiveInfinity)); if (Content.DesiredSize.Width > remainWidth) { Content.Measure(new Size(entireWidth, Double.PositiveInfinity)); Indicator = new DummyGeometryForControl(this, Content, TextVerticalAlignment); return new CGeometry[] { new LineBreakMarkGeometry(this), Indicator }; } else { Indicator = new DummyGeometryForControl(this, Content, TextVerticalAlignment); return new[] { Indicator }; } } /// public override string AsString() => String.Empty; } } ================================================ FILE: ColorTextBlock.Avalonia/CItalic.cs ================================================ using System.Collections.Generic; using FStyle = Avalonia.Media.FontStyle; namespace ColorTextBlock.Avalonia { /// /// Italic decoration /// public class CItalic : CSpan { public CItalic() { } public CItalic(IEnumerable inlines) : base(inlines) { FontStyle = FStyle.Italic; } } } ================================================ FILE: ColorTextBlock.Avalonia/CLineBreak.cs ================================================ using Avalonia; using Avalonia.Media; using ColorTextBlock.Avalonia.Geometries; using System.Collections.Generic; using System.Globalization; namespace ColorTextBlock.Avalonia { /// /// Expression of the linebreak. /// public class CLineBreak : CRun { public CLineBreak() { Text = "\n"; } protected override IEnumerable MeasureOverride( double entireWidth, double remainWidth) { var ftxt = new FormattedText( "Ty", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily, FontStyle, FontWeight), FontSize, Foreground); yield return new LineBreakMarkGeometry(this, ftxt.Height); } } } ================================================ FILE: ColorTextBlock.Avalonia/CRun.cs ================================================ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Metadata; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; namespace ColorTextBlock.Avalonia { /// /// Expression of a text /// public class CRun : CInline { /// /// THe content of the eleemnt /// /// public static readonly StyledProperty TextProperty = AvaloniaProperty.Register(nameof(Text)); /// /// THe content of the eleemnt /// [Content] public string Text { get { return GetValue(TextProperty); } set { SetValue(TextProperty, value); } } protected override IEnumerable MeasureOverride( double entireWidth, double remainWidth) { if (String.IsNullOrEmpty(Text)) { return Array.Empty(); } var runProps = CreateTextRunProperties(Foreground); var paraProps = CreateTextParagraphProperties(runProps); var source = new SimpleTextSource(Text.AsMemory(), runProps); if (remainWidth == entireWidth) { return CreateLines(source, entireWidth, paraProps); } var firstLine = TextFormatter.Current.FormatLine(source, 0, double.PositiveInfinity, paraProps); if (firstLine is null) { return Array.Empty(); } if (firstLine.Width < remainWidth) { if (firstLine.Length == Text.Length) { return new List() { new TextLineGeometry(this, source, firstLine, false) }; } return CreateLines(source, entireWidth, paraProps, firstLine); } else { var firstLineSource = source.Subsource(firstLine.FirstTextSourceIndex, firstLine.Length); var firstLineRemain = TextFormatter.Current.FormatLine(firstLineSource, 0, remainWidth, paraProps)!; var breakPosEnum = new LineBreakEnumerator(Text.AsMemory().Slice(firstLine.FirstTextSourceIndex, firstLine.Length).Span); int breakPos = breakPosEnum.MoveNext(out var lnbrk) ? lnbrk.PositionWrap : int.MaxValue; if (breakPos < firstLineRemain.Length) { // correct wrap return CreateLines(source, entireWidth, paraProps, firstLineRemain); } else { // wrong wrap; first line word is too long return CreateLines(source, entireWidth, paraProps, new LineBreakMarkGeometry(this)); } } } private IEnumerable CreateLines( SimpleTextSource source, double entireWidth, TextParagraphProperties paraProps, TextLine firstLine) { TextLine prev = firstLine; var length = firstLine.Length; while (length < Text.Length) { var line = TextFormatter.Current.FormatLine(source, length, entireWidth, paraProps, prev.TextLineBreak); if (line is null) break; yield return new TextLineGeometry(this, source, prev, true); prev = line; length += line.Length; } yield return new TextLineGeometry(this, source, prev, false); } private IEnumerable CreateLines( SimpleTextSource source, double entireWidth, TextParagraphProperties paraProps, CGeometry? prevGeo = null) { if (prevGeo is not null) yield return prevGeo; TextLine? prev = TextFormatter.Current.FormatLine(source, 0, entireWidth, paraProps); if (prev is null) yield break; var length = prev.Length; while (length < Text.Length) { var line = TextFormatter.Current.FormatLine(source, length, entireWidth, paraProps, prev.TextLineBreak); if (line is null) break; yield return new TextLineGeometry(this, source, prev, true); prev = line; length += line.Length; } yield return new TextLineGeometry(this, source, prev, false); } internal TextParagraphProperties CreateTextParagraphProperties(TextRunProperties runProps) => new GenericTextParagraphProperties( FlowDirection.LeftToRight, TextAlignment.Left, true, false, runProps, TextWrapping.Wrap, double.NaN, 0, 0); internal TextRunProperties CreateTextRunProperties(IBrush? foreground) => new GenericTextRunProperties(Typeface, FontSize, foregroundBrush: foreground); public override string AsString() => Text; } readonly struct SimpleTextSource : ITextSource { private readonly ReadOnlyMemory _text; private readonly TextRunProperties _props; public TextRunProperties RunProperties => _props; public SimpleTextSource(ReadOnlyMemory text, TextRunProperties props) { _text = text; _props = props; } public TextRun? GetTextRun(int textSourceIndex) { return new TextCharacters(_text.Slice(textSourceIndex), _props); } public SimpleTextSource Subsource(int start, int length) => new SimpleTextSource(_text.Slice(start, length), _props); public string Substring(int start, int length) => _text.Slice(start, length).ToString(); public string Substring(int start) => _text.Slice(start).ToString(); public SimpleTextSource ChangeForeground(IBrush? foreground) { var runProps = new GenericTextRunProperties(_props.Typeface, _props.FontRenderingEmSize, foregroundBrush: foreground); return new SimpleTextSource(_text, runProps); } public override string ToString() => _text.ToString(); } } ================================================ FILE: ColorTextBlock.Avalonia/CSpan.cs ================================================ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using Avalonia.Metadata; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Linq; namespace ColorTextBlock.Avalonia { /// /// Text decoration /// public class CSpan : CInline { /// /// The thickness of the border /// /// public static readonly StyledProperty BorderThicknessProperty = AvaloniaProperty.Register(nameof(BorderThickness)); /// /// The brush of the border. /// /// public static readonly StyledProperty BorderBrushProperty = AvaloniaProperty.Register(nameof(BorderBrush)); /// /// The radius of the border rounded corners /// /// public static readonly StyledProperty CornerRadiusProperty = AvaloniaProperty.Register(nameof(CornerRadius)); /// /// The box shadow effect parameters /// /// public static readonly StyledProperty BoxShadowProperty = AvaloniaProperty.Register(nameof(BoxShadow)); /// /// The padding to place around the Child control. /// /// public static readonly StyledProperty PaddingProperty = AvaloniaProperty.Register(nameof(Padding)); /// /// The margin around the element. /// /// public static readonly StyledProperty MarginProperty = InputElement.MarginProperty.AddOwner(); /// /// THe content of the eleemnt /// /// public static readonly StyledProperty> ContentProperty = AvaloniaProperty.Register>(nameof(Content)); static CSpan() { ContentProperty.Changed.AddClassHandler( (x, e) => { if (e.OldValue is IEnumerable oldlines) { foreach (var child in oldlines) x.LogicalChildren.Remove(child); } if (e.NewValue is IEnumerable newlines) { foreach (var child in newlines) x.LogicalChildren.Add(child); } }); } private Border? _border; /// /// The thickness of the border /// public Thickness BorderThickness { get => GetValue(BorderThicknessProperty); set => SetValue(BorderThicknessProperty, value); } /// /// The brush of the border. /// public IBrush BorderBrush { get => GetValue(BorderBrushProperty); set => SetValue(BorderBrushProperty, value); } /// /// The radius of the border rounded corners /// public CornerRadius CornerRadius { get => GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } /// /// The box shadow effect parameters /// public BoxShadows BoxShadow { get => GetValue(BoxShadowProperty); set => SetValue(BoxShadowProperty, value); } /// /// The padding to place around the Child control. /// public Thickness Padding { get => GetValue(PaddingProperty); set => SetValue(PaddingProperty, value); } /// /// The margin around the element. /// public Thickness Margin { get => GetValue(MarginProperty); set => SetValue(MarginProperty, value); } /// /// THe content of the eleemnt /// [Content] public IEnumerable Content { get { return GetValue(ContentProperty); } set { SetValue(ContentProperty, value); } } public CSpan() { var clst = new AvaloniaList(); // for xaml loader clst.CollectionChanged += (s, e) => { if (e.OldItems != null) foreach (var child in e.OldItems) LogicalChildren.Remove((CInline)child); if (e.NewItems != null) foreach (var child in e.NewItems) LogicalChildren.Add((CInline)child); }; Content = clst; } public CSpan(IEnumerable inlines) { Content = inlines.ToArray(); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) { case nameof(BorderThickness): case nameof(CornerRadius): case nameof(BoxShadow): case nameof(Padding): case nameof(Margin): OnBorderPropertyChanged(true); break; case nameof(BorderBrush): OnBorderPropertyChanged(false); break; } } private void OnBorderPropertyChanged(bool requestMeasure) { bool borderEnabled = BorderThickness != default || Padding != default || CornerRadius != default || Margin != default || !BoxShadow.Equals(default); if (borderEnabled) { if (_border is null) { _border = new Border(); LogicalChildren.Add(_border); } _border.BorderThickness = BorderThickness; _border.BorderBrush = BorderBrush; _border.CornerRadius = CornerRadius; _border.BoxShadow = BoxShadow; _border.Padding = Padding; _border.Margin = Margin; } else { if (_border is not null) LogicalChildren.Remove(_border); _border = null; } if (requestMeasure) RequestMeasure(); else RequestRender(); } protected override IEnumerable MeasureOverride( double entireWidth, double remainWidth) { if (_border is not null) { _border.Measure(Size.Infinity); entireWidth -= _border.DesiredSize.Width; remainWidth -= _border.DesiredSize.Width; return PrivateMeasure(_border, entireWidth, remainWidth); } else { return PrivateMeasure(entireWidth, remainWidth); } } private IEnumerable PrivateMeasure( Border border, double entireWidth, double remainWidth) { var buffer = new List(); foreach (var adding in PrivateMeasure(entireWidth, remainWidth)) { // save linebreak before span if (adding is LineBreakMarkGeometry && buffer.Count == 0) { yield return adding; continue; } buffer.Add(adding); if (adding.LineBreak) { yield return DecoratorGeometry.New(this, buffer, border); buffer.Clear(); } } if (buffer.Count != 0) { yield return DecoratorGeometry.New(this, buffer, border); } } private IEnumerable PrivateMeasure( double entireWidth, double remainWidth) { foreach (CInline inline in Content) { IEnumerable addings = inline.Measure(entireWidth, remainWidth); foreach (var add in addings) { yield return add; if (add.LineBreak) remainWidth = entireWidth; else remainWidth -= add.Width; } } } public override string AsString() => String.Join("", Content.Select(c => c.AsString())); } } ================================================ FILE: ColorTextBlock.Avalonia/CStrikethrough.cs ================================================ using System.Collections.Generic; namespace ColorTextBlock.Avalonia { /// /// Strikethrough decoration /// public class CStrikethrough : CSpan { public CStrikethrough() { } public CStrikethrough(IEnumerable inlines) : base(inlines) { IsStrikethrough = true; } } } ================================================ FILE: ColorTextBlock.Avalonia/CTextBlock.cs ================================================  using Avalonia; using Avalonia.Automation.Peers; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Rendering.Composition; using Avalonia.Utilities; using Avalonia.VisualTree; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace ColorTextBlock.Avalonia { /// /// TextBlock to enables character-by-character decoration. /// // 文字ごとの装飾を可能とするTextBlock public class CTextBlock : Control, ITextPointerHandleable { /// /// Use for adjusting vertical position between CTextBlocks. e.g. between a list marker and a list item. /// // リストマーカーと項目の縦位置の調整といった、CTextBlock間で文字の位置調整に使用します。 private static readonly StyledProperty BaseHeightProperty = AvaloniaProperty.Register("BaseHeight"); /// /// Use to indicate the height of each lines. If this value is NaN, the height is calculated by content. /// /// // 一行の高さ指定の為に使用します。指定がない(NaN)の場合、コンテンツによって行の高さが決まります。 public static readonly StyledProperty LineHeightProperty = AvaloniaProperty.Register(nameof(LineHeight), defaultValue: Double.NaN); /// /// Line to line spacing. /// /// // 行間の幅 public static readonly StyledProperty LineSpacingProperty = AvaloniaProperty.Register(nameof(LineSpacing), defaultValue: 0); /// /// The brush of background. /// /// public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); /// /// The brush of characters. /// /// public static readonly StyledProperty ForegroundProperty = TextBlock.ForegroundProperty.AddOwner(); /// /// The font family of characters /// /// public static readonly StyledProperty FontFamilyProperty = TextBlock.FontFamilyProperty.AddOwner(); /// /// The font weight of characters /// /// public static readonly StyledProperty FontWeightProperty = TextBlock.FontWeightProperty.AddOwner(); /// /// The font size of characters /// /// public static readonly StyledProperty FontSizeProperty = TextBlock.FontSizeProperty.AddOwner(); /// /// The font style of characters /// /// public static readonly StyledProperty FontStyleProperty = TextBlock.FontStyleProperty.AddOwner(); /// /// Use to indicate the vertical position of text within line. /// For example, it is used to align text to the top or to the bottom. /// /// // テキストを上揃えで描画するか下揃えで描画するか指定します。 public static readonly StyledProperty TextVerticalAlignmentProperty = AvaloniaProperty.Register( nameof(TextVerticalAlignment), defaultValue: TextVerticalAlignment.Base, inherits: true); /// /// Use to indicate the mode of text wrapping. /// /// public static readonly StyledProperty TextWrappingProperty = AvaloniaProperty.Register(nameof(TextWrapping), defaultValue: TextWrapping.Wrap); /// /// Contents to be displayed. /// /// public static readonly DirectProperty> ContentProperty = AvaloniaProperty.RegisterDirect>( nameof(Content), o => o.Content, (o, v) => o.Content = v); public static readonly StyledProperty SelectionBrushProperty = SelectableTextBlock.SelectionBrushProperty.AddOwner(); /// /// Horizontal text alignment. /// /// public static readonly StyledProperty TextAlignmentProperty = AvaloniaProperty.Register( nameof(TextAlignment), defaultValue: TextAlignment.Left); static CTextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); AffectsRender( BackgroundProperty, TextBlock.ForegroundProperty, TextBlock.FontWeightProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty); } private double _computedBaseHeight; private AvaloniaList _content; private Size _constraint; private Size _measured; private readonly List _metries; private readonly List _lines; private readonly List _containers; private CGeometry? _entered; private CGeometry? _pressed; private string? _text; private bool _measureRequested; private TextPointer? _beginSelect; private List _intermediates = new(); private TextPointer? _endSelect; public Selection? Selection => _beginSelect is not null && _endSelect is not null ? new Selection(_beginSelect.Index, _endSelect.Index) : null; /// /// The brush of background. /// public IBrush? Background { get { return GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } } /// /// The brush of characters. /// public IBrush? Foreground { get { return GetValue(ForegroundProperty); } set { SetValue(ForegroundProperty, value); } } /// /// The font family of characters /// public FontFamily FontFamily { get { return GetValue(FontFamilyProperty); } set { SetValue(FontFamilyProperty, value); } } /// /// The font size of characters /// public double FontSize { get { return GetValue(FontSizeProperty); } set { SetValue(FontSizeProperty, value); } } /// /// The font style of characters /// public FontStyle FontStyle { get { return GetValue(FontStyleProperty); } set { SetValue(FontStyleProperty, value); } } /// /// The font weight of characters /// public FontWeight FontWeight { get { return GetValue(FontWeightProperty); } set { SetValue(FontWeightProperty, value); } } /// /// Use to indicate the mode of text wrapping. /// public TextWrapping TextWrapping { get { return GetValue(TextWrappingProperty); } set { SetValue(TextWrappingProperty, value); } } /// /// Horizontal text alignment. /// public TextAlignment TextAlignment { get { return GetValue(TextAlignmentProperty); } set { SetValue(TextAlignmentProperty, value); } } /// /// Use to indicate the vertical position of text within line. /// For example, it is used to align text to the top or to the bottom. /// public TextVerticalAlignment TextVerticalAlignment { get { return GetValue(TextVerticalAlignmentProperty); } set { SetValue(TextVerticalAlignmentProperty, value); } } /// /// Use to indicate the height of each lines. If this value is NaN, the height is calculated by content. /// public double LineHeight { get { return GetValue(LineHeightProperty); } set { SetValue(LineHeightProperty, value); } } /// /// Line to line spacing. /// public double LineSpacing { get { return GetValue(LineSpacingProperty); } set { SetValue(LineSpacingProperty, value); } } /// /// Contents to be displayed. /// [Content] public AvaloniaList Content { get => _content; set { var olds = _content; if (SetAndRaise(ContentProperty, ref _content, value)) { olds.CollectionChanged -= ContentCollectionChangedd; DetachChildren(olds); AttachChildren(_content); _content.CollectionChanged += ContentCollectionChangedd; } } } /// /// Textual presentation of content. /// public string Text { get => _text ??= String.Join("", Content.Select(c => c.AsString())); } public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } public CTextBlock() { _content = new AvaloniaList(); _content.CollectionChanged += ContentCollectionChangedd; _metries = new List(); _lines = new List(); _containers = new List(); RenderOptions.SetBitmapInterpolationMode(this, BitmapInterpolationMode.HighQuality); } public CTextBlock(string text) : this() { _content.Add(new CRun() { Text = text }); } public CTextBlock(params CInline[] inlines) : this((IEnumerable)inlines) { } public CTextBlock(IEnumerable inlines) : this() { _content.AddRange(inlines); } #region pointer event protected override void OnPointerExited(PointerEventArgs e) { base.OnPointerExited(e); if (_entered is not null) { _entered.OnMouseLeave?.Invoke(this); _entered = null; } } protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); Point point = e.GetPosition(this); if (_entered is not null) { var relX = point.X - _entered.Left; var relY = point.Y - _entered.Top; if (!isEntered(_entered)) { _entered.OnMouseLeave?.Invoke(this); _entered = null; } else return; } foreach (CGeometry metry in _metries) { if (isEntered(metry)) { metry.OnMouseEnter?.Invoke(this); _entered = metry; break; } } bool isEntered(CGeometry metry) { var relX = point.X - metry.Left; var relY = point.Y - metry.Top; return 0 <= relX && relX <= metry.Width && 0 <= relY && relY <= metry.Height; } } protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { Point point = e.GetPosition(this); foreach (CGeometry metry in _metries) { if ((metry.OnMousePressed is not null || metry.OnMouseReleased is not null) && isEntered(metry)) { metry.OnMousePressed?.Invoke(this); _pressed = metry; e.Handled = true; return; } } bool isEntered(CGeometry metry) { var relX = point.X - metry.Left; var relY = point.Y - metry.Top; return 0 <= relX && relX <= metry.Width && 0 <= relY && relY <= metry.Height; } } } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); if (_pressed is not null && e.InitialPressMouseButton == MouseButton.Left) { e.Handled = true; _pressed.OnMouseReleased?.Invoke(this); Point point = e.GetPosition(this); var relX = point.X - _pressed.Left; var relY = point.Y - _pressed.Top; if (0 <= relX && relX <= _pressed.Width && 0 <= relY && relY <= _pressed.Height) { _pressed.OnClick?.Invoke(this); } _pressed = null; } } #endregion protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); switch (change.Property.Name) { case nameof(Content): case nameof(TextBlock.FontSize): case nameof(TextBlock.FontStyle): case nameof(TextBlock.FontWeight): case nameof(TextWrapping): case nameof(Bounds): case nameof(TextVerticalAlignment): case nameof(LineHeight): case nameof(LineSpacing): OnMeasureSourceChanged(); break; case nameof(BaseHeightProperty): if (_computedBaseHeight != GetValue(BaseHeightProperty)) { _measureRequested = true; InvalidateMeasure(); InvalidateArrange(); } break; } } public void ObserveBaseHeightOf(CTextBlock target) { if (target is not null) this.Bind(BaseHeightProperty, target.GetBindingObservable(BaseHeightProperty)); } private void ContentCollectionChangedd(object? sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: if (e.OldItems is not null) DetachChildren(e.OldItems.Cast()); break; case NotifyCollectionChangedAction.Replace: if (e.OldItems is not null) DetachChildren(e.OldItems.Cast()); if (e.NewItems is not null) AttachChildren(e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Add: if (e.NewItems is not null) AttachChildren(e.NewItems.Cast()); break; } } /// /// Add CInline to LogicalChildren to inherit the value of AvaloniaProperty. /// And add Control, which is haved by CInlineUIContainer, to VisualChildren. /// private void AttachChildren(IEnumerable newItems) { foreach (CInline item in newItems) { LogicalChildren.Add(item); AttachForVisual(item); } void AttachForVisual(CInline item) { if (item is CInlineUIContainer container) { var content = container.Content; var visparent = container.Content.GetVisualParent(); if (visparent is CTextBlock cblock) { cblock.VisualChildren.Remove(content); cblock.LogicalChildren.Remove(content); } else if (visparent is object) { Debug.Print("Control has another parent"); return; } VisualChildren.Add(container.Content); LogicalChildren.Add(container.Content); _containers.Add(container); } else if (item is CSpan span) foreach (var child in span.Content) AttachForVisual(child); } } /// /// Remove CInline to LogicalChildren to inherit the value of AvaloniaProperty. /// And remove Control, which is haved by CInlineUIContainer, to VisualChildren. /// private void DetachChildren(IEnumerable removeItems) { foreach (CInline item in removeItems) { LogicalChildren.Remove(item); DetachForVisual(item); } void DetachForVisual(CInline item) { if (item is CInlineUIContainer container) { VisualChildren.Remove(container.Content); LogicalChildren.Remove(container.Content); _containers.Remove(container); } else if (item is CSpan span) foreach (var child in span.Content) DetachForVisual(child); } } internal void OnMeasureSourceChanged() { SetValue(BaseHeightProperty, default); _measureRequested = true; InvalidateMeasure(); InvalidateArrange(); } private void RepaintRequested() { InvalidateVisual(); } /// /// Check to see if the arrangement size is different from the size of measuring. /// // 配置領域が寸法計算時に与えられた領域より広すぎるもしくは狭すぎないか確認します。 protected override Size ArrangeOverride(Size finalSize) { if (_measured.Width > finalSize.Width) { finalSize = finalSize.WithWidth(Math.Ceiling(_measured.Width)); } foreach (var container in _containers) { var indicator = container.Indicator; if (indicator is null) continue; indicator.Control.Arrange(new Rect(indicator.Left, indicator.Top, indicator.Width, indicator.Height)); } if (AreClose(_constraint.Width, finalSize.Width)) { return finalSize; } _constraint = new Size(finalSize.Width, Double.PositiveInfinity); _measured = UpdateGeometry(); return finalSize; } protected override Size MeasureOverride(Size availableSize) { if (_measured.Width == 0d || !AreClose(availableSize.Width, _constraint.Width) || _measureRequested) { _measureRequested = false; _constraint = availableSize; _measured = UpdateGeometry(); } InvalidateArrange(); return _measured; } private static bool AreClose(double v1, double v2) { if (v1 == v2) return true; double eps = (Math.Abs(v1) + Math.Abs(v2) + 10.0) * 2.2204460492503131e-016; double diff = Math.Abs(v1 - v2); return diff < eps; } private Size UpdateGeometry() { _metries.Clear(); _lines.Clear(); double entireWidth = _constraint.Width; if (Double.IsInfinity(_constraint.Width) && Bounds.Width != 0) entireWidth = Bounds.Width; double width = 0; double height = 0; // measure & split by linebreak var reqHeight = GetValue(BaseHeightProperty); var entireLineHeight = LineHeight; { LineInfo? now = null; double remainWidth = entireWidth; foreach (CInline inline in Content) { IEnumerable inlineGeometry = inline.Measure( (TextWrapping == TextWrapping.NoWrap) ? Double.PositiveInfinity : entireWidth, (TextWrapping == TextWrapping.NoWrap) ? Double.PositiveInfinity : remainWidth); foreach (CGeometry metry in inlineGeometry) { if (now is null) { _lines.Add(now = new LineInfo()); if (_lines.Count == 1) now.RequestBaseHeight = reqHeight; } if (now.Add(metry)) { if (!Double.IsNaN(entireLineHeight)) now.OverwriteHeight(entireLineHeight); width = Math.Max(width, now.Width); height += now.Height; now = null; remainWidth = entireWidth; } else remainWidth -= metry.Width; } } if (now is not null) { if (!Double.IsNaN(entireLineHeight)) now.OverwriteHeight(entireLineHeight); width = Math.Max(width, now.Width); height += now.Height; } } if (_lines.Count > 0) { _computedBaseHeight = _lines[0].BaseHeight; SetValue(BaseHeightProperty, _lines[0].BaseHeight); } var lineSpc = LineSpacing; height += lineSpc * (_lines.Count - 1); // set position { var topOffset = 0d; var leftOffset = 0d; foreach (LineInfo lineInf in _lines) { lineInf.Top = topOffset; switch (TextAlignment) { case TextAlignment.Left: leftOffset = 0d; break; case TextAlignment.Center: leftOffset = (entireWidth - lineInf.Width) / 2; break; case TextAlignment.Right: leftOffset = entireWidth - lineInf.Width; break; } foreach (CGeometry metry in lineInf.Metries) { metry.Left = leftOffset; switch (metry.TextVerticalAlignment) { case TextVerticalAlignment.Top: metry.Top = topOffset; break; case TextVerticalAlignment.Center: metry.Top = topOffset + (lineInf.Height - metry.Height) / 2; break; case TextVerticalAlignment.Bottom: metry.Top = topOffset + lineInf.Height - metry.Height; break; case TextVerticalAlignment.Base: metry.Top = topOffset + lineInf.BaseHeight - metry.BaseHeight; break; } leftOffset += metry.Width; _metries.Add(metry); metry.Arranged(); } topOffset += lineInf.Height + lineSpc; } } foreach (CGeometry metry in _metries) metry.RepaintRequested += RepaintRequested; if (_beginSelect is not null && _endSelect is not null) { Select(_beginSelect.Index, _endSelect.Index); } return new Size(width, height); } public override void Render(DrawingContext context) { if (Background is not null) { context.FillRectangle(Background, new Rect(0, 0, Bounds.Width, Bounds.Height)); } IBrush select = SelectionBrush ?? Brushes.Cyan; List? fillAfter = null; if (_beginSelect is not null && _endSelect is not null) { fillAfter = new List(); TextPointer bgn, end; if (_beginSelect < _endSelect) { bgn = _beginSelect; end = _endSelect; } else { bgn = _endSelect; end = _beginSelect; } if (ReferenceEquals(bgn.Geometry, end.Geometry)) { var rct = new Rect( bgn.Geometry.Left + bgn.Distance, bgn.Geometry.Top, end.Distance - bgn.Distance, bgn.Geometry.Height); TryRender(bgn.Geometry, rct); } else { TryRender(bgn.Geometry, new Rect(bgn.Geometry.Left + bgn.Distance, bgn.Geometry.Top, bgn.Geometry.Width - bgn.Distance, bgn.Geometry.Height)); foreach (var inter in _intermediates) { TryRender(inter, new Rect(inter.Left, inter.Top, inter.Width, inter.Height)); } TryRender(end.Geometry, new Rect(end.Geometry.Left, end.Geometry.Top, end.Distance, end.Geometry.Height)); } void TryRender(CGeometry metry, Rect rct) { if (metry is TextGeometry) { context.FillRectangle(select, rct); } else { fillAfter.Add(rct); } } } foreach (var metry in _metries) { metry.Render(context); } if (fillAfter is not null) { if (select is ISolidColorBrush colorBrush) { var selectFill = new SolidColorBrush(colorBrush.Color, .5); foreach (var fillRct in fillAfter) { context.FillRectangle(selectFill, fillRct); } } else { foreach (var fillRct in fillAfter) { var pen = new Pen(select, 2); var rct = new Rect(fillRct.Left - 1, fillRct.Top - 1, fillRct.Width + 2, fillRct.Height + 2); context.DrawRectangle(pen, rct); } } } } protected override AutomationPeer OnCreateAutomationPeer() { return new CTextBlockAutomationPeer(this); } public void Select(int begin, int end) { int beginBack = begin; int endBack = end; for (var i = 0; i < _metries.Count; ++i) { var metry = _metries[i]; var caretLength = metry.CaretLength; if (begin < caretLength || (i == _metries.Count - 1 && begin == caretLength)) { _beginSelect = metry.CalcuatePointerFrom(begin).Wrap(this, beginBack - begin); begin = Int32.MaxValue; } else begin -= caretLength; if (end < caretLength || (i == _metries.Count - 1 && end == caretLength)) { _endSelect = metry.CalcuatePointerFrom(end).Wrap(this, endBack - end); if (endBack != _endSelect.Index) throw new Exception(); end = Int32.MaxValue; } else end -= caretLength; } ComplementIntermediate(); InvalidateVisual(); } public void Select(TextPointer begin, TextPointer end) { _beginSelect = begin; _endSelect = end; ComplementIntermediate(); InvalidateVisual(); } private void ComplementIntermediate() { bool bgn = false; bool end = false; _intermediates.Clear(); foreach (var metry in _metries) { bool hitB = false; bool hitE = false; bgn |= (hitB = ReferenceEquals(metry, _beginSelect.Geometry)); end |= (hitE = ReferenceEquals(metry, _endSelect.Geometry)); if (bgn && end) break; if (hitB | hitE) continue; if (bgn | end) { _intermediates.Add(metry); } } } public void ClearSelection() { _beginSelect = null; _endSelect = null; _intermediates.Clear(); InvalidateVisual(); } public TextPointer CalcuatePointerFrom(double x, double y) { if (y < 0) { return GetBegin(); } int indexAdd = 0; foreach (var line in _lines) { if (y <= line.Top + line.Height) { foreach (var target in line.Metries) { if (x <= target.Left + target.Width) { return target.CalcuatePointerFrom(x, y) .Wrap(this, indexAdd); } else { indexAdd += target.CaretLength; } } } else { indexAdd += line.Metries.Sum(t => t.CaretLength); } } return GetEnd(); } public TextPointer CalcuatePointerFrom(int index) { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); foreach (var metry in _metries) { var caretLength = metry.CaretLength; if (index < caretLength) { return metry.CalcuatePointerFrom(index); } else index -= caretLength; } throw new ArgumentOutOfRangeException(nameof(index)); } public TextPointer GetBegin() { if (_metries.Count != 0) { return _metries[0].GetBegin().Wrap(this, 0); } else { return new TextPointer(this, 0); } } public TextPointer GetEnd() { if (_metries.Count != 0) { var pointer = _metries[_metries.Count - 1].GetEnd(); int indexAdd = _metries.Take(_metries.Count - 1).Sum(t => t.CaretLength); return pointer.Wrap(this, indexAdd); } else { return new TextPointer(this, 0); } } public string GetSelectedText() { if (_beginSelect is null || _endSelect is null) { return string.Empty; } TextPointer bgn, end; if (_beginSelect < _endSelect) { bgn = _beginSelect; end = _endSelect; } else { bgn = _endSelect; end = _beginSelect; } if (ReferenceEquals(bgn.Geometry, end.Geometry)) { if (bgn.Geometry is TextLineGeometry tlg) { return tlg.Text.Substring(bgn.InternalIndex, end.InternalIndex - bgn.InternalIndex); } else return ""; } else { var buffer = new StringBuilder(); if (bgn.Geometry is TextLineGeometry btlg) buffer.Append(btlg.Text.Substring(bgn.InternalIndex)); foreach (var inter in _intermediates) { if (inter is TextLineGeometry itlg) buffer.Append(itlg.ToString()); } if (end.Geometry is TextLineGeometry etlg) buffer.Append(etlg.Text.Substring(etlg.Line.FirstTextSourceIndex, end.InternalIndex - etlg.Line.FirstTextSourceIndex)); return buffer.ToString(); } } } public class Selection { public int From { get; } public int To { get; } public Selection(int f, int t) { From = f; To = t; } } class LineInfo { public List Metries = new(); public double RequestBaseHeight; private double BaseHeight1; private double BaseHeight2; private double _height; private double _dheightTop; private double _dheightBtm; public double Top { get; internal set; } public double Width { private set; get; } public double Height => Math.Max(_height, _dheightTop + _dheightBtm); public double BaseHeight => Math.Max(RequestBaseHeight, BaseHeight1 != 0 ? BaseHeight1 : BaseHeight2); public bool Add(CGeometry metry) { Metries.Add(metry); Width += metry.Width; switch (metry.TextVerticalAlignment) { case TextVerticalAlignment.Base: Max(ref BaseHeight1, metry.BaseHeight); Max(ref _dheightTop, metry.BaseHeight); Max(ref _dheightBtm, metry.Height - metry.BaseHeight); break; case TextVerticalAlignment.Top: Max(ref BaseHeight1, metry.BaseHeight); Max(ref _height, metry.Height); break; case TextVerticalAlignment.Center: Max(ref BaseHeight1, metry.Height / 2); Max(ref _height, metry.Height); break; case TextVerticalAlignment.Bottom: Max(ref BaseHeight2, metry.BaseHeight); Max(ref _height, metry.Height); break; default: Throw("sorry library manager forget to modify."); break; } return metry.LineBreak; } public void OverwriteHeight(double height) { _height = height; _dheightBtm = _dheightTop = 0; } private static void Max(ref double v1, double v2) => v1 = Math.Max(v1, v2); private static void Throw(string msg) => throw new InvalidOperationException(msg); } } ================================================ FILE: ColorTextBlock.Avalonia/CTextBlockAutomationPeer.cs ================================================ using Avalonia.Automation.Peers; namespace ColorTextBlock.Avalonia { /// /// The automation peer for CTextBlock. /// public class CTextBlockAutomationPeer : ControlAutomationPeer { public CTextBlockAutomationPeer(CTextBlock owner) : base(owner) { } public new CTextBlock Owner => (CTextBlock)base.Owner; protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Text; protected override string? GetNameCore() => Owner.Text; protected override bool IsControlElementCore() => Owner.TemplatedParent is null && base.IsControlElementCore(); } } ================================================ FILE: ColorTextBlock.Avalonia/CUnderline.cs ================================================ using System.Collections.Generic; namespace ColorTextBlock.Avalonia { /// /// Underline decoration /// public class CUnderline : CSpan { public CUnderline() { } public CUnderline(IEnumerable inlines) : base(inlines) { IsUnderline = true; } } } ================================================ FILE: ColorTextBlock.Avalonia/ColorTextBlock.Avalonia.csproj ================================================  Library $(PackageTargetFrameworks) ColorTextBlock.Avalonia $(PackageVersion) whistyun Copyright (c) 2020 whistyun https://github.com/whistyun/Markdown.Avalonia/tree/master/ColorTextBlock.Avalonia/ MIT 9 enable ================================================ FILE: ColorTextBlock.Avalonia/Fonts/FontFamilyCollector.cs ================================================ using Avalonia.Media; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ColorTextBlock.Avalonia.Fonts { internal class FontFamilyCollector { public static FontFamily? TryGetMonospace() { string[] RequestFamilies = { "menlo", "monaco", "consolas", "droid sans mono", "inconsolata", "courier new", "monospace", "dejavu sans mono", }; var monospaceName = FontManager.Current.SystemFonts .Where(family => RequestFamilies.Any(reqNm => family.Name.ToLower().Contains(reqNm))) .FirstOrDefault(); return monospaceName; } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/CGeometry.cs ================================================ using Avalonia.Controls; using Avalonia.Media; using System; using System.Diagnostics.CodeAnalysis; namespace ColorTextBlock.Avalonia.Geometries { public abstract class CGeometry : ITextPointerHandleable { public CInline Owner { get; } public double Left { get; set; } public double Top { get; set; } public double Width { get; } public double Height { get; } public double BaseHeight { get; } public bool LineBreak { get; } public TextVerticalAlignment TextVerticalAlignment { get; } public event Action? RepaintRequested; public virtual Action? OnMouseEnter { get; set; } public virtual Action? OnMouseLeave { get; set; } public virtual Action? OnMousePressed { get; set; } public virtual Action? OnMouseReleased { get; set; } public virtual Action? OnClick { get; set; } private int? _caretLength; public CGeometry( CInline owner, double width, double height, double baseHeight, TextVerticalAlignment textVerticalAlignment, bool linebreak) { this.Owner = owner; this.Width = width; this.Height = height; this.BaseHeight = baseHeight; this.TextVerticalAlignment = textVerticalAlignment; this.LineBreak = linebreak; } public abstract void Render(DrawingContext ctx); internal void RequestRepaint() => RepaintRequested?.Invoke(); public abstract TextPointer CalcuatePointerFrom(int index); public abstract TextPointer CalcuatePointerFrom(double x, double y); public abstract TextPointer GetBegin(); public abstract TextPointer GetEnd(); public virtual void Arranged() { } public virtual int CaretLength { get { if (!_caretLength.HasValue) _caretLength = GetEnd().Index - GetBegin().Index; return _caretLength.Value; } } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/DecoratorGeometry.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using static System.Net.Mime.MediaTypeNames; namespace ColorTextBlock.Avalonia.Geometries { public class DecoratorGeometry : CGeometry { public CGeometry[] Targets { get; } public Border Decorate { get; } private Action? _OnMouseEnter; private Action? _OnMouseLeave; private Action? _OnMousePressed; private Action? _OnMouseReleased; private Action? _OnClick; public override Action? OnMouseEnter { get => ctrl => { _OnMouseEnter?.Invoke(ctrl); foreach (var target in Targets) target.OnMouseEnter?.Invoke(ctrl); }; set => _OnMouseEnter = value; } public override Action? OnMouseLeave { get => ctrl => { _OnMouseLeave?.Invoke(ctrl); foreach (var target in Targets) target.OnMouseLeave?.Invoke(ctrl); }; set => _OnMouseLeave = value; } public override Action? OnMousePressed { get => ctrl => { _OnMousePressed?.Invoke(ctrl); foreach (var target in Targets) target.OnMousePressed?.Invoke(ctrl); }; set => _OnMousePressed = value; } public override Action? OnMouseReleased { get => ctrl => { _OnMouseReleased?.Invoke(ctrl); foreach (var target in Targets) target.OnMouseReleased?.Invoke(ctrl); }; set => _OnMouseReleased = value; } public override Action? OnClick { get => ctrl => { _OnClick?.Invoke(ctrl); foreach (var target in Targets) target.OnClick?.Invoke(ctrl); }; set => _OnClick = value; } internal static DecoratorGeometry New( CSpan owner, IEnumerable oneline, Border decorate) { double width = 0; double height = 0; double descentHeightTop = 0; double descentHeightBtm = 0; double baseHeight = 0; double baseHeight2 = 0; void Max(ref double v1, double v2) => v1 = Math.Max(v1, v2); foreach (var one in oneline) { width += one.Width; switch (one.TextVerticalAlignment) { case TextVerticalAlignment.Base: Max(ref baseHeight, one.BaseHeight); Max(ref descentHeightTop, one.BaseHeight); Max(ref descentHeightBtm, one.Height - one.BaseHeight); break; case TextVerticalAlignment.Top: Max(ref baseHeight, one.BaseHeight); Max(ref height, one.Height); break; case TextVerticalAlignment.Center: Max(ref baseHeight, one.Height / 2); Max(ref height, one.Height); break; case TextVerticalAlignment.Bottom: Max(ref baseHeight2, one.BaseHeight); Max(ref height, one.Height); break; default: throw new InvalidOperationException("sorry library manager forget to modify."); } } Max(ref height, descentHeightTop + descentHeightBtm); baseHeight = baseHeight != 0 ? baseHeight : baseHeight2; return new DecoratorGeometry( width + decorate.DesiredSize.Width, height + decorate.DesiredSize.Height, baseHeight + decorate.Margin.Top + decorate.BorderThickness.Top + decorate.Padding.Top, owner, oneline.ToArray(), decorate); } private DecoratorGeometry( double w, double h, double lh, CSpan owner, CGeometry[] targets, Border decorate) : base( owner, w, h, lh, owner.TextVerticalAlignment, targets[targets.Length - 1].LineBreak) { this.Targets = targets; this.Decorate = decorate; } public override void Arranged() { var left = Left + Decorate.BorderThickness.Left + Decorate.Padding.Left + Decorate.Margin.Left; var top = Top + Decorate.BorderThickness.Top + Decorate.Padding.Top + Decorate.Margin.Top; var btm = Top + Height - Decorate.BorderThickness.Bottom - Decorate.Padding.Bottom - Decorate.Margin.Bottom; foreach (var target in Targets) { target.Left = left; target.Top = target.TextVerticalAlignment switch { TextVerticalAlignment.Top => top, TextVerticalAlignment.Center => (top + btm - target.Height) / 2, TextVerticalAlignment.Bottom => btm - target.Height, TextVerticalAlignment.Base => Top + BaseHeight - target.BaseHeight, _ => throw new InvalidOperationException("sorry library manager forget to modify.") }; left += target.Width; target.Arranged(); } } public override void Render(DrawingContext ctx) { using (ctx.PushTransform(Matrix.CreateTranslation(Left + Decorate.Margin.Left, Top + Decorate.Margin.Top))) { Decorate.Background = Owner.Background; Decorate.Arrange(new Rect(0, 0, Width, Height)); Decorate.Render(ctx); } foreach (var target in Targets) target.Render(ctx); } public override TextPointer CalcuatePointerFrom(double x, double y) { if (x < Left) { return GetBegin(); } int indexAdd = 0; foreach (var target in Targets.Take(Targets.Length - 1)) { if (x <= target.Left + target.Width) { return target.CalcuatePointerFrom(x, y) .Wrap(Owner, indexAdd); } else { indexAdd += target.CaretLength; } } return Targets[Targets.Length - 1].GetEnd().Wrap(Owner, indexAdd); } public override TextPointer CalcuatePointerFrom(int index) { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); int relindex = index; foreach (var target in Targets) { if (relindex < target.CaretLength) { return target.CalcuatePointerFrom(relindex) .Wrap(Owner, index - relindex); } relindex -= target.CaretLength; } throw new ArgumentOutOfRangeException(nameof(index)); } public override TextPointer GetBegin() { var pointer = Targets[0].GetBegin(); return pointer.Wrap(Owner, 0); } public override TextPointer GetEnd() { var pointer = Targets[Targets.Length - 1].GetEnd(); int indexAdd = Targets.Take(Targets.Length - 1).Sum(t => t.CaretLength); return pointer.Wrap(Owner, indexAdd); } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/DummyGeometryForControl.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Media; using ColorTextBlock.Avalonia.Geometries; using System; using static System.Net.Mime.MediaTypeNames; namespace ColorTextBlock.Avalonia.Geometies { internal class DummyGeometryForControl : CGeometry { public Control Control { get; } public DummyGeometryForControl(CInlineUIContainer owner, Control control, TextVerticalAlignment alignment) : base( owner, control.DesiredSize.Width, control.DesiredSize.Height, control.DesiredSize.Height, alignment, false) { Control = control; } public override void Render(DrawingContext ctx) { } public override TextPointer CalcuatePointerFrom(double x, double y) { if (x < Left + Width / 2) { return GetBegin(); } else { return GetEnd(); } } public override TextPointer CalcuatePointerFrom(int index) { return index switch { 0 => GetBegin(), 1 => GetEnd(), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TextPointer GetBegin() { return new TextPointer(this); } public override TextPointer GetEnd() { return new TextPointer(this, 1, Width); } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/ImageGeometry.cs ================================================ using Avalonia; using Avalonia.Media; using Avalonia.Media.Imaging; using System; using System.Diagnostics.CodeAnalysis; namespace ColorTextBlock.Avalonia.Geometries { public class ImageGeometry : CGeometry { public new double Width { get; } public new double Height { get; } public IImage Image { get; } internal ImageGeometry( CImage owner, IImage image, double width, double height, TextVerticalAlignment alignment) : base(owner, width, height, height, alignment, false) { this.Image = image; this.Width = width; this.Height = height; } public override void Render(DrawingContext ctx) { ctx.DrawImage( Image, new Rect(Image.Size), new Rect(Left, Top, Width, Height)); } public override TextPointer CalcuatePointerFrom(double x, double y) { if (x < Left + Width / 2) { return GetBegin(); } else { return GetEnd(); } } public override TextPointer CalcuatePointerFrom(int index) { return index switch { 0 => GetBegin(), 1 => GetEnd(), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TextPointer GetBegin() { return new TextPointer(this); } public override TextPointer GetEnd() { return new TextPointer(this, 1, Width); } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/LineBreakMarkGeometry.cs ================================================ using Avalonia; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.TextFormatting; using System; using System.Linq; using static System.Net.Mime.MediaTypeNames; namespace ColorTextBlock.Avalonia.Geometries { internal class LineBreakMarkGeometry : TextGeometry { private bool IsDummy { get; } internal LineBreakMarkGeometry( CInline owner, double lineHeight) : base(owner, 0, lineHeight, lineHeight, TextVerticalAlignment.Base, true) { IsDummy = false; } internal LineBreakMarkGeometry(CInline owner) : base(owner, 0, 0, 0, TextVerticalAlignment.Base, true) { IsDummy = true; } public override void Render(DrawingContext ctx) { } public override TextPointer CalcuatePointerFrom(double x, double y) { throw new InvalidOperationException(); } public override TextPointer CalcuatePointerFrom(int index) { return index switch { 0 => GetBegin(), 1 => GetEnd(), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TextPointer GetBegin() { return new TextPointer(this); } public override TextPointer GetEnd() { return IsDummy ? new TextPointer(this) : new TextPointer(this, 1, Width); } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/TextGeometry.cs ================================================ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; using System.Linq; namespace ColorTextBlock.Avalonia.Geometries { internal abstract class TextGeometry : CGeometry { private IBrush? _TemporaryForeground; public IBrush? TemporaryForeground { get => _TemporaryForeground; set => _TemporaryForeground = value; } private IBrush? _TemporaryBackground; public IBrush? TemporaryBackground { get => _TemporaryBackground; set => _TemporaryBackground = value; } public IBrush? Foreground { get => Owner?.Foreground; } public IBrush? Background { get => Owner?.Background; } public bool IsUnderline { get => Owner is null ? false : Owner.IsUnderline; } public bool IsStrikethrough { get => Owner is null ? false : Owner.IsStrikethrough; } internal TextGeometry( CInline owner, double width, double height, double lineHeight, TextVerticalAlignment alignment, bool linebreak) : base(owner, width, height, lineHeight, alignment, linebreak) { } } } ================================================ FILE: ColorTextBlock.Avalonia/Geometies/TextLineGeometry.cs ================================================ using Avalonia; using Avalonia.Media; using Avalonia.Media.TextFormatting; using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using static System.Net.Mime.MediaTypeNames; namespace ColorTextBlock.Avalonia.Geometries { internal class TextLineGeometry : TextGeometry { public SimpleTextSource Text { get; private set; } public TextLine Line { get; private set; } public IBrush? LayoutForeground { get; private set; } internal TextLineGeometry( CRun owner, SimpleTextSource text, TextLine tline, bool linebreak) : base(owner, tline.WidthIncludingTrailingWhitespace, tline.Height, tline.Baseline, owner.TextVerticalAlignment, linebreak) { Text = text; Line = tline; LayoutForeground = owner.Foreground; } public override void Render(DrawingContext ctx) { var foreground = TemporaryForeground ?? Foreground; var background = TemporaryBackground ?? Background; if (LayoutForeground != foreground) { LayoutForeground = foreground; Text = Text.ChangeForeground(foreground); var owner = (CRun)Owner; var parPrps = owner.CreateTextParagraphProperties(Text.RunProperties); Line = TextFormatter.Current.FormatLine( Text, Line.FirstTextSourceIndex, Width, parPrps)!; } if (background != null) { ctx.FillRectangle(background, new Rect(Left, Top, Width, Height)); } Line.Draw(ctx, new Point(Left, Top)); if (IsUnderline) { var ypos = Math.Round(Top + Height); ctx.DrawLine(new Pen(foreground, 2), new Point(Left, ypos), new Point(Left + Width, ypos)); } if (IsStrikethrough) { var ypos = Math.Round(Top + Height / 2); ctx.DrawLine(new Pen(foreground, 2), new Point(Left, ypos), new Point(Left + Width, ypos)); } } public override TextPointer CalcuatePointerFrom(double x, double y) { var relX = x - Left; if (relX < 0) return GetBegin(); if (relX >= Width) return GetEnd(); var hit = Line.GetCharacterHitFromDistance(relX); var dst = Line.GetDistanceFromCharacterHit(hit); return new TextPointer((CRun)Owner, this, hit, dst, false); } public override TextPointer CalcuatePointerFrom(int index) { var hit = new CharacterHit(Line.FirstTextSourceIndex + index); var dst = Line.GetDistanceFromCharacterHit(hit); return new TextPointer((CRun)Owner, this, hit, dst, false); } public override TextPointer GetBegin() { var hit = Line.GetCharacterHitFromDistance(0); return new TextPointer((CRun)Owner, this, hit, false); } public override TextPointer GetEnd() { var hit = Line.GetCharacterHitFromDistance(Double.MaxValue); return new TextPointer((CRun)Owner, this, hit, Width, true); } public override string ToString() => Text.Substring(Line.FirstTextSourceIndex, Line.Length); } } ================================================ FILE: ColorTextBlock.Avalonia/StringToRunConverter.cs ================================================ using System; using System.ComponentModel; using System.Globalization; using System.Text.RegularExpressions; namespace ColorTextBlock.Avalonia { public class StringToRunConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) { return sourceType == typeof(string); } public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) { var txt = (string)value; txt = Regex.Replace(txt, "[\r\n \t]+", " "); return new CRun() { Text = String.IsNullOrEmpty(txt) ? " " : txt }; } } } ================================================ FILE: ColorTextBlock.Avalonia/TextPointer.cs ================================================ using Avalonia.Media; using ColorTextBlock.Avalonia.Geometries; using System; using System.Collections.Generic; using System.Linq; namespace ColorTextBlock.Avalonia { public class TextPointer : IEquatable, IComparable { public int Index { get; } internal int InternalIndex { get; } internal int TrailingLength { get; } internal double Distance { get; } internal CGeometry Geometry { get; } internal int PathDepth => _path.Length; internal CInline this[int idx] => _path[idx]; private CInline[] _path; private TextPointer(CInline[] path, CGeometry geometry, int index, int internalIndex, int trallingLength, double distance) { _path = path; Geometry = geometry; Index = index; InternalIndex = internalIndex; TrailingLength = trallingLength; Distance = distance; } internal TextPointer(CRun inline, TextLineGeometry target, CharacterHit charHit, bool isLast) { _path = new[] { inline }; Geometry = target; if (isLast) { var lastIdx = charHit.FirstCharacterIndex + charHit.TrailingLength; Index = lastIdx - target.Line.FirstTextSourceIndex; InternalIndex = lastIdx; TrailingLength = 0; } else { Index = charHit.FirstCharacterIndex - target.Line.FirstTextSourceIndex; InternalIndex = charHit.FirstCharacterIndex; TrailingLength = charHit.TrailingLength; } } internal TextPointer(CRun inline, TextLineGeometry target, CharacterHit charHit, double distance, bool isLast) : this(inline, target, charHit, isLast) { Distance = distance; } internal TextPointer(CGeometry inline) { _path = new[] { inline.Owner }; Geometry = inline; Index = 0; InternalIndex = 0; TrailingLength = 0; } internal TextPointer(CGeometry inline, int idx, double distance) { _path = new[] { inline.Owner }; Geometry = inline; Index = idx; InternalIndex = 0; TrailingLength = 0; Distance = distance; } internal TextPointer(CTextBlock host, int idx) { _path = Array.Empty(); Index = idx; InternalIndex = 0; TrailingLength = 0; } internal TextPointer Wrap(CInline owner, int indexAdding) { var path = new List(_path.Length + 1); path.Add(owner); path.AddRange(_path); return new TextPointer( path.ToArray(), Geometry, Index + indexAdding, InternalIndex, TrailingLength, Distance); } internal TextPointer Wrap(CTextBlock host, int indexAdding) { return new TextPointer( _path, Geometry, Index + indexAdding, InternalIndex, TrailingLength, Distance); } public override int GetHashCode() { return _path.Sum(e => e.GetHashCode()) + Index.GetHashCode() + InternalIndex.GetHashCode() + TrailingLength.GetHashCode(); } public bool Equals(TextPointer? other) { return PathDepth == other.PathDepth && Enumerable.Range(0, PathDepth).All(i => Object.ReferenceEquals(_path[i], other[i])) && Index == other.Index && InternalIndex == other.InternalIndex && TrailingLength == other.TrailingLength; } public int CompareTo(TextPointer? other) => other is not null ? Index.CompareTo(other.Index) : throw new ArgumentNullException(nameof(other)); public static bool operator <(TextPointer left, TextPointer right) => left.CompareTo(right) < 0; public static bool operator >(TextPointer left, TextPointer right) => left.CompareTo(right) > 0; public static bool operator <=(TextPointer left, TextPointer right) => left.CompareTo(right) <= 0; public static bool operator >=(TextPointer left, TextPointer right) => left.CompareTo(right) >= 0; } public interface ITextPointerHandleable { /// /// Calcuates position from relative coordinates. /// The origin of the relative coordinates is based on CTextBlock. /// /// The x coordinate of caret position on CTextBlock /// The y coordinate of caret position on CTextBlock /// public TextPointer CalcuatePointerFrom(double x, double y); public TextPointer CalcuatePointerFrom(int index); public TextPointer GetBegin(); public TextPointer GetEnd(); } public interface ISelectable { public void ClearSelection(); public void Select(TextPointer start, TextPointer end); } } ================================================ FILE: ColorTextBlock.Avalonia/TextVerticalAlignment.cs ================================================  namespace ColorTextBlock.Avalonia { /// /// The vertical position of text within line /// /// public enum TextVerticalAlignment { /// /// Text elements are placed at the top of the line. /// Top, /// /// Text elements are placed at the middle of the line. /// Center, /// /// Text elements are placed at the bottom of the line. /// Bottom, /// /// Text elements are placed at the bottom of the line. /// This treats only the height of text element and ignores the padding of element. /// Therefore vertical positions of the text will be aligned. /// Base, } } ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2010 Bevan Arps, 2020 Whistyun Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Markdown.Avalonia/Markdown.Avalonia.csproj ================================================  $(PackageTargetFrameworks) $(PackageVersion) Bevan Arps(original); whistyun Markdown.Avalonia Markdown.Avalonia.Full Markdown.Avalonia.Full Markdown Controls for Avalonia Copyright (c) 2010 Bevan Arps, 2020 whistyun https://github.com/whistyun/Markdown.Avalonia 9 MIT Markdown.Avalonia.md Markdown Avalonia Avaloniaui ================================================ FILE: Markdown.Avalonia/MarkdownScrollViewer.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Full { public class MarkdownScrollViewer : global::Markdown.Avalonia.MarkdownScrollViewer { public MarkdownScrollViewer() { Plugins = new MdAvPlugins(); } } } ================================================ FILE: Markdown.Avalonia/MdAvPlugins.cs ================================================ using Markdown.Avalonia.Html; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.Svg; using Markdown.Avalonia.SyntaxHigh; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Full { public class MdAvPlugins : global::Markdown.Avalonia.MdAvPlugins { public MdAvPlugins() { } protected override SetupInfo CreateInfo() { var setupInf = new SetupInfo(); var hasSyntaxHigh = false; var hasSvgFormat = false; var hasHtml = false; SyntaxHighlight? syntaxPlugin = null; ( IEnumerable orderedPlugin, Dictionary dic ) = ComputeOrderedPlugins(); foreach (var plugin in orderedPlugin) { if (plugin is IMdAvPluginRequestAnother another) { another.Inject(another.DependsOn.Select(dep => dic[dep])); } plugin.Setup(setupInf); if (plugin is SyntaxHighlight light) { hasSyntaxHigh = true; syntaxPlugin = light; } hasSvgFormat |= plugin is SvgFormat; hasHtml |= plugin is HtmlPlugin; } if (!hasSyntaxHigh) { syntaxPlugin = new SyntaxHighlight(); syntaxPlugin.Setup(setupInf); } if (!hasSvgFormat) { var svgPlugin = new SvgFormat(); svgPlugin.Setup(setupInf); } if (!hasHtml) { var htmlPlugin = new HtmlPlugin(); htmlPlugin.Inject(new[] { syntaxPlugin }); htmlPlugin.Setup(setupInf); } if (PathResolver is not null) setupInf.SetOnce(PathResolver); if (ContainerBlockHandler is not null) setupInf.SetOnce(ContainerBlockHandler); if (HyperlinkCommand is not null) setupInf.SetOnce(HyperlinkCommand); setupInf.Freeze(); return setupInf; } } } ================================================ FILE: Markdown.Avalonia/Properties/AssemblyInfo.cs ================================================ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia", "Markdown.Avalonia.Full")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia", "mdxaml")] ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/ButtonParser.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class ButtonParser : IInlineTagParser { public IEnumerable SupportTag => new[] { "button" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var doc = new StackPanel() { Orientation = Orientation.Vertical }; doc.Children.AddRange(manager.ParseChildrenAndGroup(node)); doc.Loaded += (s, e) => { var desiredWidth = doc.DesiredSize.Width; var desiredHeight = doc.DesiredSize.Height; for (int i = 0; i < 10; ++i) { desiredWidth /= 2; var size = new Size(desiredWidth, double.PositiveInfinity); doc.Measure(size); if (desiredHeight != doc.DesiredSize.Height) break; // Give up because it will not be wrapped back. if (i == 9) return; } var preferedWidth = desiredWidth * 2; for (int i = 0; i < 10; ++i) { var width = (desiredWidth + preferedWidth) / 2; var size = new Size(width, double.PositiveInfinity); doc.Measure(size); if (desiredHeight == doc.DesiredSize.Height) { preferedWidth = width; } else { desiredWidth = width; } } doc.Width = preferedWidth; }; var btn = new Button() { Content = doc, IsEnabled = false, }; generated = new[] { new CInlineUIContainer(btn) }; return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/CodeBlockParser.cs ================================================ using Avalonia; using Avalonia.Controls; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using Markdown.Avalonia.SyntaxHigh; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Markdown.Avalonia.Html.Core.Parsers { public class CodeBlockParser : IBlockTagParser { SyntaxHighlightProvider _provider; public CodeBlockParser(SyntaxHighlight syntax) { _provider = new SyntaxHighlightProvider(syntax.Aliases); } public IEnumerable SupportTag => new[] { "pre" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); var codeElements = node.ChildNodes.CollectTag("code"); if (codeElements.Count != 0) { var rslt = new List(); foreach (var codeElement in codeElements) { // "language-**", "lang-**", "**" or "sourceCode **" var classVal = codeElement.Attributes["class"]?.Value; var langCode = ParseLangCode(classVal); rslt.Add(DocUtils.CreateCodeBlock(langCode, codeElement.InnerText, manager, _provider)); } generated = rslt; return rslt.Count > 0; } else if (node.ChildNodes.TryCastTextNode(out var textNodes)) { var buff = new StringBuilder(); foreach (var textNode in textNodes) buff.Append(textNode.InnerText); generated = new[] { DocUtils.CreateCodeBlock(null, buff.ToString(), manager, _provider) }; return true; } else return false; } private static string ParseLangCode(string? classVal) { if (classVal is null) return ""; // "language-**", "lang-**", "**" or "sourceCode **" var indics = Enumerable.Range(0, classVal.Length) .Reverse() .Where(i => !Char.IsLetterOrDigit(classVal[i])); return classVal.Substring(indics.Any() ? indics.First() + 1 : 0); } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/CommentParser.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using System; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { /// /// remove comment element /// public class CommentParsre : IBlockTagParser, IInlineTagParser { public IEnumerable SupportTag => new[] { HtmlNode.HtmlNodeTypeNameComment }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/DetailsParser.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Layout; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using Markdown.Avalonia; using System.Collections.Generic; using System.ComponentModel; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class DetailsParser : IBlockTagParser { public IEnumerable SupportTag => new[] { "details" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var summary = node.ChildNodes.FirstOrDefault(e => e.IsElement("summary")); if (summary is null) { generated = EnumerableExt.Empty(); return false; } var content = node.ChildNodes.Where(e => !ReferenceEquals(e, summary)); var header = Create(manager.Engine, manager.ParseChildrenAndGroup(summary)); var expander = new Expander() { Header = header, Content = Create(manager.Engine, manager.Grouping(manager.ParseChildrenJagigng(content))), }; if (node.Attributes["open"] is HtmlAttribute openAttr && bool.TryParse(openAttr.Value, out var isOpened)) { expander.IsExpanded = isOpened; } generated = new[] { expander }; return true; } private static StackPanel Create(IMarkdownEngine engine, IEnumerable blocks) { var doc = new StackPanel() { Orientation = Orientation.Vertical }; doc.Children.AddRange(blocks); return doc; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/HorizontalRuleParser.cs ================================================ using Avalonia; using Avalonia.Controls; using HtmlAgilityPack; using Markdown.Avalonia.Controls; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class HorizontalRuleParser : IBlockTagParser { public IEnumerable SupportTag => new[] { "hr" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rule = new Rule(RuleType.Single); rule.Classes.Add(Tags.TagRuleSingle.GetClass()); generated = new[] { rule }; return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/ITagParser.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public interface ITagParser { IEnumerable SupportTag { get; } bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); } public interface IInlineTagParser : ITagParser { bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); } public interface IBlockTagParser : ITagParser { bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated); } public interface IHasPriority { int Priority { get; } } public static class HasPriority { public const int DefaultPriority = 10000; public static int GetPriority(this ITagParser parser) => parser is IHasPriority prop ? prop.Priority : HasPriority.DefaultPriority; } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/ImageParser.cs ================================================ using Avalonia; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Layout; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using Markdown.Avalonia.Plugins; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class ImageParser : IInlineTagParser { private SetupInfo _setupInfo; public ImageParser(SetupInfo info) { _setupInfo = info; } public IEnumerable SupportTag => new[] { "img", "image" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var link = node.Attributes["src"]?.Value; var alt = node.Attributes["alt"]?.Value; if (link is null) { generated = EnumerableExt.Empty(); return false; } var title = node.Attributes["title"]?.Value; var widthTxt = node.Attributes["width"]?.Value; var heightTxt = node.Attributes["height"]?.Value; CImage image = _setupInfo.LoadImage(link); if (!String.IsNullOrEmpty(title) && !title.Any(ch => !Char.IsLetterOrDigit(ch))) { image.Classes.Add(title); } if (Length.TryParse(heightTxt, out var heightLen)) { if (heightLen.Unit == Unit.Percentage) { image.Bind(CImage.LayoutHeightProperty, new Binding(nameof(Layoutable.Height)) { RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(CTextBlock), }, Converter = new MultiplyConverter(heightLen.Value / 100) }); } else { image.LayoutHeight = heightLen.ToPoint(); } } // Bind size so document is updated when image is downloaded if (Length.TryParse(widthTxt, out var widthLen)) { if (widthLen.Unit == Unit.Percentage) { image.Bind(CImage.LayoutHeightProperty, new Binding(nameof(Layoutable.Width)) { RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(CTextBlock), }, Converter = new MultiplyConverter(widthLen.Value / 100) }); } else { if (image.LayoutHeight.HasValue) image.LayoutWidth = widthLen.ToPoint(); else image.RelativeWidth = widthLen.ToPoint(); } } generated = new[] { image }; return true; } class MultiplyConverter : IValueConverter { public double Value { get; } public MultiplyConverter(double v) { Value = v; } public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { return value is null ? 0d : Value * (Double)value; } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { return value is null ? 0d : ((Double)value) / Value; } } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/InputParser.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class InputParser : IInlineTagParser { public IEnumerable SupportTag => new[] { "input" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var type = node.Attributes["type"]?.Value ?? "text"; double? width = null; var widAttr = node.Attributes["width"]; var sizAttr = node.Attributes["size"]; if (widAttr is not null) { if (double.TryParse(widAttr.Value, out var v)) width = v; } if (sizAttr is not null) { if (int.TryParse(sizAttr.Value, out var v)) width = v * 7; } CInlineUIContainer inline; switch (type) { default: case "text": var txt = new TextBox() { Text = node.Attributes["value"]?.Value ?? "", IsReadOnly = true, }; if (width.HasValue) txt.Width = width.Value; else if (String.IsNullOrEmpty(txt.Text)) txt.Width = 100; inline = new CInlineUIContainer(txt); break; case "button": case "reset": case "submit": var btn = new Button() { Content = node.Attributes["value"]?.Value ?? "", IsEnabled = false, }; if (width.HasValue) btn.Width = width.Value; else if (String.IsNullOrEmpty(btn.Content.ToString())) btn.Width = 100; inline = new CInlineUIContainer(btn); break; case "radio": var radio = new RadioButton() { IsEnabled = false, }; if (node.Attributes["checked"] != null) radio.IsChecked = true; inline = new CInlineUIContainer(radio); break; case "checkbox": var chk = new CheckBox() { IsEnabled = false }; if (node.Attributes["checked"] != null) chk.IsChecked = true; inline = new CInlineUIContainer(chk); break; case "range": var slider = new Slider() { IsEnabled = false, Minimum = 0, Value = 50, Maximum = 100, Width = 100, }; var minAttr = node.Attributes["min"]; if (minAttr is not null && double.TryParse(minAttr.Value, out var minVal)) { slider.Minimum = minVal; } var maxAttr = node.Attributes["max"]; if (maxAttr is not null && double.TryParse(maxAttr.Value, out var maxVal)) { slider.Maximum = maxVal; } var valAttr = node.Attributes["value"]; if (valAttr is not null && double.TryParse(valAttr.Value, out var val)) { slider.Value = val; } else { slider.Value = (slider.Minimum + slider.Maximum) / 2; } inline = new CInlineUIContainer(slider); break; } generated = new[] { inline }; return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/OrderListParser.cs ================================================ using HtmlAgilityPack; using System; using System.Collections.Generic; using Markdown.Avalonia.Html.Core.Utils; using System.Windows; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using System.Reflection; using ColorTextBlock.Avalonia; using Avalonia.Media; namespace Markdown.Avalonia.Html.Core.Parsers { public class OrderListParser : IBlockTagParser { public IEnumerable SupportTag => new[] { "ol" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var list = new Grid(); list.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); list.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star))); int index = 0; int order = 1; var startAttr = node.Attributes["start"]; if (startAttr is not null && Int32.TryParse(startAttr.Value, out var start)) { order = start; } foreach (var listItemTag in node.ChildNodes.CollectTag("li")) { var itemContent = manager.ParseChildrenAndGroup(listItemTag); var markerTxt = new CTextBlock(order + "."); markerTxt.TextAlignment = TextAlignment.Right; markerTxt.TextWrapping = TextWrapping.NoWrap; markerTxt.Classes.Add(global::Markdown.Avalonia.Markdown.ListMarkerClass); var item = CreateItem(itemContent); list.RowDefinitions.Add(new RowDefinition()); list.Children.Add(markerTxt); list.Children.Add(item); Grid.SetRow(markerTxt, index); Grid.SetColumn(markerTxt, 0); Grid.SetRow(item, index); Grid.SetColumn(item, 1); ++index; ++order; } generated = new[] { list }; return true; } private StackPanel CreateItem(IEnumerable children) { var panel = new StackPanel() { Orientation = Orientation.Vertical }; foreach (var child in children) panel.Children.Add(child); return panel; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/ProgressParser.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class ProgressParser : IInlineTagParser { public IEnumerable SupportTag => new[] { "progress", "meter" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var bar = new ProgressBar() { Value = TryParse(node.Attributes["value"]?.Value, 1), Minimum = TryParse(node.Attributes["min"]?.Value, 0), Maximum = TryParse(node.Attributes["max"]?.Value, 1), Width = 50, Height = 12, }; generated = new[] { new CInlineUIContainer(bar) }; return true; } private static int TryParse(string? txt, int def) { if (txt is null) return def; return int.TryParse(txt, out var v) ? v : def; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TagIgnoreParser.cs ================================================ using Avalonia; using Avalonia.Controls; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class TagIgnoreParser : IBlockTagParser, IInlineTagParser { public IEnumerable SupportTag => new[] { "title", "meta", "link", "script", "style", "datalist" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { generated = EnumerableExt.Empty(); return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TextAreaParser.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class TextAreaParser : IInlineTagParser { public IEnumerable SupportTag => new[] { "textarea" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var area = new TextBox() { AcceptsReturn = true, AcceptsTab = true, Text = node.InnerText.Trim(), TextWrapping = TextWrapping.Wrap, }; int? rows = null; int? cols = null; var rowsAttr = node.Attributes["rows"]; var colsAttr = node.Attributes["cols"]; if (rowsAttr is not null) { if (int.TryParse(rowsAttr.Value, out var v)) rows = v * 14; } if (colsAttr is not null) { if (int.TryParse(colsAttr.Value, out var v)) cols = v * 7; } if (rows.HasValue) area.Height = rows.Value; if (cols.HasValue) area.Width = cols.Value; generated = new[] { new CInlineUIContainer(area) }; return true; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TextNodeParser.cs ================================================ using Avalonia; using Avalonia.Controls.Documents; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Parsers { public class TextNodeParser : IInlineTagParser { public IEnumerable SupportTag => new[] { HtmlNode.HtmlNodeTypeNameText }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { if (node is HtmlTextNode textNode) { generated = Replace(textNode.Text, manager); return true; } generated = EnumerableExt.Empty(); return false; } public IEnumerable Replace(string text, ReplaceManager manager) => text.StartsWith("\n") ? new[] { new CRun() { Text = text.Replace('\n', ' ') } } : manager.Engine.RunSpanGamut(text.Replace('\n', ' ')); } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TypicalBlockParser.cs ================================================ using Avalonia; using Avalonia.Controls; using HtmlAgilityPack; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class TypicalBlockParser : IBlockTagParser { private const string _resource = "Markdown.Avalonia.Html.Core.Parsers.TypicalBlockParser.tsv"; private TypicalParseInfo _parser; public IEnumerable SupportTag => new[] { _parser.HtmlTag }; public TypicalBlockParser(TypicalParseInfo parser) { _parser = parser; } bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = _parser.TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = _parser.TryReplace(node, manager, out var list); generated = list.Cast(); return rtn; } public static IEnumerable Load() { foreach (var info in TypicalParseInfo.Load(_resource)) { yield return new TypicalBlockParser(info); } } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TypicalBlockParser.tsv ================================================ #HtmlTag | FlowDocumentTag | TagName | ExtraModify address | #border | TagAddress | article | #border | TagArticle | aside | #border | TagAside | blockquote | #border | TagBlockquote | center | #border | TagCenter | Center div | #blocks | | footer | #border | TagFooter | h1 | #blocks | TagHeading1 | h2 | #blocks | TagHeading2 | h3 | #blocks | TagHeading3 | h4 | #blocks | TagHeading4 | h5 | #blocks | TagHeading5 | h6 | #blocks | TagHeading6 | noframes | #blocks | | noscript | #blocks | | p | #blocks | | ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TypicalInlineParser.cs ================================================ using Avalonia; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class TypicalInlineParser : IInlineTagParser { private const string _resource = "Markdown.Avalonia.Html.Core.Parsers.TypicalInlineParser.tsv"; private readonly TypicalParseInfo _parser; public IEnumerable SupportTag => new[] { _parser.HtmlTag }; public TypicalInlineParser(TypicalParseInfo parser) { _parser = parser; } bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = _parser.TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = _parser.TryReplace(node, manager, out var list); generated = list.Cast(); return rtn; } public static IEnumerable Load() { foreach (var info in TypicalParseInfo.Load(_resource)) { yield return new TypicalInlineParser(info); } } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TypicalInlineParser.tsv ================================================ #HtmlTag | FlowDocumentTag | TagName | ExtraModify a | ColorTextBlock.Avalonia.CHyperlink | TagHyperlink | Hyperlink abbr | ColorTextBlock.Avalonia.CSpan | TagAbbr | Acronym acronym | ColorTextBlock.Avalonia.CSpan | TagAbbr | Acronym b | ColorTextBlock.Avalonia.CBold | TagBold | bdi | ColorTextBlock.Avalonia.CSpan | TagBdi | br | ColorTextBlock.Avalonia.CLineBreak | | cite | ColorTextBlock.Avalonia.CSpan | TagCite | del | ColorTextBlock.Avalonia.CSpan | TagStrikethrough | Strikethrough em | ColorTextBlock.Avalonia.CItalic | TagItalic | i | ColorTextBlock.Avalonia.CItalic | TagItalic | ins | ColorTextBlock.Avalonia.CUnderline | TagUnderline | mark | ColorTextBlock.Avalonia.CSpan | TagMark | s | ColorTextBlock.Avalonia.CSpan | TagStrikethrough | Strikethrough strike | ColorTextBlock.Avalonia.CSpan | TagStrikethrough | Strikethrough strong | ColorTextBlock.Avalonia.CBold | TagBold | sub | ColorTextBlock.Avalonia.CSpan | TagSubscript | Subscript sup | ColorTextBlock.Avalonia.CSpan | TagSuperscript | Superscript u | ColorTextBlock.Avalonia.CUnderline | TagUnderline | code | ColorTextBlock.Avalonia.CCode | TagCodeSpan | kbd | ColorTextBlock.Avalonia.CCode | TagCodeSpan | var | ColorTextBlock.Avalonia.CCode | TagCodeSpan | ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/TypicalParseInfo.cs ================================================ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Layout; using ColorTextBlock.Avalonia; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; namespace Markdown.Avalonia.Html.Core.Parsers { public class TypicalParseInfo { public string HtmlTag { get; } public string FlowDocumentTagText { get; } public Type? FlowDocumentTag { get; } public string? TagNameReference { get; } public Tags TagName { get; } public string? ExtraModifyName { get; } public TypicalParseInfo(string[] line) { FlowDocumentTagText = line[1]; if (FlowDocumentTagText.StartsWith("#")) { FlowDocumentTag = null; } else { Type? elementType = AppDomain.CurrentDomain .GetAssemblies() .Select(asm => asm.GetType(FlowDocumentTagText)) .OfType() .FirstOrDefault(); if (elementType is null) throw new ArgumentException($"Failed to load type '{line[1]}'"); FlowDocumentTag = elementType; } HtmlTag = line[0]; TagNameReference = GetArrayAt(line, 2); ExtraModifyName = GetArrayAt(line, 3); if (TagNameReference is not null) { TagName = (Tags)Enum.Parse(typeof(Tags), TagNameReference); } if (ExtraModifyName is not null) { switch ("ExtraModify" + ExtraModifyName) { case nameof(ExtraModifyHyperlink): case nameof(ExtraModifyStrikethrough): case nameof(ExtraModifySubscript): case nameof(ExtraModifySuperscript): case nameof(ExtraModifyAcronym): case nameof(ExtraModifyCenter): break; default: throw new InvalidOperationException("unknown method ExtraModify" + ExtraModifyName); } } static string? GetArrayAt(string[] array, int idx) { if (idx < array.Length && !string.IsNullOrWhiteSpace(array[idx])) { return array[idx]; } return null; } } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { // create instance if (FlowDocumentTag is null) { switch (FlowDocumentTagText) { case "#border": var pnl = new StackPanel(); pnl.Orientation = Orientation.Vertical; var parseResult = manager.ParseChildrenAndGroup(node).ToArray(); foreach (var ctrl in parseResult) pnl.Children.Add(ctrl); var bdr = new Border(); bdr.Child = pnl; generated = new[] { bdr }; break; case "#blocks": generated = manager.ParseChildrenAndGroup(node).ToArray(); break; case "#jagging": generated = manager.ParseChildrenJagging(node).ToArray(); break; case "#inlines": if (manager.ParseChildrenJagging(node).TryCast(out var inlines)) { generated = inlines.ToArray(); break; } else { generated = EnumerableExt.Empty(); return false; } default: throw new InvalidOperationException(); } // for href anchor //if (node.Attributes["id"]?.Value is string idval // && generated.FirstOrDefault() is AvaloniaObject tag) //{ // tag.SetValue(DocumentAnchor.HyperlinkAnchorProperty, idval); //} } else { var tag = (StyledElement)Activator.CreateInstance(FlowDocumentTag)!; // for href anchor //if (node.Attributes["id"]?.Value is string idval) //{ // tag.SetValue(DocumentAnchor.HyperlinkAnchorProperty, idval); //} if (tag is StackPanel pnl) { pnl.Orientation = Orientation.Vertical; var parseResult = manager.ParseChildrenAndGroup(node).ToArray(); foreach (var ctrl in parseResult) pnl.Children.Add(ctrl); } else if (tag is CTextBlock textbox) { var parseResult = manager.ParseChildrenJagging(node).ToArray(); if (parseResult.TryCast(out var parsed)) { textbox.Content.AddRange(parsed); } else if (parseResult.Length == 1 && parseResult[0] is CTextBlock) { tag = parseResult[0]; } else { generated = EnumerableExt.Empty(); return false; } } else if (tag is CSpan span) { if (!SetupCSpan(span)) { generated = EnumerableExt.Empty(); return false; } } else if (tag is CCode code) { var codecontent = (AvaloniaList)code.Content; var codespan = new CSpan(); codecontent.Add(codespan); if (!SetupCSpan(codespan)) { generated = EnumerableExt.Empty(); return false; } } else if (tag is not CLineBreak) { throw new InvalidOperationException(); } generated = new[] { tag }; bool SetupCSpan(CSpan span) { var content = (AvaloniaList)span.Content; var parseResult = manager.ParseChildrenJagging(node).ToArray(); if (parseResult.TryCast(out var parsed)) { content.AddRange(parsed); } else if (tag is CSpan && manager.Grouping(parseResult).TryCast(out var paragraphs)) { // FIXME: Markdonw.Avalonia can't bubbling a block element in a inline element. foreach (var para in paragraphs) foreach (var inline in para.Content.ToArray()) content.Add(inline); } else return false; return true; } } // apply tag if (TagNameReference is not null) { var clsNm = TagName.GetClass(); foreach (var tag in generated) { tag.Classes.Add(clsNm); if (tag is Border bdr) bdr.Child.Classes.Add(clsNm); } } // extra modify if (ExtraModifyName is not null) { switch ("ExtraModify" + ExtraModifyName) { case nameof(ExtraModifyHyperlink): foreach (var tag in generated) ExtraModifyHyperlink((CHyperlink)tag, node, manager); break; case nameof(ExtraModifyStrikethrough): foreach (var tag in generated) ExtraModifyStrikethrough((CSpan)tag, node, manager); break; case nameof(ExtraModifySubscript): foreach (var tag in generated) ExtraModifySubscript((CSpan)tag, node, manager); break; case nameof(ExtraModifySuperscript): foreach (var tag in generated) ExtraModifySuperscript((CSpan)tag, node, manager); break; case nameof(ExtraModifyAcronym): foreach (var tag in generated) ExtraModifyAcronym((CSpan)tag, node, manager); break; case nameof(ExtraModifyCenter): foreach (var tag in generated) ExtraModifyCenter((Border)tag, node, manager); break; } } return true; } public void ExtraModifyHyperlink(CHyperlink link, HtmlNode node, ReplaceManager manager) { var href = node.Attributes["href"]?.Value; if (href is not null) { link.CommandParameter = href; link.Command = (urlTxt) => { var command = manager.HyperlinkCommand; if (command != null && command.CanExecute(urlTxt)) { command.Execute(urlTxt); } }; } } public void ExtraModifyStrikethrough(CSpan span, HtmlNode node, ReplaceManager manager) { span.IsStrikethrough = true; } public void ExtraModifySubscript(CSpan span, HtmlNode node, ReplaceManager manager) { // TODO implements Subscript //Typography.SetVariants(span, FontVariants.Subscript); } public void ExtraModifySuperscript(CSpan span, HtmlNode node, ReplaceManager manager) { // TODO implements Superscript //Typography.SetVariants(span, FontVariants.Superscript); } public void ExtraModifyAcronym(CSpan span, HtmlNode node, ReplaceManager manager) { // TODO implements Acronym //var title = node.Attributes["title"]?.Value; //if (title is not null) // span.ToolTip = title; } public void ExtraModifyCenter(Border center, HtmlNode node, ReplaceManager manager) { center.HorizontalAlignment = HorizontalAlignment.Center; foreach (var child in ((StackPanel)center.Child!).Children) { if (child is CTextBlock cbox) { cbox.HorizontalAlignment = HorizontalAlignment.Center; } } } internal static IEnumerable Load(string resourcePath) { var asm = typeof(TypicalBlockParser).Assembly; using var stream = asm.GetManifestResourceStream(resourcePath); if (stream is null) throw new ArgumentException($"resource not found: '{resourcePath}'"); using var reader = new StreamReader(stream!); while (reader.ReadLine() is string line) { if (line.StartsWith("#")) continue; var elements = line.Split('|').Select(t => t.Trim()).ToArray(); yield return new TypicalParseInfo(elements); } } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers/UnorderListParser.cs ================================================ using HtmlAgilityPack; using System.Collections.Generic; using Markdown.Avalonia.Html.Core.Utils; using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; using ColorTextBlock.Avalonia; namespace Markdown.Avalonia.Html.Core.Parsers { public class UnorderListParser : IBlockTagParser { public IEnumerable SupportTag => new[] { "ul" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var list = new Grid(); list.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); list.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star))); int index = 0; foreach (var listItemTag in node.ChildNodes.CollectTag("li")) { var itemContent = manager.ParseChildrenAndGroup(listItemTag); var markerTxt = new CTextBlock("・"); markerTxt.TextAlignment = TextAlignment.Right; markerTxt.TextWrapping = TextWrapping.NoWrap; markerTxt.Classes.Add(global::Markdown.Avalonia.Markdown.ListMarkerClass); var item = CreateItem(itemContent); list.RowDefinitions.Add(new RowDefinition()); list.Children.Add(markerTxt); list.Children.Add(item); Grid.SetRow(markerTxt, index); Grid.SetColumn(markerTxt, 0); Grid.SetRow(item, index); Grid.SetColumn(item, 1); ++index; } generated = new[] { list }; return true; } private StackPanel CreateItem(IEnumerable children) { var panel = new StackPanel() { Orientation = Orientation.Vertical }; foreach (var child in children) panel.Children.Add(child); return panel; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers.MarkdigExtensions/FigureParser.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Layout; using HtmlAgilityPack; using Markdown.Avalonia.Html.Core.Utils; using System; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Core.Parsers.MarkdigExtensions { public class FigureParser : IBlockTagParser { public IEnumerable SupportTag => new[] { "figure" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var captionPair = node.ChildNodes .SkipComment() .Filter(nd => string.Equals(nd.Name, "figcaption", StringComparison.OrdinalIgnoreCase)); var captionList = captionPair.Item1; var contentList = captionPair.Item2; var captionBlock = captionList.SelectMany(c => manager.Grouping(manager.ParseBlockAndInline(c))); var contentBlock = contentList.SelectMany(c => manager.Grouping(manager.ParseChildrenJagging(c))); var section = new DockPanel() { LastChildFill = true }; section.Tag = Tags.TagFigure.GetClass(); foreach (var caption in captionBlock) { DockPanel.SetDock(caption, Dock.Top); section.Children.Add(caption); } var contentPanel = new StackPanel() { Orientation = Orientation.Vertical }; foreach (var content in contentBlock) { contentPanel.Children.Add(content); } section.Children.Add(contentPanel); generated = new[] { section }; return false; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Parsers.MarkdigExtensions/GridTableParser.cs ================================================ using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Markdown.Avalonia.Html.Core.Utils; using Avalonia; using Avalonia.Controls; using Markdown.Avalonia.Html.Tables; using Avalonia.Layout; namespace Markdown.Avalonia.Html.Core.Parsers.MarkdigExtensions { public class GridTableParser : IBlockTagParser, IHasPriority { public int Priority => HasPriority.DefaultPriority + 1000; public IEnumerable SupportTag => new[] { "table" }; bool ITagParser.TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var rtn = TryReplace(node, manager, out var list); generated = list; return rtn; } public bool TryReplace(HtmlNode node, ReplaceManager manager, out IEnumerable generated) { var table = new Table(); var theadRows = node.SelectNodes("./thead/tr"); if (theadRows is not null) { var group = CreateRowGroup(theadRows, manager); SetupClass(group, Tags.TagTableHeader.GetClass()); table.RowGroups.AddRange(group); } var tbodyRows = new List(); foreach (var child in node.ChildNodes) { if (string.Equals(child.Name, "tr", StringComparison.OrdinalIgnoreCase)) tbodyRows.Add(child); if (string.Equals(child.Name, "tbody", StringComparison.OrdinalIgnoreCase)) tbodyRows.AddRange(child.ChildNodes.CollectTag("tr")); } if (tbodyRows.Count > 0) { var group = CreateRowGroup(tbodyRows, manager); table.RowGroups.AddRange(group); int idx = 0; foreach (var row in group) { var useTag = (++idx & 1) == 0 ? Tags.TagEvenTableRow : Tags.TagOddTableRow; var clsNm = useTag.GetClass(); foreach (var cell in row) cell.Content.Classes.Add(clsNm); } } var tfootRows = node.SelectNodes("./tfoot/tr"); if (tfootRows is not null) { var group = CreateRowGroup(tfootRows, manager); SetupClass(group, Tags.TagTableFooter.GetClass()); table.RowGroups.AddRange(group); } ParseColumnStyle(node, table); table.Structure(); // table var grid = new Grid(); // table columns grid.ColumnDefinitions = new AutoScaleColumnDefinitions(table.ColumnLengths, grid); // table rows int rowIdx = 0; foreach (var row in table.RowGroups) { grid.RowDefinitions.Add(new RowDefinition()); foreach (var cell in row) { grid.Children.Add(cell.Content); Grid.SetRow(cell.Content, rowIdx); Grid.SetColumn(cell.Content, cell.ColumnIndex); Grid.SetRowSpan(cell.Content, cell.RowSpan); Grid.SetColumnSpan(cell.Content, cell.ColSpan); } ++rowIdx; } grid.Classes.Add(global::Markdown.Avalonia.Markdown.TableClass); var border = new Border(); border.Child = grid; border.Classes.Add(global::Markdown.Avalonia.Markdown.TableClass); if (table.ColumnLengths.All(l => l.Unit != LengthUnit.Percent)) { border.Classes.Add("TightTable"); } var captions = node.SelectNodes("./caption"); if (captions is not null) { var section = new DockPanel() { LastChildFill = true }; foreach (var captionNode in captions) { foreach (var caption in manager.ParseChildrenAndGroup(captionNode)) { DockPanel.SetDock(caption, Dock.Top); caption.Classes.Add(Tags.TagTableCaption.GetClass()); section.Children.Add(caption); } } section.Children.Add(border); generated = new[] { section }; } else { generated = new[] { border }; } return true; } private static void SetupClass(List> group, string cls) { foreach (var row in group) foreach (var cell in row) cell.Content.Classes.Add(cls); } private static void ParseColumnStyle(HtmlNode tableTag, Table table) { var colHolder = tableTag.ChildNodes.HasOneTag("colgroup", out var colgroup) ? colgroup! : tableTag; foreach (var col in colHolder.ChildNodes.CollectTag("col")) { var styleAttr = col.Attributes["style"]; if (styleAttr is null) continue; var mch = Regex.Match(styleAttr.Value, "width[ \t]*:[ \t]*([^;\"]+)(%|em|ex|mm|cm|in|pt|pc|)"); if (!mch.Success) continue; if (!Length.TryParse(mch.Groups[1].Value + mch.Groups[2].Value, out var length)) continue; table.ColumnLengths.Add(length.Unit switch { Unit.Percentage => new LengthInfo(length.Value, LengthUnit.Percent), _ => new LengthInfo(length.ToPoint(), LengthUnit.Pixel) }); } } private static List> CreateRowGroup( IEnumerable rows, ReplaceManager manager) { var group = new List>(); foreach (var rowTag in rows) { var row = new List(); foreach (var cellTag in rowTag.ChildNodes.CollectTag("td", "th")) { var cell = new TableCell(manager.ParseChildrenAndGroup(cellTag)); int colspan = TryParse(cellTag.Attributes["colspan"]?.Value); int rowspan = TryParse(cellTag.Attributes["rowspan"]?.Value); cell.RowSpan = rowspan; cell.ColSpan = colspan; row.Add(cell); } group.Add(row); } return group; static int TryParse(string? txt) => int.TryParse(txt, out var v) ? v : 1; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/ReplaceManager.cs ================================================ using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Windows.Input; using Markdown.Avalonia.Html.Core.Parsers; using Markdown.Avalonia.Html.Core.Utils; using Markdown.Avalonia.Html.Core.Parsers.MarkdigExtensions; using System.Linq; using System.Text; using Markdown.Avalonia; using Avalonia.Controls; using Avalonia; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.SyntaxHigh; namespace Markdown.Avalonia.Html.Core { public class ReplaceManager { private readonly Dictionary> _inlineBindParsers; private readonly Dictionary> _blockBindParsers; private readonly Dictionary> _bindParsers; private TextNodeParser textParser; public ReplaceManager(SyntaxHighlight highlight, SetupInfo info) { _inlineBindParsers = new(); _blockBindParsers = new(); _bindParsers = new(); UnknownTags = UnknownTagsOption.Drop; Register(new TagIgnoreParser()); Register(new CommentParsre()); Register(new ImageParser(info)); Register(new CodeBlockParser(highlight)); //Register(new CodeSpanParser()); Register(new OrderListParser()); Register(new UnorderListParser()); Register(textParser = new TextNodeParser()); Register(new HorizontalRuleParser()); Register(new FigureParser()); Register(new GridTableParser()); Register(new InputParser()); Register(new ButtonParser()); Register(new TextAreaParser()); Register(new ProgressParser()); Register(new DetailsParser()); foreach (var parser in TypicalBlockParser.Load()) Register(parser); foreach (var parser in TypicalInlineParser.Load()) Register(parser); } public IEnumerable InlineTags => _inlineBindParsers.Keys.Where(tag => !tag.StartsWith("#")); public IEnumerable BlockTags => _blockBindParsers.Keys.Where(tag => !tag.StartsWith("#")); public bool MaybeSupportBodyTag(string tagName) => _blockBindParsers.ContainsKey(tagName.ToLower()); public bool MaybeSupportInlineTag(string tagName) => _inlineBindParsers.ContainsKey(tagName.ToLower()); public UnknownTagsOption UnknownTags { get; set; } public IMarkdownEngine Engine { get; set; } public ICommand? HyperlinkCommand => Engine.HyperlinkCommand; public string? AssetPathRoot => Engine.AssetPathRoot; public void Register(ITagParser parser) { if (parser is IInlineTagParser inlineParser) { PrivateRegister(inlineParser, _inlineBindParsers); } if (parser is IBlockTagParser blockParser) { PrivateRegister(blockParser, _blockBindParsers); } PrivateRegister(parser, _bindParsers); static void PrivateRegister(T parser, Dictionary> bindParsers) where T : ITagParser { foreach (var tag in parser.SupportTag) { if (!bindParsers.TryGetValue(tag.ToLower(), out var list)) { list = new(); bindParsers.Add(tag.ToLower(), list); } int parserPriority = GetPriority(parser); int i = 0; int count = list.Count; for (; i < count; ++i) if (parserPriority <= GetPriority(list[i])) break; list.Insert(i, parser); } } static int GetPriority(object? p) => p is IHasPriority prop ? prop.Priority : HasPriority.DefaultPriority; } /// /// Convert a html tag list to an element of markdown. /// public IEnumerable Parse(string htmldoc) { var doc = new HtmlDocument(); doc.LoadHtml(htmldoc); return Parse(doc); } /// /// Convert a html tag list to an element of markdown. /// public IEnumerable Parse(HtmlDocument doc) { var contents = new List(); var head = PickBodyOrHead(doc.DocumentNode, "head"); if (head is not null) contents.AddRange(head.ChildNodes.SkipComment()); var body = PickBodyOrHead(doc.DocumentNode, "body"); if (body is not null) contents.AddRange(body.ChildNodes.SkipComment()); if (contents.Count == 0) { var root = doc.DocumentNode.ChildNodes.SkipComment(); if (root.Count == 1 && string.Equals(root[0].Name, "html", StringComparison.OrdinalIgnoreCase)) contents.AddRange(root[0].ChildNodes.SkipComment()); else contents.AddRange(root); } var jaggingResult = ParseJagging(contents); return Grouping(jaggingResult); } /// /// Convert html tag children to an element of markdown. /// Inline elements are aggreated into paragraph. /// public IEnumerable ParseChildrenAndGroup(HtmlNode node) { var jaggingResult = ParseChildrenJagging(node); return Grouping(jaggingResult); } /// /// Convert html tag children to an element of markdown. /// this result contains a block element and an inline element. /// public IEnumerable ParseChildrenJagging(HtmlNode node) { return ParseChildrenJagigng(node.ChildNodes); } public IEnumerable ParseChildrenJagigng(IEnumerable nodes) { // search empty line var empNd = nodes.Select((nd, idx) => new { Node = nd, Index = idx }) .Where(tpl => tpl.Node is HtmlTextNode) .Select(tpl => new { NodeIndex = tpl.Index, TextIndex = tpl.Node.InnerText.IndexOf("\n\n") }) .FirstOrDefault(tpl => tpl.TextIndex != -1); if (empNd is null) { return ParseJagging(nodes); } else { return ParseJaggingAndRunBlockGamut(nodes, empNd.NodeIndex, empNd.TextIndex); } } /// /// Convert a html tag to an element of markdown. /// this result contains a block element and an inline element. /// private IEnumerable ParseJagging(IEnumerable nodes) { bool isPrevBlock = true; StyledElement? lastElement = null; foreach (var node in nodes) { if (node.IsComment()) continue; // remove blank text between the blocks. if (isPrevBlock && node is HtmlTextNode txt && String.IsNullOrWhiteSpace(txt.Text)) continue; foreach (var element in ParseBlockAndInline(node)) { lastElement = element; yield return element; } isPrevBlock = lastElement is Control; } } private IEnumerable ParseJaggingAndRunBlockGamut(IEnumerable nodes, int nodeIdx, int textIdx) { var parseTargets = new List(); var textBuf = new StringBuilder(); var mdTextBuf = new StringBuilder(); foreach (var tpl in nodes.Select((value, i) => new { Node = value, Index = i })) { if (tpl.Index < nodeIdx) { parseTargets.Add(tpl.Node); } else if (tpl.Index == nodeIdx) { var nodeText = tpl.Node.InnerText; textBuf.Append(nodeText.Substring(0, textIdx)); mdTextBuf.Append(nodeText.Substring(textIdx + 2)); } else { mdTextBuf.Append(tpl.Node.OuterHtml); } } foreach (var elm in ParseJagging(parseTargets)) yield return elm; foreach (var elm in textParser.Replace(textBuf.ToString(), this)) yield return elm; foreach (var elm in Engine.RunBlockGamut(mdTextBuf.ToString(), ParseStatus.Init)) yield return elm; } /// /// Convert a html tag to an element of markdown. /// Only tag node and text node are accepted. /// /// /// public IEnumerable ParseBlockAndInline(HtmlNode node) { if (_bindParsers.TryGetValue(node.Name.ToLower(), out var binds)) { foreach (var bind in binds) { if (bind.TryReplace(node, this, out var parsed)) { return parsed; } } } return UnknownTags switch { UnknownTagsOption.PassThrough => HtmlUtils.IsBlockTag(node.Name) ? new[] { new CTextBlock(new CRun() { Text = node.OuterHtml }) } : new[] { new CRun() { Text = node.OuterHtml } }, UnknownTagsOption.Drop => EnumerableExt.Empty(), UnknownTagsOption.Bypass => ParseJagging(node.ChildNodes), _ => throw new UnknownTagException(node) }; } public IEnumerable ParseBlock(string html) { var doc = new HtmlDocument(); doc.LoadHtml(html); foreach (var node in doc.DocumentNode.ChildNodes) foreach (var block in ParseBlock(node)) yield return block; } public IEnumerable ParseInline(string html) { var doc = new HtmlDocument(); doc.LoadHtml(html); foreach (var node in doc.DocumentNode.ChildNodes) foreach (var inline in ParseInline(node)) yield return inline; } public IEnumerable ParseBlock(HtmlNode node) { if (_blockBindParsers.TryGetValue(node.Name.ToLower(), out var binds)) { foreach (var bind in binds) { if (bind.TryReplace(node, this, out var parsed)) { return parsed; } } } return UnknownTags switch { UnknownTagsOption.PassThrough => new[] { new CTextBlock(new CRun() { Text = node.OuterHtml }) }, UnknownTagsOption.Drop => EnumerableExt.Empty(), UnknownTagsOption.Bypass => node.ChildNodes .SkipComment() .SelectMany(nd => ParseBlock(nd)), _ => throw new UnknownTagException(node) }; } public IEnumerable ParseInline(HtmlNode node) { if (_inlineBindParsers.TryGetValue(node.Name.ToLower(), out var binds)) { foreach (var bind in binds) { if (bind.TryReplace(node, this, out var parsed)) { return parsed; } } } return UnknownTags switch { UnknownTagsOption.PassThrough => new[] { new CRun() { Text = node.OuterHtml } }, UnknownTagsOption.Drop => EnumerableExt.Empty(), UnknownTagsOption.Bypass => node.ChildNodes .SkipComment() .SelectMany(nd => ParseInline(nd)), _ => throw new UnknownTagException(node) }; } /// /// Convert IMdElement to IMdBlock. /// Inline elements are aggreated into paragraph. /// public IEnumerable Grouping(IEnumerable elements) { static CTextBlock? Group(IList inlines) { // trim whiltepace plain while (inlines.Count > 0) { if (inlines[0] is CRun run && String.IsNullOrWhiteSpace(run.Text)) { inlines.RemoveAt(0); } else break; } while (inlines.Count > 0) { if (inlines[inlines.Count - 1] is CRun run && String.IsNullOrWhiteSpace(run.Text)) { inlines.RemoveAt(inlines.Count - 1); } else break; } using (var list = inlines.GetEnumerator()) { CInline? prev = null; if (list.MoveNext()) { prev = list.Current; DocUtils.TrimStart(prev); while (list.MoveNext()) { var now = list.Current; if (now is CLineBreak) { DocUtils.TrimEnd(prev); if (list.MoveNext()) { now = list.Current; DocUtils.TrimStart(now); } } prev = now; } } if (prev is not null) DocUtils.TrimEnd(prev); } if (inlines.Count > 0) { var para = new CTextBlock(); para.Content.AddRange(inlines); return para; } return null; } List stored = new(); foreach (var e in elements) { if (e is CInline inline) { stored.Add(inline); continue; } // grouping inlines if (stored.Count != 0) { var para = Group(stored); if (para is not null) yield return para; stored.Clear(); } yield return (Control)e; } if (stored.Count != 0) { var para = Group(stored); if (para is not null) yield return para; stored.Clear(); } } private static HtmlNode? PickBodyOrHead(HtmlNode documentNode, string headOrBody) { // html? foreach (var child in documentNode.ChildNodes) { if (child.Name == HtmlNode.HtmlNodeTypeNameText || child.Name == HtmlNode.HtmlNodeTypeNameComment) continue; switch (child.Name.ToLower()) { case "html": // body? head? foreach (var descendants in child.ChildNodes) { if (descendants.Name == HtmlNode.HtmlNodeTypeNameText || descendants.Name == HtmlNode.HtmlNodeTypeNameComment) continue; switch (descendants.Name.ToLower()) { case "head": if (headOrBody == "head") return descendants; break; case "body": if (headOrBody == "body") return descendants; break; default: return null; } } break; case "head": if (headOrBody == "head") return child; break; case "body": if (headOrBody == "body") return child; break; default: return null; } } return null; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Tags.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Engine = Markdown.Avalonia.Markdown; namespace Markdown.Avalonia.Html.Core { public enum Tags { TagTable, TagTableHeader, TagTableBody, TagEvenTableRow, TagOddTableRow, TagTableFooter, TagTableCaption, TagBlockquote, TagBold, TagCite, TagFooter, TagItalic, TagMark, TagStrikethrough, TagSubscript, TagSuperscript, TagUnderline, TagHyperlink, TagFigure, TagRuleSingle, TagHeading1, TagHeading2, TagHeading3, TagHeading4, TagHeading5, TagHeading6, TagCodeSpan, TagCodeBlock, TagAddress, TagArticle, TagAside, TagCenter, TagAbbr, TagBdi } public static class TagsExt { public static string GetClass(this Tags tag) { return tag switch { Tags.TagHeading1 => Engine.Heading1Class, Tags.TagHeading2 => Engine.Heading2Class, Tags.TagHeading3 => Engine.Heading3Class, Tags.TagHeading4 => Engine.Heading4Class, Tags.TagHeading5 => Engine.Heading5Class, Tags.TagHeading6 => Engine.Heading6Class, Tags.TagTable => Engine.TableClass, Tags.TagTableHeader => Engine.TableHeaderClass, Tags.TagEvenTableRow => Engine.TableRowEvenClass, Tags.TagOddTableRow => Engine.TableRowOddClass, Tags.TagTableFooter => Engine.TableFooterClass, Tags.TagBlockquote => Engine.BlockquoteClass, Tags.TagCodeBlock => Engine.CodeBlockClass, _ => tag.ToString().Substring(3) }; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/TextRange.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.Html.Core { public struct TextRange { public int Start { get; } public int End { get; } public int Length => End - Start; public TextRange(int start, int end) { Start = start; End = end; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/UnknownTagException.cs ================================================ using HtmlAgilityPack; using System; namespace Markdown.Avalonia.Html.Core { /// /// MarkdownFromHtml can not convert a certain tag. /// This exception is thrown when is set to . /// public class UnknownTagException : Exception { /// /// Tag name that could not be converted /// public string TagName { get; } /// /// Tag that could not be converted /// public string Content { get; } public UnknownTagException(HtmlNode node) : base($"unknown tag: {node.Name}") { TagName = node.Name; Content = node.OuterHtml; } } } ================================================ FILE: Markdown.Avalonia.Html/Core/UnknownTagsOption.cs ================================================ namespace Markdown.Avalonia.Html.Core { /// /// Behavior options about unknown tag. /// public enum UnknownTagsOption { /// /// Unknown tag is outputed as is. /// PassThrough, /// /// Unknown tag is removed from the result. /// Drop, /// /// The unknown tag itself is ignored. /// Only the content of the tag is evaluated. /// Bypass, /// /// Throw UnknownTagException. /// Raise, } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/DocUtils.cs ================================================ using Avalonia.Controls; using Avalonia.Layout; using AvaloniaEdit; using ColorTextBlock.Avalonia; using Markdown.Avalonia.SyntaxHigh; using Markdown.Avalonia.SyntaxHigh.Extensions; using System; using System.Linq; using System.Windows; using System.Windows.Input; namespace Markdown.Avalonia.Html.Core.Utils { static class DocUtils { public static Control CreateCodeBlock(string? lang, string code, ReplaceManager manager, SyntaxHighlightProvider provider) { var txtEdit = new TextEditor(); if (!String.IsNullOrEmpty(lang)) { txtEdit.Tag = lang; txtEdit.SetValue(SyntaxHighlightWrapperExtension.ProviderProperty, provider); } txtEdit.Text = code; txtEdit.HorizontalAlignment = HorizontalAlignment.Stretch; txtEdit.IsReadOnly = true; var result = new Border(); result.Classes.Add(Tags.TagCodeBlock.GetClass()); result.Child = txtEdit; return result; } public static void TrimStart(CInline? inline) { if (inline is null) return; if (inline is CSpan span) { TrimStart(span.Content.FirstOrDefault()); } else if (inline is CRun run) { run.Text = run.Text.TrimStart(); } } public static void TrimEnd(CInline? inline) { if (inline is null) return; if (inline is CSpan span) { TrimEnd(span.Content.LastOrDefault()); } else if (inline is CRun run) { run.Text = run.Text.TrimEnd(); } } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/EnumerableExt.cs ================================================ using System.Collections; using System.Collections.Generic; namespace Markdown.Avalonia.Html.Core.Utils { internal static class EnumerableExt { public static bool TryCast(this IEnumerable list, out List casts) { casts = new List(); foreach (var e in list) { if (e is T t) casts.Add(t); else return false; } return true; } public static T[] Empty() => EmptyArray.Value; } internal class EmptyArray { // net45 dosen't have Array.Empty() #pragma warning disable CA1825 public static T[] Value = new T[0]; #pragma warning restore CA1825 } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/HtmlUtils.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Html.Core.Utils { class HtmlUtils { private static readonly HashSet s_blockTags = new() { "address", "article", "aside", "base", "basefont", "blockquote", "caption", "center", "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "iframe", "legend", "li", "link", "main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup", "option", "p", "param", "pre", "script", "section", "source", "style", "summary", "table", "textarea", "tbody", "td", "tfoot", "th", "thead", "title", "tr", "track", "ul", }; public static bool IsBlockTag(string tagName) => s_blockTags.Contains(tagName.ToLower()); } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/Length.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Html.Core.Utils { internal class Length { public double Value { get; } public Unit Unit { get; } public Length(double value, Unit unit) { Value = value; Unit = unit; } public double ToPoint() { return Unit switch { Unit.Percentage => throw new InvalidOperationException("Percentage canot convert point"), Unit.em => Value * 11, Unit.ex => Value * 11 / 2, Unit.QuarterMillimeters => Value * 3.77952755905512 / 4, Unit.Millimeters => Value * 3.77952755905512, Unit.Centimeters => Value * 37.7952755905512, Unit.Inches => Value * 96.0, Unit.Points => Value * 1.33333333333333, Unit.Picas => Value * 16, Unit.Pixels => Value, _ => throw new NotSupportedException("") }; } public static bool TryParse(string? text, #if NET6_0_OR_GREATER [MaybeNullWhen(false)] out Length rslt) #else out Length rslt) #endif { if (String.IsNullOrEmpty(text)) goto failParse; var mch = Regex.Match(text, @"^([0-9\.\+\-eE]+)(%|em|ex|mm|Q|cm|in|pt|pc|px|)$"); if (!mch.Success) goto failParse; var numTxt = mch.Groups[1].Value.Trim(); var unitTxt = mch.Groups[2].Value; if (!double.TryParse(numTxt, out var numVal)) goto failParse; var unitEnm = unitTxt switch { "%" => Unit.Percentage, "em" => Unit.em, "ex" => Unit.ex, "mm" => Unit.Millimeters, "Q" => Unit.QuarterMillimeters, "cm" => Unit.Centimeters, "in" => Unit.Inches, "pt" => Unit.Points, "pc" => Unit.Picas, "px" => Unit.Pixels, "" => Unit.Pixels, _ => Unit.Pixels, }; rslt = new Length(numVal, unitEnm); return true; failParse: rslt = null; return false; } } internal enum Unit { Percentage, em, ex, QuarterMillimeters, Millimeters, Centimeters, Inches, // pt: 1/72 in Points, // pc: 1/6 in Picas, // px; 1/96 in Pixels } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/NodeCollectionExt.cs ================================================ using HtmlAgilityPack; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Markdown.Avalonia.Html.Core.Utils { internal static class NodeCollectionExt { public static List SkipComment(this HtmlNodeCollection list) { var count = list.Count; var store = new List(count); for (var i = 0; i < count; ++i) { var e = list[i]; if (e.IsComment()) continue; store.Add(e); } return store; } public static bool IsElement(this HtmlNode node, string tagName) { return node.NodeType == HtmlNodeType.Element && string.Equals(node.Name, tagName, StringComparison.OrdinalIgnoreCase); } public static bool IsComment(this HtmlNode node) => node is HtmlCommentNode; public static List CollectTag(this HtmlNodeCollection list) { var count = list.Count; var store = new List(count); for (var i = 0; i < count; ++i) { var e = list[i]; if (e.NodeType != HtmlNodeType.Element) continue; store.Add(e); } return store; } public static List CollectTag(this HtmlNodeCollection list, string tagName) { var count = list.Count; var store = new List(count); for (var i = 0; i < count; ++i) { var e = list[i]; if (e.IsElement(tagName)) store.Add(e); } return store; } public static List CollectTag(this HtmlNodeCollection list, params string[] tagNames) { var count = list.Count; var store = new List(count); for (var i = 0; i < count; ++i) { var e = list[i]; if (e.NodeType != HtmlNodeType.Element) continue; if (tagNames.Any(tagName => e.IsElement(tagName))) store.Add(e); } return store; } public static bool HasOneTag( this HtmlNodeCollection list, string tagName, #if NET6_0_OR_GREATER [MaybeNullWhen(false)] out HtmlNode child) #else out HtmlNode child) #endif { var children = CollectTag(list, tagName); if (children.Count == 1) { child = children[0]; return true; } else { child = null!; return false; } } public static bool TryCastTextNode(this HtmlNodeCollection list, out List texts) { var count = list.Count; texts = new List(count); for (var i = 0; i < count; ++i) { var e = list[i]; if (e is HtmlTextNode txtNd) { texts.Add(txtNd); continue; } if (e.IsComment()) { continue; } return false; } return true; } public static Tuple, List> Filter(this IEnumerable list, Func filterFunc) { var filterIn = new List(); var filterOut = new List(); foreach (var e in list) { if (filterFunc(e)) { filterIn.Add(e); } else { filterOut.Add(e); } } return Tuple.Create(filterIn, filterOut); } } } ================================================ FILE: Markdown.Avalonia.Html/Core/Utils/StringExt.cs ================================================ using System; using System.Net; namespace Markdown.Avalonia.Html.Core.Utils { internal static class StringExt { public static string[] SplitLine(this string text) => text.Split('\n'); internal static bool TryDecode(this string text, ref int start, out string decoded) { // max length of entity is 33 (∳) var hit = text.IndexOf(';', start, Math.Min(text.Length - start, 40)); if (hit == -1) { decoded = string.Empty; return false; } var entity = text.Substring(start, hit - start + 1); decoded = WebUtility.HtmlDecode(entity); start = hit; if (decoded == "<" && start + 1 < text.Length) { var c = text[start + 1]; if ('a' <= c && c <= 'z' && 'A' <= c && c <= 'Z') { // '<[a-zA-Z]' may be treated as tag decoded = entity; } } return true; } } } ================================================ FILE: Markdown.Avalonia.Html/HtmlBlockParser.cs ================================================ using Markdown.Avalonia.Html.Core; using System.Collections.Generic; using System.Text.RegularExpressions; using Markdown.Avalonia.Parsers; using Markdown.Avalonia; using Avalonia.Controls; using Markdown.Avalonia.SyntaxHigh; using Markdown.Avalonia.Plugins; namespace Markdown.Avalonia.Html { public class HtmlBlockParser : BlockParser { private static readonly Regex s_emptyLine = new Regex("\n{2,}", RegexOptions.Compiled); private static readonly Regex s_headTagPattern = new(@"^<[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_tagPattern = new(@"<(?'close'/?)[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); private ReplaceManager _replacer; public HtmlBlockParser(SyntaxHighlight highlight, SetupInfo info) : base(s_headTagPattern, nameof(HtmlBlockParser)) { _replacer = new ReplaceManager(highlight, info); } public override IEnumerable Convert( string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = SimpleHtmlUtils.SearchTagRangeContinuous(text, firstMatch); _replacer.Engine = engine; var textchip = text.Substring(parseTextBegin, parseTextEnd - parseTextBegin); return _replacer.Parse(textchip); } } } ================================================ FILE: Markdown.Avalonia.Html/HtmlInlineParser.cs ================================================ using ColorTextBlock.Avalonia; using Markdown.Avalonia.Html.Core; using Markdown.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.SyntaxHigh; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Html { public class HtmlInlineParser : InlineParser { private readonly ReplaceManager _replacer; public HtmlInlineParser(SyntaxHighlight highlight, SetupInfo info) : this(new ReplaceManager(highlight, info)) { } private HtmlInlineParser(ReplaceManager replacer) : base(SimpleHtmlUtils.CreateTagstartPattern(replacer.InlineTags), nameof(HtmlInlineParser)) { _replacer = replacer; FirstMatchPattern = SimpleHtmlUtils.CreateTagstartPattern(_replacer.InlineTags); } public Regex FirstMatchPattern { get; } public override IEnumerable Convert( string text, Match firstMatch, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = SimpleHtmlUtils.SearchTagRange(text, firstMatch); _replacer.Engine = engine; return _replacer.ParseInline(text.Substring(parseTextBegin, parseTextEnd - parseTextBegin)); } } } ================================================ FILE: Markdown.Avalonia.Html/HtmlPlugin.cs ================================================ using Markdown.Avalonia.Plugins; using Markdown.Avalonia.SyntaxHigh; using System; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html { public class HtmlPlugin : IMdAvPluginRequestAnother { private SyntaxHighlight? _syntax; public IEnumerable DependsOn => new[] { typeof(SyntaxHighlight) }; public void Inject(IEnumerable plugin) { _syntax = (SyntaxHighlight)plugin.First(); } public void Setup(SetupInfo info) { if (_syntax is null) throw new InvalidOperationException("SyntaxHighlight requried"); var _block = new HtmlBlockParser(_syntax, info); var _inline = new HtmlInlineParser(_syntax, info); info.EnableNoteBlock = false; info.RegisterTop(_block); info.Register(_inline); } } } ================================================ FILE: Markdown.Avalonia.Html/Markdown.Avalonia.Html.csproj ================================================ Library $(PackageTargetFrameworks) Markdown.Avalonia.Html $(PackageVersion) whistyun html tag processor for Markdown.Avalonia © Simon Baynes 2013; whistyun 2023 https://github.com/whistyun/Markdown.Avalonia Markdown.Avalonia.Html.md MIT 9 enable ================================================ FILE: Markdown.Avalonia.Html/SimpleHtmlUtils.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Html { internal static class SimpleHtmlUtils { private static readonly HashSet s_emptyList = new(new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", }); private static readonly Regex s_tagPattern = new(@"<(?'close'/?)[\t ]*(?'tagname'[a-z][a-z0-9]*)(?'attributes'[ \t][^>]*|/)?>", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex s_emptylinePattern = new(@"\n{2}", RegexOptions.Compiled); public static Regex CreateTagstartPattern(IEnumerable tags) { var taglist = string.Join("|", tags); return new Regex(@$"<[\t ]*(?'tagname'{taglist})(?'attributes'[ \t][^>]*|/)?>", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.IgnoreCase); } public static int SearchTagRange(string text, Match tagStartPatternMatch) { int searchStart = tagStartPatternMatch.Index + tagStartPatternMatch.Length; if (tagStartPatternMatch.Value.EndsWith("/>")) { return searchStart; } else { int end = SearchTagEnd(text, searchStart, tagStartPatternMatch.Groups["tagname"].Value); return end == -1 ? text.Length : end; } } public static int SearchTagRangeContinuous(string text, Match tagStartPatternMatch) { int idx = SearchTagRange(text, tagStartPatternMatch); for (; ; ) { if (text.Length - 1 <= idx) return idx; var emp = s_emptylinePattern.Match(text, idx); if (!emp.Success) return text.Length - 1; var tag = s_tagPattern.Match(text, idx); if (tag.Success && tag.Index < emp.Index) { idx = SearchTagRange(text, tag); } else return emp.Index; } } public static int SearchTagEnd(string text, int start, string startTagName) { var tags = new Stack(); tags.Push(startTagName); for (; ; ) { var isEmptyTag = s_emptyList.Contains(tags.Peek()); var mch = s_tagPattern.Match(text, start); if (isEmptyTag && (!mch.Success || mch.Index != start)) { if (tags.Count == 1) return start; tags.Pop(); } if (!mch.Success) return -1; start = mch.Index + mch.Length; if (mch.Value.EndsWith("/>")) { continue; } var tagName = mch.Groups["tagname"].Value.ToLower(); if (!String.IsNullOrEmpty(mch.Groups["close"].Value)) { // pop until same tag name be found. while (tags.Count > 0) { var peekTag = tags.Peek(); tags.Pop(); if (peekTag == tagName) break; } if (tags.Count == 0) { return mch.Index + mch.Length; } } else { if (s_emptyList.Contains(tags.Peek())) { tags.Pop(); } tags.Push(mch.Groups["tagname"].Value); } } } } } ================================================ FILE: Markdown.Avalonia.Html/Tables/AutoScaleColumnDefinitions.cs ================================================ using Avalonia.Controls; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Security.Cryptography; namespace Markdown.Avalonia.Html.Tables { class AutoScaleColumnDefinitions : ColumnDefinitions { private readonly int _columnCount; private Grid _grid; private bool _isAutoArrangeTried = false; private readonly ReadOnlyCollection _baseLength; public AutoScaleColumnDefinitions(IEnumerable baseLength, Grid owner) { _baseLength = baseLength.ToList().AsReadOnly(); _columnCount = _baseLength.Count; _grid = owner; // To determine the required width for each column, use `Auto` foreach (var def in _baseLength) { if (def.Unit == LengthUnit.Pixel) Add(new ColumnDefinition(def.Value, GridUnitType.Pixel)); else if (def.Unit == LengthUnit.Percent) Add(new ColumnDefinition(1, GridUnitType.Pixel)); else Add(new ColumnDefinition(1, GridUnitType.Auto)); } _grid.LayoutUpdated += _grid_LayoutUpdated; } private void _grid_LayoutUpdated(object? sender, EventArgs e) { // If auto scale is disabled, use `1 *` for each column. // This is default value used for grid column width in Markdown.Avalonia. var isEnabled = global::Markdown.Avalonia.Controls.AutoScaleColumnDefinitions.GetIsEnabled(_grid); if (!isEnabled) { if (_grid.ColumnDefinitions.All(def => def.Width.IsStar && def.Width.Value == 1d)) return; _isAutoArrangeTried = false; Clear(); for (var i = 0; i < _columnCount; ++i) Add(new ColumnDefinition(1, GridUnitType.Star)); ReSetGridChildren(); } // If column width adjustement is already done, skip the following process. else { if (_isAutoArrangeTried) return; _isAutoArrangeTried = true; AdjustColumn(); } } private void AdjustColumn() { double entireWidth = _grid.Bounds.Width; double entireColumnWidth = entireWidth / _columnCount; double[] widths = MeasureColumnWidths(); int[] scaleIndics = Enumerable.Range(0, _columnCount) .Where(i => _baseLength[i].IsPercent) .ToArray(); int[] fixedIndics = Enumerable.Range(0, _columnCount) .Where(i => !_baseLength[i].IsPercent) .ToArray(); foreach (var i in scaleIndics) widths[i] = entireWidth / widths.Length / 2; double scaleWidth = scaleIndics.Sum(i => widths[i]); double wealthy = entireWidth - scaleWidth; var minColWid = entireWidth / widths.Length / 2; var allowedColWid = wealthy / fixedIndics.Length; // sum of the shortfall width for each columns, excluding extra widths. var required = fixedIndics.Sum(i => Math.Max(widths[i] - allowedColWid, 0)); if (required < 1d) { // equality arrange Clear(); for (var i = 0; i < _columnCount; ++i) { if (_baseLength[i].IsPercent) Add(new ColumnDefinition(_baseLength[i].Value, GridUnitType.Star)); else Add(new ColumnDefinition(widths[i], GridUnitType.Pixel)); } ReSetGridChildren(); } else { var units = _baseLength.Select(l => l.Unit).ToArray(); if (minColWid < allowedColWid) { CollectWidthFrom( Enumerable.Range(0, _columnCount).ToArray(), widths, units, minColWid); } else { CollectWidthFrom( fixedIndics, widths, units, allowedColWid); } // equality arrange Clear(); for (var i = 0; i < _columnCount; ++i) { if (units[i] == LengthUnit.Percent) { Add(new ColumnDefinition(_baseLength[i].Value, GridUnitType.Star)); } else { Add(new ColumnDefinition(widths[i], GridUnitType.Pixel)); } } ReSetGridChildren(); } } void CollectWidthFrom(int[] indics, double[] widths, LengthUnit[] units, double colWid) { var required = indics.Sum(i => Math.Max(widths[i] - colWid, 0)); // collect from wealthy columns var wealthy = indics.Sum(i => Math.Max(colWid - widths[i], 0)); bool isShortabe; double collect = (isShortabe = required > wealthy) ? wealthy : required; foreach (var i in indics) { if (widths[i] < colWid) { widths[i] = colWid - collect * (colWid - widths[i]) / wealthy; if (isShortabe) units[i] = LengthUnit.Pixel; } else { widths[i] = colWid + collect * (widths[i] - colWid) / required; } } } /// /// Determines the maximum requred width for each column. /// private double[] MeasureColumnWidths() { var width = new double[_columnCount]; foreach (var element in _grid.Children.OfType()) { var colidx = Grid.GetColumn(element); var colspan = Grid.GetColumnSpan(element); if (colspan == 1) { width[colidx] = Math.Max(width[colidx], element.Bounds.Width); } else { // If the cell spans multiple columns, check whether there is sufficient width available for each column. var requreWidth = element.Bounds.Width; var consumedWid = Enumerable.Range(colidx, colspan).Sum(i => width[i]); if (consumedWid >= requreWidth) continue; // If there is insufficient width, distribute the shortfall for each column. var adding = Math.Ceiling((requreWidth - consumedWid) / colspan); foreach (var i in Enumerable.Range(colidx, colspan)) { width[i] += adding; } } if (width.All(d => d != 0d)) break; } for (var i = 0; i < width.Length; ++i) width[i] = Math.Max(width[i], 1); return width; } private void ReSetGridChildren() { var backup = _grid.Children.ToArray(); _grid.Children.Clear(); _grid.Children.AddRange(backup); } } readonly struct LengthInfo { public double Value { get; } public LengthUnit Unit { get; } public bool IsPixcel => Unit == LengthUnit.Pixel; public bool IsPercent => Unit == LengthUnit.Percent; public bool IsAutoFit => Unit == LengthUnit.Auto; public LengthInfo(double v, LengthUnit u) { Value = v; Unit = u; } } enum LengthUnit { Pixel, Percent, Auto } } ================================================ FILE: Markdown.Avalonia.Html/Tables/Table.cs ================================================ using Avalonia.Controls; using System; using System.Collections.Generic; using System.Linq; namespace Markdown.Avalonia.Html.Tables { class Table { public List ColumnLengths { get; } public List> RowGroups { get; } public int ColCount { get; private set; } public int RowCount { get; private set; } public Table() { ColumnLengths = new(); RowGroups = new(); } public void Structure() { var colCntAtDetail = new List(); var maxColCntInDetails = Math.Max(1, ColumnLengths.Count); // The list of multi-row cells. // Key: Column index where the target cell is located. var multiRowsAtColIdx = new Dictionary(); for (var rowIdx = 0; rowIdx < RowGroups.Count; ++rowIdx) { List row = RowGroups[rowIdx]; var colOffset = 0; // Setup ColspanIndex of each cells in the current row. for (int colIdx = 0; colIdx < row.Count;) { int colSpan; if (multiRowsAtColIdx.TryGetValue(colOffset, out var span)) { colSpan = span.ColSpan; } else { var cell = (TableCell)row[colIdx]; cell.ColumnIndex = colOffset; colSpan = cell.ColSpan; if (cell.RowSpan > 1) { multiRowsAtColIdx[colOffset] = new MdSpan(cell.RowSpan, cell.ColSpan); } ++colIdx; } colOffset += colSpan; } // Increments end of column index by the sum of the remaining multi-row cells. colOffset += multiRowsAtColIdx .Where(ent => ent.Key >= colOffset) .Sum(ent => ent.Value.ColSpan); // Removes multi-row cells, 複数行にまたがるセルの削除(必要なら) foreach (var spanEntry in multiRowsAtColIdx.ToArray()) { if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } colCntAtDetail.Add(colOffset); maxColCntInDetails = Math.Max(maxColCntInDetails, colOffset); } // If multi-row cells remain, insert empty rows. while (multiRowsAtColIdx.Count > 0) { var row = new List(); RowGroups.Add(row); var colOffset = 0; foreach (var spanEntry in multiRowsAtColIdx.OrderBy(tpl => tpl.Key)) { while (colOffset < spanEntry.Key) { var cell = new TableCell(); cell.ColumnIndex = colOffset++; row.Add(cell); } colOffset += spanEntry.Value.ColSpan; if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } colCntAtDetail.Add(colOffset); } ColCount = maxColCntInDetails; RowCount = RowGroups.Count; // insert coldef for the shortfall var hasScaleWidCol = ColumnLengths.Any(c => c.Unit == LengthUnit.Percent); while (ColumnLengths.Count < ColCount) { if (hasScaleWidCol) ColumnLengths.Add(new LengthInfo(1, LengthUnit.Auto)); else ColumnLengths.Add(new LengthInfo(1, LengthUnit.Percent)); } // insert cell for the shortfall for (var rowIdx = 0; rowIdx < RowGroups.Count; ++rowIdx) { for (var retry = colCntAtDetail[rowIdx]; retry < ColCount; ++retry) { var cell = new TableCell(); cell.ColumnIndex = retry; RowGroups[rowIdx].Add(cell); } } } class MdSpan { public int Life { set; get; } public int ColSpan { set; get; } public MdSpan(int l, int c) { Life = l; ColSpan = c; } } } } ================================================ FILE: Markdown.Avalonia.Html/Tables/TableCell.cs ================================================ using System; using System.Text; using Avalonia.Media; using Avalonia.Layout; using System.Collections.Generic; using Avalonia.Controls; using System.Linq; namespace Markdown.Avalonia.Html.Tables { class TableCell { public int ColumnIndex { set; get; } public Border Content { get; set; } public int RowSpan { set; get; } public int ColSpan { set; get; } public TextAlignment? Horizontal { set; get; } public VerticalAlignment? Vertical { set; get; } public TableCell() { RowSpan = 1; ColSpan = 1; Horizontal = null; Vertical = null; Content = new(); } public TableCell(IEnumerable controls) : this() { var ctrls = controls.ToArray(); switch (ctrls.Length) { case 0: break; case 1: Content.Child = ctrls[0]; break; default: var panel = new StackPanel() { Orientation = Orientation.Vertical }; panel.Children.AddRange(ctrls); Content.Child = panel; break; } } } } ================================================ FILE: Markdown.Avalonia.Svg/Markdown.Avalonia.Svg.csproj ================================================  Library $(PackageTargetFrameworks) Markdown.Avalonia.Svg $(PackageVersion) grifsun; whistyun Copyright (c) 2023 grifsun, whistyun https://github.com/whistyun/Markdown.Avalonia Markdown.Avalonia.Svg.md MIT 9 enable ================================================ FILE: Markdown.Avalonia.Svg/Properties/AssemblyInfo.cs ================================================ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia.Svg", "Markdown.Avalonia.Svg")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia.Svg", "mdsvg")] ================================================ FILE: Markdown.Avalonia.Svg/SvgFormat.cs ================================================ using Markdown.Avalonia.Plugins; namespace Markdown.Avalonia.Svg { public class SvgFormat : IMdAvPlugin { public void Setup(SetupInfo info) { info.Register(new SvgImageResolver()); } } } ================================================ FILE: Markdown.Avalonia.Svg/SvgImageResolver.cs ================================================ using Avalonia.Media; using Avalonia.Svg; using Markdown.Avalonia.Utils; using System; using System.IO; using System.Reflection.Metadata; using System.Threading.Tasks; using System.Xml; namespace Markdown.Avalonia.Svg { internal class SvgImageResolver : IImageResolver { // private static readonly AvaloniaAssetLoader _svgAssetLoader = new(); public async Task Load(Stream stream) { var task = Task.Run(() => { if (IsSvgFile(stream)) { var source = SvgSource.Load(stream); var picture = source.Picture; var svgsrc = new SvgSource() { Picture = picture }; return new VectorImage() { Source = svgsrc }; } return null; }); return await task; } private static bool IsSvgFile(Stream fileStream) { try { int firstChr = fileStream.ReadByte(); if (firstChr != ('<' & 0xFF)) return false; fileStream.Seek(0, SeekOrigin.Begin); using (var xmlReader = XmlReader.Create(fileStream)) { return xmlReader.MoveToContent() == XmlNodeType.Element && "svg".Equals(xmlReader.Name, StringComparison.OrdinalIgnoreCase); } } catch { return false; } finally { fileStream.Seek(0, SeekOrigin.Begin); } } } } ================================================ FILE: Markdown.Avalonia.Svg/VectorImage.cs ================================================ using System; using System.Diagnostics; using Avalonia; using Avalonia.Media; using Avalonia.Svg; using ShimSkiaSharp; namespace Markdown.Avalonia.Svg { /// /// An that uses a for content. /// internal class VectorImage : IImage { /// /// Gets or sets the content. /// public SvgSource? Source { get; set; } /// public Size Size => Source?.Picture is { } ? new Size(Source.Picture.CullRect.Width, Source.Picture.CullRect.Height) : default; private SKPicture? _previousPicture = null; private AvaloniaPicture? _avaloniaPicture = null; /// void IImage.Draw( DrawingContext context, Rect sourceRect, Rect destRect) { var source = Source; if (source?.Picture is null) { _previousPicture = null; _avaloniaPicture?.Dispose(); _avaloniaPicture = null; return; } if (Size.Width <= 0 || Size.Height <= 0) { return; } var bounds = source.Picture.CullRect; var scaleMatrix = Matrix.CreateScale( destRect.Width / sourceRect.Width, destRect.Height / sourceRect.Height); var translateMatrix = Matrix.CreateTranslation( -sourceRect.X + destRect.X - bounds.Left, -sourceRect.Y + destRect.Y - bounds.Top); using (context.PushClip(destRect)) using (context.PushTransform(translateMatrix)) using (context.PushTransform(scaleMatrix)) { try { if (_avaloniaPicture is null || source.Picture != _previousPicture) { _previousPicture = source.Picture; _avaloniaPicture?.Dispose(); _avaloniaPicture = AvaloniaPicture.Record(source.Picture); } if (_avaloniaPicture is { }) { _avaloniaPicture.Draw(context); } } catch (Exception ex) { Debug.WriteLine($"{ex.Message}"); Debug.WriteLine($"{ex.StackTrace}"); } } } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Alias.cs ================================================ using System; namespace Markdown.Avalonia.SyntaxHigh { public class Alias { public string? Name { get; set; } private string? _realName; private Uri? _xshd; public string? RealName { get => _realName; set { _realName = value; Validation(nameof(RealName)); } } public Uri? XSHD { get => _xshd; set { _xshd = value; Validation(nameof(XSHD)); } } private void Validation(string name) { if (_realName != null && _xshd != null) { throw new ArgumentException(name); } } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/CodeBlockElement.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Layout; using AvaloniaEdit; using ColorDocument.Avalonia; using Markdown.Avalonia.SyntaxHigh.Extensions; using System; using System.Collections.Generic; using System.Text; using static System.Runtime.InteropServices.JavaScript.JSType; namespace Markdown.Avalonia.SyntaxHigh { internal class CodeBlockElement : DocumentElement { private SyntaxHighlightProvider _provider; private string _code; private Lazy _control; public override Control Control => _control.Value; public override IEnumerable Children => Array.Empty(); public CodeBlockElement(SyntaxHighlightProvider provider, string lang, string code) { _provider = provider; _code = code; _control = new Lazy(() => Create(lang, code)); } public override void ConstructSelectedText(StringBuilder stringBuilder) { stringBuilder.Append(_code); } public override void Select(Point from, Point to) { Helper?.Register(Control); } public override void UnSelect() { Helper?.Unregister(Control); } private Border Create(string lang, string code) { var langLabel = new Label() { Content = lang }; langLabel.Classes.Add("LangInfo"); var copyButton = new Button() { Content = new TextBlock() }; copyButton.Classes.Add("CopyButton"); var txtEdit = new TextEditor(); txtEdit.Tag = lang; txtEdit.SetValue(SyntaxHighlightWrapperExtension.ProviderProperty, _provider); txtEdit.Text = code; txtEdit.HorizontalAlignment = HorizontalAlignment.Stretch; txtEdit.IsReadOnly = true; copyButton.Click += (s, e) => { var item = new DataTransferItem(); item.Set(DataFormat.Text, txtEdit.Text); var data = new DataTransfer(); data.Add(item); var clipboard = TopLevel.GetTopLevel(txtEdit)?.Clipboard; clipboard?.SetDataAsync(data); }; var cdPd = new CodePad(); cdPd.Content = txtEdit; cdPd.ExandableMenu = copyButton; cdPd.AlwaysShowMenu = langLabel; var result = new Border(); result.Classes.Add(Markdown.CodeBlockClass); result.Child = cdPd; return result; } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/CodePad.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Input; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.SyntaxHigh { internal class CodePad : Panel { private bool _isMenuVisible; private Control? _content; private Control? _alwaysShowMenu; private Control? _expandableMenu; public Control? Content { get => _content; set { if (_content is not null) { Children.Remove(_content); } _content = value; if (_content is not null) { Children.Insert(0, _content); } } } public Control? ExandableMenu { get => _expandableMenu; set { if (_expandableMenu is not null) { Children.Remove(_expandableMenu); } _expandableMenu = value; if (_expandableMenu is not null && _isMenuVisible) { Children.Add(_expandableMenu); } } } public Control? AlwaysShowMenu { get => _alwaysShowMenu; set { if (_alwaysShowMenu is not null) { Children.Remove(_alwaysShowMenu); } _alwaysShowMenu = value; if (_alwaysShowMenu is not null) { Children.Add(_alwaysShowMenu); } } } public CodePad() { } protected override void OnPointerEntered(PointerEventArgs args) { if (!_isMenuVisible) { _isMenuVisible = true; if (_expandableMenu is not null) { if (!Children.Contains(_expandableMenu)) Children.Add(_expandableMenu); else { _expandableMenu.IsVisible = true; } } } } protected override void OnPointerExited(PointerEventArgs e) { if (_isMenuVisible && _expandableMenu is not null) { if (Children.Contains(_expandableMenu)) { _expandableMenu.IsVisible = false; } _isMenuVisible = false; } } protected override Size MeasureOverride(Size availableSize) { var reqSz = base.MeasureOverride(availableSize); double addHeight = _alwaysShowMenu is not null ? _alwaysShowMenu.DesiredSize.Height : 0d; double height = _expandableMenu is not null ? _expandableMenu.DesiredSize.Height : 0d; return new Size(reqSz.Width, addHeight + Math.Max(reqSz.Height, height)); } protected override Size ArrangeOverride(Size finalSize) { var yoffset = _alwaysShowMenu is not null ? _alwaysShowMenu.DesiredSize.Height : 0d; if (_content is not null) { _content.Arrange(new Rect(0, yoffset / 2, finalSize.Width, finalSize.Height)); } if (_alwaysShowMenu is not null) { var menuSz = _alwaysShowMenu.DesiredSize; _alwaysShowMenu.Arrange(new Rect(finalSize.Width - menuSz.Width, 0, menuSz.Width, menuSz.Height)); } if (_expandableMenu is not null) { var menuSz = _expandableMenu.DesiredSize; _expandableMenu.Arrange(new Rect(finalSize.Width - menuSz.Width, yoffset, menuSz.Width, menuSz.Height)); } return finalSize; } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Extensions/HSV.cs ================================================ using Avalonia.Media; using System; namespace Markdown.Avalonia.SyntaxHigh.Extensions { struct HSV { public int Hue; public byte Saturation; public byte Value; public HSV(Color color) { int max = Math.Max(color.R, Math.Max(color.G, color.B)); int min = Math.Min(color.R, Math.Min(color.G, color.B)); int div = max - min; if (div == 0) { Hue = 0; Saturation = 0; } else { Hue = (min == color.B) ? 60 * (color.G - color.R) / div + 60 : (min == color.R) ? 60 * (color.B - color.G) / div + 180 : 60 * (color.R - color.B) / div + 300; Saturation = (byte)div; } Value = (byte)max; } public Color ToColor() { if (Hue == 0 && Saturation == 0) { return Color.FromRgb(Value, Value, Value); } //byte c = Saturation; // int HueInt = Hue / 60; int x = (int)(Saturation * (1 - Math.Abs((Hue / 60f) % 2 - 1))); static Color FromRgb(int r, int g, int b) => Color.FromRgb((byte)r, (byte)g, (byte)b); return (Hue / 60) switch { 1 => FromRgb(Value - Saturation + x, Value, Value - Saturation), 2 => FromRgb(Value - Saturation, Value, Value - Saturation + x), 3 => FromRgb(Value - Saturation, Value - Saturation + x, Value), 4 => FromRgb(Value - Saturation + x, Value - Saturation, Value), 5 or 6 => FromRgb(Value, Value - Saturation, Value - Saturation + x), _ => FromRgb(Value, Value - Saturation + x, Value - Saturation), }; } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Extensions/HighlightWrapper.cs ================================================ using Avalonia.Media; using AvaloniaEdit.Highlighting; using System; using System.Collections.Generic; namespace Markdown.Avalonia.SyntaxHigh.Extensions { public class HighlightWrapper : IHighlightingDefinition { private readonly IHighlightingDefinition _baseDef; private readonly Color _foreColor; private readonly Dictionary _converted; private readonly Dictionary _namedRuleSet; private readonly Dictionary _namedColors; public HighlightWrapper(IHighlightingDefinition baseDef, Color foreColor) { _baseDef = baseDef; _foreColor = foreColor; _converted = new Dictionary(); _namedRuleSet = new Dictionary(); _namedColors = new Dictionary(); foreach (var color in baseDef.NamedHighlightingColors) { var name = color.Name; var newCol = color.Clone(); newCol.Foreground = color.Foreground is null ? null : new MixHighlightingBrush(color.Foreground, foreColor); _namedColors[name] = newCol; } MainRuleSet = Wrap(baseDef.MainRuleSet)!; } public string Name => "Re:" + _baseDef.Name; public HighlightingRuleSet MainRuleSet { get; } public IEnumerable NamedHighlightingColors => _namedColors.Values; public IDictionary Properties => _baseDef.Properties; public HighlightingColor? GetNamedColor(string name) { return _namedColors.TryGetValue(name, out var color) ? color : null; } public HighlightingRuleSet? GetNamedRuleSet(string name) { return _namedRuleSet.TryGetValue(name, out var rset) ? rset : null; } private HighlightingRuleSet? Wrap(HighlightingRuleSet ruleSet) { if (ruleSet is null) return null; if (!String.IsNullOrEmpty(ruleSet.Name) && _namedRuleSet.TryGetValue(ruleSet.Name, out var cachedRule)) return cachedRule; if (_converted.TryGetValue(ruleSet, out var cachedRule2)) return cachedRule2; var copySet = new HighlightingRuleSet() { Name = ruleSet.Name }; _converted[ruleSet] = copySet; if (!String.IsNullOrEmpty(copySet.Name)) _namedRuleSet[copySet.Name] = copySet; foreach (var baseSpan in ruleSet.Spans) { if (baseSpan is null) continue; var copySpan = new HighlightingSpan() { StartExpression = baseSpan.StartExpression, EndExpression = baseSpan.EndExpression, RuleSet = Wrap(baseSpan.RuleSet), StartColor = Wrap(baseSpan.StartColor), SpanColor = Wrap(baseSpan.SpanColor), EndColor = Wrap(baseSpan.EndColor), SpanColorIncludesStart = baseSpan.SpanColorIncludesStart, SpanColorIncludesEnd = baseSpan.SpanColorIncludesEnd, }; copySet.Spans.Add(copySpan); } foreach (var baseRule in ruleSet.Rules) { var copyRule = new HighlightingRule() { Regex = baseRule.Regex, Color = Wrap(baseRule.Color) }; copySet.Rules.Add(copyRule); } return copySet; } private HighlightingColor? Wrap(HighlightingColor color) { if (color is null) return null; if (!String.IsNullOrEmpty(color.Name) && _namedColors.TryGetValue(color.Name, out var cachedColor)) return cachedColor; var copyColor = color.Clone(); copyColor.Foreground = color.Foreground is null ? null : new MixHighlightingBrush(color.Foreground, _foreColor); if (!String.IsNullOrEmpty(copyColor.Name)) _namedColors[copyColor.Name] = copyColor; return copyColor; } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Extensions/MixHighlightingBrush.cs ================================================ using Avalonia.Media; using AvaloniaEdit.Highlighting; using AvaloniaEdit.Rendering; using System; namespace Markdown.Avalonia.SyntaxHigh.Extensions { class MixHighlightingBrush : HighlightingBrush { private readonly HighlightingBrush _baseBrush; private readonly Color _fore; public MixHighlightingBrush(HighlightingBrush baseBrush, Color fore) { this._baseBrush = baseBrush; this._fore = fore; } public override IBrush GetBrush(ITextRunConstructionContext context) { var originalBrush = _baseBrush.GetBrush(context); return (originalBrush is ISolidColorBrush sbrsh) ? new SolidColorBrush(WrapColor(sbrsh.Color)) : originalBrush; } public override Color? GetColor(ITextRunConstructionContext context) { if (_baseBrush.GetBrush(context) is ISolidColorBrush sbrsh) { return WrapColor(sbrsh.Color); } else { var colorN = this._baseBrush.GetColor(context); return colorN.HasValue ? WrapColor(colorN.Value) : colorN; } } private Color WrapColor(Color color) { if (color.A == 0) return color; var foreMax = Math.Max(_fore.R, Math.Max(_fore.G, _fore.B)); var tgtHsv = new HSV(color); int newValue = tgtHsv.Value + foreMax; int newSaturation = tgtHsv.Saturation; if (newValue > 255) { var newSaturation2 = newSaturation - (newValue - 255); newValue = 255; var sChRtLm = (color.R >= color.G && color.R >= color.B) ? 0.95f * 0.7f : (color.G >= color.R && color.G >= color.B) ? 0.95f : 0.95f * 0.5f; var sChRt = Math.Max(sChRtLm, newSaturation2 / (float)newSaturation); if (Single.IsInfinity(sChRt)) sChRt = sChRtLm; newSaturation = (int)(newSaturation * sChRt); } tgtHsv.Value = (byte)newValue; tgtHsv.Saturation = (byte)newSaturation; var newColor = tgtHsv.ToColor(); return newColor; } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Extensions/SyntaxHighlightWrapperExtension.cs ================================================ using Avalonia; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using AvaloniaEdit; using AvaloniaEdit.Highlighting; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; namespace Markdown.Avalonia.SyntaxHigh.Extensions { /// /// Change syntax color according to the Foreground color. /// /// /// This class change hue and saturation of the syntax color according to Foreground. /// This class assume that Foreground is the complementary color of Background. /// /// You may think It's better to change it according to Bachground, /// But Background may be declared as absolutly transparent. /// public class SyntaxHighlightWrapperExtension : MarkupExtension { public static readonly AvaloniaProperty ProviderProperty = AvaloniaProperty.Register("Provider"); private string ForegroundName; public SyntaxHighlightWrapperExtension(string colorKey) { this.ForegroundName = colorKey; } public override object ProvideValue(IServiceProvider serviceProvider) { var dyExt = new DynamicResourceExtension(ForegroundName); var brush = dyExt.ProvideValue(serviceProvider); var tag = new Binding(nameof(TextEditor.Tag)) { RelativeSource = new RelativeSource(RelativeSourceMode.Self) }; var provider = new Binding(ProviderProperty.Name) { RelativeSource = new RelativeSource(RelativeSourceMode.Self) }; return new MultiBinding() { Bindings = new BindingBase[] { brush, provider, tag }, Converter = new SyntaxHighlightWrapperConverter() }; } class SyntaxHighlightWrapperConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { var provider = values[1] as SyntaxHighlightProvider; var codeLang = values[2] as string; if (String.IsNullOrEmpty(codeLang)) return null; var highlight = provider is null ? HighlightingManager.Instance.GetDefinitionByExtension("." + codeLang) : provider.Solve(codeLang!); if (highlight is null) return null; Color foreColor = values[0] is SolidColorBrush cBrush ? cBrush.Color : values[0] is Color cColor ? cColor : Colors.Black; try { return new HighlightWrapper(highlight, foreColor); } catch (Exception e) { Trace.TraceError(e.ToString()); return highlight; } } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Markdown.Avalonia.SyntaxHigh.csproj ================================================  Library $(PackageTargetFrameworks) Markdown.Avalonia.SyntaxHigh $(PackageVersion) whistyun Copyright (c) 2021 whistyun https://github.com/whistyun/Markdown.Avalonia Markdown.Avalonia.SyntaxHigh.md MIT 9 enable AppendixOfDefaultTheme.axaml AppendixOfFluentAvalonia.axaml AppendixOfFluentTheme.axaml ================================================ FILE: Markdown.Avalonia.SyntaxHigh/Properties/AssemblyInfo.cs ================================================ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia.SyntaxHigh", "Markdown.Avalonia.SyntaxHigh")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia.SyntaxHigh", "mdsyntax")] [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia.SyntaxHigh/Styles", "Markdown.Avalonia.SyntaxHigh.StyleCollections")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia.SyntaxHigh/Styles", "mdsyntaxstyles")] ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfDefaultTheme.axaml ================================================ ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfDefaultTheme.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.SyntaxHigh.StyleCollections { public class AppendixOfDefaultTheme : Styles { public AppendixOfDefaultTheme() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfFluentAvalonia.axaml ================================================ ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfFluentAvalonia.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.SyntaxHigh.StyleCollections { public class AppendixOfFluentAvalonia : Styles { public AppendixOfFluentAvalonia() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfFluentTheme.axaml ================================================ ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleCollections/AppendixOfFluentTheme.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.SyntaxHigh.StyleCollections { public class AppendixOfFluentTheme : Styles { public AppendixOfFluentTheme() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/StyleEdit.cs ================================================ using Markdown.Avalonia.Plugins; using Avalonia.Styling; using Markdown.Avalonia.SyntaxHigh.StyleCollections; namespace Markdown.Avalonia.SyntaxHigh { internal class StyleEdit : IStyleEdit { public void Edit(string styleName, Styles styles) { switch (styleName) { case nameof(MarkdownStyle.SimpleTheme): styles.AddRange(new AppendixOfDefaultTheme()); break; case nameof(MarkdownStyle.FluentTheme): styles.AddRange(new AppendixOfFluentTheme()); break; } } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/SyntaxHighlightProvider.cs ================================================ using Avalonia.Platform; using Avalonia; using AvaloniaEdit.Highlighting.Xshd; using AvaloniaEdit.Highlighting; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; using System.Text; using System.Xml; using System.Linq; namespace Markdown.Avalonia.SyntaxHigh { public class SyntaxHighlightProvider { private ObservableCollection _aliases; private Dictionary _nameSolver; private Dictionary _definitions; public SyntaxHighlightProvider(ObservableCollection aliases) { _aliases = aliases; _nameSolver = new Dictionary(); _definitions = new Dictionary(); _aliases.CollectionChanged += (s, e) => AliasesCollectionChanged(e); AliasesCollectionChanged(null); } public IHighlightingDefinition Solve(string lang) { for (var i = 0; i < 10; ++i) if (_nameSolver.TryGetValue(lang.ToLower(), out var realName)) lang = realName; else break; if (_definitions.TryGetValue(lang, out var def)) return def; return HighlightingManager.Instance.GetDefinitionByExtension("." + lang); } private void AliasesCollectionChanged(NotifyCollectionChangedEventArgs? arg) { IEnumerable adding; if (arg is null || arg.OldItems != null) { _nameSolver.Clear(); _definitions.Clear(); SetupForBuiltIn(); adding = _aliases; } else if (arg?.NewItems != null) { adding = arg.NewItems.Cast(); } else adding = Array.Empty(); foreach (var alias in adding) { if (alias.Name is null) continue; if (!String.IsNullOrEmpty(alias.RealName)) { _nameSolver[alias.Name] = alias.RealName; } else if (alias.XSHD != null) { var definition = Load(alias.XSHD); if (definition is null) throw new ArgumentException($"Failed loading: {alias.XSHD}"); _definitions[alias.Name] = definition; } } } private void SetupForBuiltIn() { // https://github.com/AvaloniaUI/AvaloniaEdit/blob/master/src/AvaloniaEdit/Highlighting/Resources/Resources.cs _nameSolver["c#"] = "cs"; _nameSolver["csharp"] = "cs"; _nameSolver["javascript"] = "js"; _nameSolver["coco"] = "atg"; _nameSolver["c++"] = "c"; _nameSolver["powershell"] = "ps1"; _nameSolver["python"] = "py"; _nameSolver["markdown"] = "md"; } private IHighlightingDefinition? Load(Uri source) { switch (source.Scheme) { case "file": return File.Exists(source.LocalPath) ? Open(File.OpenRead(source.LocalPath)) : null; case "avares": return AssetLoader.Exists(source) ? Open(AssetLoader.Open(source)) : null; default: throw new ArgumentException($"unsupport scheme '{source.Scheme}'"); } IHighlightingDefinition Open(Stream stream) { try { using (var reader = XmlReader.Create(stream)) return HighlightingLoader.Load(reader, HighlightingManager.Instance); } finally { stream.Close(); } } } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/SyntaxHiglight.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using Avalonia.Metadata; using AvaloniaEdit.Highlighting; using AvaloniaEdit; using Markdown.Avalonia.Plugins; using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Avalonia; using Avalonia.Styling; using Markdown.Avalonia.SyntaxHigh.StyleCollections; using Avalonia.Collections; using System.Collections.ObjectModel; namespace Markdown.Avalonia.SyntaxHigh { public class SyntaxHighlight : IMdAvPlugin { [Content] public ObservableCollection Aliases { get; } = new ObservableCollection(); public void Setup(SetupInfo info) { info.Register(new SyntaxOverride(Aliases, info)); info.Register(new StyleEdit()); } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/SyntaxOverride.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using AvaloniaEdit; using Markdown.Avalonia.Plugins; using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Avalonia; using System.Collections.ObjectModel; using Markdown.Avalonia.SyntaxHigh.Extensions; using Markdown.Avalonia.Parsers; using System.Diagnostics; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; namespace Markdown.Avalonia.SyntaxHigh { internal class SyntaxOverride : BlockOverride2 { private SyntaxHighlightProvider _provider; private SetupInfo _info; public SyntaxOverride(ObservableCollection aliases, SetupInfo info) : base("CodeBlocksWithLangEvaluator") { _provider = new SyntaxHighlightProvider(aliases); _info = info; } public override IEnumerable? Convert2( string text, Match match, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { var closeTagPattern = new Regex($"\n[ ]*{match.Groups[1].Value}[ ]*\n"); var closeTagMatch = closeTagPattern.Match(text, match.Index + match.Length); int codeEndIndex; if (closeTagMatch.Success) { codeEndIndex = closeTagMatch.Index; parseTextEnd = closeTagMatch.Index + closeTagMatch.Length; } else if (_info.EnablePreRenderingCodeBlock) { codeEndIndex = text.Length; parseTextEnd = text.Length; } else { parseTextBegin = parseTextEnd = -1; return null; } parseTextBegin = match.Index; string code = text.Substring(match.Index + match.Length, codeEndIndex - (match.Index + match.Length)); string lang = match.Groups[2].Value; return Convert(lang, code); } private IEnumerable Convert(string lang, string code) { if (String.IsNullOrEmpty(lang)) { yield return new PlainCodeBlockElement(code); } else { // check wheither style is set if (!ThemeDetector.IsAvalonEditSetup) { SetupStyle(); } yield return new CodeBlockElement(_provider, lang, code); } } private static void SetupStyle() { if (Application.Current is null) return; string resourceUriTxt; if (ThemeDetector.IsFluentUsed) resourceUriTxt = "avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml"; else if (ThemeDetector.IsSimpleUsed) resourceUriTxt = "avares://AvaloniaEdit/Themes/Simple/AvaloniaEdit.xaml"; else { Debug.Print("Markdown.Avalonia.SyntaxHigh can't add style for AvaloniaEdit. See https://github.com/whistyun/Markdown.Avalonia/wiki/Setup-AvaloniaEdit-for-syntax-hightlighting"); return; } var aeStyle = new StyleInclude(new Uri("avares://Markdown.Avalonia/")) { Source = new Uri(resourceUriTxt) }; Application.Current.Styles.Add(aeStyle); } } } ================================================ FILE: Markdown.Avalonia.SyntaxHigh/ThemeDetector.cs ================================================ using Avalonia; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; namespace Markdown.Avalonia.SyntaxHigh { static class ThemeDetector { private static readonly string s_SimpleThemeFQCN = "Avalonia.Themes.Simple.SimpleTheme"; private static readonly string s_FluentThemeFQCN = "Avalonia.Themes.Fluent.FluentTheme"; private static readonly string s_SimpleThemeHost = "Avalonia.Themes.Simple"; private static readonly string s_FluentThemeHost = "Avalonia.Themes.Fluent"; private static bool _isAvalonEditSetup; private static bool? s_isSimpleUsed; private static bool? s_isFluentUsed; private static bool CheckStyleSourceHost(IStyle style, string hostName) { if (style is StyleInclude incld) { var uri = incld.Source; if (uri is null) return false; if (!uri.IsAbsoluteUri) return false; try { return uri.Host == hostName; } catch { return false; } } else return false; } private static bool? CheckApplicationCurrentStyle(string themeFQCN, string avaresHost) { if (Application.Current is null || Application.Current.Styles is null) return null; foreach (var style in Application.Current.Styles) { if (style.GetType().FullName == themeFQCN) { return true; } if (style is StyleInclude incld) { var uri = incld.Source; if (uri is null) return false; if (!uri.IsAbsoluteUri) return false; try { return uri.Host == avaresHost; } catch { return false; } } else return false; } return false; } public static bool IsAvalonEditSetup { get { if (_isAvalonEditSetup) return true; if (Application.Current is null || Application.Current.Styles is null) return false; foreach (var style in Application.Current.Styles) { if (CheckStyleSourceHost(style, "AvaloniaEdit")) { return _isAvalonEditSetup = true; } } return _isAvalonEditSetup = false; } } public static bool IsFluentUsed { get { if (s_isFluentUsed.HasValue) return s_isFluentUsed.Value; return Nvl(s_isFluentUsed = CheckApplicationCurrentStyle(s_FluentThemeFQCN, s_FluentThemeHost)); } } public static bool IsSimpleUsed { get { if (s_isSimpleUsed.HasValue) return s_isSimpleUsed.Value; return Nvl(s_isSimpleUsed = CheckApplicationCurrentStyle(s_SimpleThemeFQCN, s_SimpleThemeHost)); } } private static bool Nvl(bool? val) => val.HasValue && val.Value; } } ================================================ FILE: Markdown.Avalonia.Tight/CascadeDictionary.cs ================================================ using Avalonia; using Avalonia.Controls; using System; using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; namespace Markdown.Avalonia { public class CascadeDictionary { public IResourceDictionary Owner { get; set; } = new ResourceDictionary(); public WeakReference? Parent { get; set; } public void SetParent(StyledElement element) { Parent = new WeakReference(element); } #if NET6_0_OR_GREATER public bool TryGet(object key, [MaybeNullWhen(false)] out object? val) #else public bool TryGet(object key, out object val) #endif { if (Owner.TryGetResource(key, null, out var ownerRsc)) { val = ownerRsc!; return true; } StyledElement? node; if (Parent is null || !Parent.TryGetTarget(out node)) { val = null!; return false; } while (node is object) { if (node.TryGetResource(key, out var rsc)) { val = rsc!; return true; } node = node.Parent; } val = null!; return false; } } } ================================================ FILE: Markdown.Avalonia.Tight/ChatAISetup.cs ================================================ using Markdown.Avalonia.Plugins; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia { public class ChatAISetup : IMdAvPlugin { public void Setup(SetupInfo info) { info.EnableNoteBlock = false; info.EnableRuleExt = false; info.EnableTextAlignment = false; info.EnableListMarkerExt = false; info.EnableContainerBlockExt = false; info.EnableTextileInline = false; info.EnablePreRenderingCodeBlock = true; } } } ================================================ FILE: Markdown.Avalonia.Tight/ContainerSwitcher.cs ================================================ using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Metadata; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia { public class ContainerSwitch : AvaloniaDictionary, IContainerBlockHandler { public Border? ProvideControl(string assetPathRoot, string blockName, string lines) { // blockName may be "name [title] (url) {option}". // This collect some character until "[", "(" or "{". var trimedBlockName = blockName.Trim(); var match = Regex.Match(trimedBlockName, @"[\(\[\{]"); if (match.Success) { trimedBlockName = trimedBlockName.Substring(0, match.Index).Trim(); } if (this.TryGetValue(trimedBlockName, out var processor)) { return processor.ProvideControl(assetPathRoot, blockName, lines); } else return null; } } } ================================================ FILE: Markdown.Avalonia.Tight/Controls/AutoScaleColumnDefinitions.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Remote.Protocol.Viewport; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Markdown.Avalonia.Controls { public class AutoScaleColumnDefinitions : ColumnDefinitions { public static readonly AttachedProperty IsEnabledProperty = AvaloniaProperty.RegisterAttached("IsEnabled", false); public static bool GetIsEnabled(Control control) => control.GetValue(IsEnabledProperty); public static void SetIsEnabled(Control control, bool value) => control.SetValue(IsEnabledProperty, value); private int _columnCount; private Grid _grid; private bool _isAutoArrangeTried = false; public AutoScaleColumnDefinitions(int columnCount, Grid owner) { _columnCount = columnCount; _grid = owner; for (var i = 0; i < _columnCount; ++i) Add(new ColumnDefinition(1, GridUnitType.Auto)); _grid.LayoutUpdated += _grid_LayoutUpdated; } private void _grid_LayoutUpdated(object? sender, EventArgs e) { // If auto scale is disabled, use `1 *` for each column. // This is default value used for grid column width in Markdown.Avalonia. if (!GetIsEnabled(_grid)) { if (_grid.ColumnDefinitions.All(def => def.Width.IsStar && def.Width.Value == 1d)) return; _isAutoArrangeTried = false; Clear(); for (var i = 0; i < _columnCount; ++i) Add(new ColumnDefinition(1, GridUnitType.Star)); ReSetGridChildren(); } // If column width adjustement is already done, skip the following process. if (_isAutoArrangeTried) return; _isAutoArrangeTried = true; /// Determines the maximum requred width for each column. var width = new double[_columnCount]; foreach (var element in _grid.Children.OfType()) { var colidx = Grid.GetColumn(element); var colspan = Grid.GetColumnSpan(element); if (colspan == 1) { width[colidx] = Math.Max(width[colidx], element.Bounds.Width); } else { var requreWidth = element.Bounds.Width; var consumedWid = Enumerable.Range(colidx, colspan).Sum(i => width[i]); if (consumedWid >= requreWidth) continue; var adding = Math.Ceiling((requreWidth - consumedWid) / colspan); foreach (var i in Enumerable.Range(colidx, colspan)) { width[i] += adding; } } if (width.All(d => d != 0d)) break; } for (var i = 0; i < width.Length; ++i) width[i] = Math.Max(width[i], 1); // arrange width at every columns var allowedWidth = _grid.Bounds.Width; var allowedColWid = allowedWidth / width.Length; Clear(); var required = width.Sum(w => Math.Max(w - allowedColWid, 0)); if (required < 1d) { // equality arrange for (var i = 0; i < _columnCount; ++i) Add(new ColumnDefinition(1, GridUnitType.Star)); } else { // collect from wealthy columns var wealthy = width.Sum(w => Math.Max(allowedColWid - w, 0)); var hold = Enumerable.Repeat(allowedColWid, width.Length).ToArray(); var unit = Enumerable.Repeat(GridUnitType.Star, width.Length).ToArray(); bool isShortabe; double collect = (isShortabe = required > wealthy) ? wealthy : required; for (var i = 0; i < width.Length; ++i) { if (width[i] < allowedColWid) { hold[i] -= collect * (allowedColWid - width[i]) / wealthy; if (isShortabe) unit[i] = GridUnitType.Pixel; } else { hold[i] += collect * (width[i] - allowedColWid) / required; } } for (var i = 0; i < hold.Length; ++i) Add(new ColumnDefinition(hold[i], unit[i])); } ReSetGridChildren(); } private void ReSetGridChildren() { var backup = _grid.Children.ToArray(); _grid.Children.Clear(); _grid.Children.AddRange(backup); } } } ================================================ FILE: Markdown.Avalonia.Tight/Controls/Rule.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Media; using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Text; using HAlign = Avalonia.Layout.HorizontalAlignment; using VAlign = Avalonia.Layout.VerticalAlignment; namespace Markdown.Avalonia.Controls { public class Rule : UserControl { const double _SingleLineWidth = 1; const double _BoldLineWidth = 2; const double _LineMargin = 1; public static readonly StyledProperty SingleLineWidthProperty = AvaloniaProperty.Register(nameof(SingleLineWidth), defaultValue: _SingleLineWidth); public static readonly StyledProperty BoldLineWidthProperty = AvaloniaProperty.Register(nameof(SingleLineWidth), defaultValue: _BoldLineWidth); public static readonly StyledProperty LineMarginProperty = AvaloniaProperty.Register(nameof(LineMargin), defaultValue: _LineMargin); static Rule() { AffectsRender( BackgroundProperty, ForegroundProperty); AffectsMeasure( SingleLineWidthProperty, BoldLineWidthProperty, LineMarginProperty); } public double SingleLineWidth { get { return GetValue(SingleLineWidthProperty); } set { SetValue(SingleLineWidthProperty, value); } } public double BoldLineWidth { get { return GetValue(BoldLineWidthProperty); } set { SetValue(BoldLineWidthProperty, value); } } public double LineMargin { get { return GetValue(LineMarginProperty); } set { SetValue(LineMarginProperty, value); } } public RuleType Type { get; set; } public Rule(RuleType ruleType) { this.Type = ruleType; this.HorizontalAlignment = HAlign.Stretch; this.VerticalAlignment = VAlign.Center; var cls = Enum.GetName(typeof(RuleType), ruleType); if (cls is null) throw new ArgumentException(nameof(ruleType)); this.Classes.Add(cls); } protected override Size MeasureOverride(Size availableSize) { return Type switch { RuleType.Single => new Size(10, LineMargin * 2 + SingleLineWidth), RuleType.TwoLines => new Size(10, LineMargin * 2 + SingleLineWidth * 3), RuleType.Bold => new Size(10, LineMargin * 2 + BoldLineWidth), RuleType.BoldWithSingle => new Size(10, LineMargin * 2 + SingleLineWidth * 2 + BoldLineWidth), _ => throw new InvalidOperationException(), }; } public override void Render(DrawingContext context) { var brush = Foreground; var single = new Pen(brush, SingleLineWidth); var bold = new Pen(brush, BoldLineWidth); var width = Bounds.Width; switch (Type) { case RuleType.Single: context.DrawLine( single, new Point(0d, LineMargin + SingleLineWidth / 2), new Point(width, LineMargin + SingleLineWidth / 2)); break; case RuleType.TwoLines: context.DrawLine( single, new Point(0d, LineMargin + SingleLineWidth / 2), new Point(width, LineMargin + SingleLineWidth / 2)); context.DrawLine( single, new Point(0d, LineMargin * 2 + SingleLineWidth * 3 / 2), new Point(width, LineMargin * 2 + SingleLineWidth * 3 / 2)); break; case RuleType.Bold: context.DrawLine( bold, new Point(0d, LineMargin + BoldLineWidth / 2), new Point(width, LineMargin + BoldLineWidth / 2)); break; case RuleType.BoldWithSingle: context.DrawLine( bold, new Point(0d, LineMargin + BoldLineWidth / 2), new Point(width, LineMargin + BoldLineWidth / 2)); context.DrawLine( single, new Point(0d, LineMargin * 2 + BoldLineWidth + SingleLineWidth / 2), new Point(width, LineMargin * 2 + BoldLineWidth + SingleLineWidth / 2)); break; default: throw new InvalidOperationException(); } } } public enum RuleType { Single, TwoLines, Bold, BoldWithSingle, } } ================================================ FILE: Markdown.Avalonia.Tight/CopyMode.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia { public enum CopyMode { None, PlainText, } } ================================================ FILE: Markdown.Avalonia.Tight/EmojiTable.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Text; namespace Markdown.Avalonia { public static class EmojiTable { private static ConcurrentDictionary s_keywords; static EmojiTable() { s_keywords = LoadTxt(); } /* Workaround for Visual Studio Xaml Designer. When you open MarkdownStyle from Xaml Designer, A null error occurs. Perhaps static constructor is not executed. */ private static ConcurrentDictionary LoadTxt() { var resourceName = "Markdown.Avalonia.EmojiTable.txt"; var dic = new ConcurrentDictionary(); Assembly asm = typeof(EmojiTable).Assembly; using var stream = asm.GetManifestResourceStream(resourceName); Debug.Assert(stream is not null); using var reader = new StreamReader(stream, true); string? line; while ((line = reader.ReadLine()) is not null) { var elms = line.Split('\t'); dic[elms[1]] = elms[0]; } return dic; } #if NET6_0_OR_GREATER public static bool TryGet(string keyword, [MaybeNullWhen(false)] out string? emoji) #else public static bool TryGet(string keyword, out string emoji) #endif { if (s_keywords is null) s_keywords = LoadTxt(); return s_keywords.TryGetValue(keyword, out emoji); } } } ================================================ FILE: Markdown.Avalonia.Tight/EmojiTable.txt ================================================ 👎 -1 👍 +1 💯 100 🎱 8ball 🅰️ a 🆎 ab 🔤 abc 🔡 abcd 🉑 accept 🚡 aerial_tramway ✈️ airplane ⏰ alarm_clock 👽 alien 🚑 ambulance ⚓ anchor 👼 angel 💢 anger 😠 angry 😧 anguished 🐜 ant 🍎 apple ♒ aquarius ♈ aries ◀️ arrow_backward ⏬ arrow_double_down ⏫ arrow_double_up ⬇️ arrow_down 🔽 arrow_down_small ▶️ arrow_forward ⤵️ arrow_heading_down ⤴️ arrow_heading_up ⬅️ arrow_left ↙️ arrow_lower_left ↘️ arrow_lower_right ➡️ arrow_right ↪️ arrow_right_hook ⬆️ arrow_up ↕️ arrow_up_down 🔼 arrow_up_small ↖️ arrow_upper_left ↗️ arrow_upper_right 🔃 arrows_clockwise 🔄 arrows_counterclockwise 🎨 art 🚛 articulated_lorry 😲 astonished 🏧 atm 🅱️ b 👶 baby 🍼 baby_bottle 🐤 baby_chick 🚼 baby_symbol 🛄 baggage_claim 🎈 balloon ☑️ ballot_box_with_check 🎍 bamboo 🍌 banana ‼️ bangbang 🏦 bank 📊 bar_chart 💈 barber ⚾ baseball 🏀 basketball 🛀 bath 🛁 bathtub 🔋 battery 🐻 bear 🍺 beer 🍻 beers 🐞 beetle 🔰 beginner 🔔 bell 🍱 bento 🚴 bicyclist 🚲 bike 👙 bikini 🐦 bird 🎂 birthday ⚫ black_circle 🃏 black_joker ✒️ black_nib 🔲 black_square_button 🌼 blossom 🐡 blowfish 📘 blue_book 🚙 blue_car 💙 blue_heart 😊 blush 🐗 boar ⛵ boat 💣 bomb 📖 book 🔖 bookmark 📑 bookmark_tabs 📚 books 💥 boom 👢 boot 💐 bouquet 🙇 bow 🎳 bowling 👦 boy 🍞 bread 👰 bride_with_veil 🌉 bridge_at_night 💼 briefcase 💔 broken_heart 🐛 bug 💡 bulb 🚅 bullettrain_front 🚄 bullettrain_side 🚌 bus 🚏 busstop 👤 bust_in_silhouette 👥 busts_in_silhouette 🌵 cactus 🍰 cake 📆 calendar 📲 calling 🐫 camel 📷 camera ♋ cancer 🍬 candy 🔠 capital_abcd ♑ capricorn 🚗 car 📇 card_index 🎠 carousel_horse 🐱 cat 🐈 cat2 💿 cd 💹 chart 📉 chart_with_downwards_trend 📈 chart_with_upwards_trend 🏁 checkered_flag 🍒 cherries 🌸 cherry_blossom 🌰 chestnut 🐔 chicken 🚸 children_crossing 🍫 chocolate_bar 🎄 christmas_tree ⛪ church 🎦 cinema 🎪 circus_tent 🌇 city_sunrise 🌆 city_sunset 🆑 cl 👏 clap 🎬 clapper 📋 clipboard 🕐 clock1 🕙 clock10 🕥 clock1030 🕚 clock11 🕦 clock1130 🕛 clock12 🕧 clock1230 🕜 clock130 🕑 clock2 🕝 clock230 🕒 clock3 🕞 clock330 🕓 clock4 🕟 clock430 🕔 clock5 🕠 clock530 🕕 clock6 🕡 clock630 🕖 clock7 🕢 clock730 🕗 clock8 🕣 clock830 🕘 clock9 🕤 clock930 📕 closed_book 🔐 closed_lock_with_key 🌂 closed_umbrella ☁️ cloud ♣️ clubs 🇨🇳 cn 🍸 cocktail ☕ coffee 😰 cold_sweat 💥 collision 💻 computer 🎊 confetti_ball 😖 confounded 😕 confused ㊗️ congratulations 🚧 construction 👷 construction_worker 🏪 convenience_store 🍪 cookie 🆒 cool 👮 cop ©️ copyright 🌽 corn 👫 couple 💑 couple_with_heart 💏 couplekiss 🐮 cow 🐄 cow2 💳 credit_card 🐊 crocodile 🎌 crossed_flags 👑 crown 😢 cry 😿 crying_cat_face 🔮 crystal_ball 💘 cupid ➰ curly_loop 💱 currency_exchange 🍛 curry 🍮 custard 🛃 customs 🌀 cyclone 💃 dancer 👯 dancers 🍡 dango 🎯 dart 💨 dash 📅 date 🇩🇪 de 🌳 deciduous_tree 🏬 department_store 💠 diamond_shape_with_a_dot_inside ♦️ diamonds 😞 disappointed 😥 disappointed_relieved 💫 dizzy 😵 dizzy_face 🚯 do_not_litter 🐶 dog 🐕 dog2 💵 dollar 🎎 dolls 🐬 dolphin 🚪 door 🍩 doughnut 🐉 dragon 🐲 dragon_face 👗 dress 🐪 dromedary_camel 💧 droplet 📀 dvd 📧 e-mail 👂 ear 🌾 ear_of_rice 🌍 earth_africa 🌎 earth_americas 🌏 earth_asia 🥚 egg 🍆 eggplant ✴️ eight_pointed_black_star ✳️ eight_spoked_asterisk 🔌 electric_plug 🐘 elephant ✉️ email 🔚 end ✉️ envelope 🇪🇸 es 💶 euro 🏰 european_castle 🏤 european_post_office 🌲 evergreen_tree ❗ exclamation 😑 expressionless 👓 eyeglasses 👀 eyes 👊 facepunch 🏭 factory 🍂 fallen_leaf 👪 family ⏩ fast_forward 📠 fax 😨 fearful 🐾 feet 🎡 ferris_wheel 📁 file_folder 🔥 fire 🚒 fire_engine 🎆 fireworks 🌓 first_quarter_moon 🌛 first_quarter_moon_with_face 🐟 fish 🍥 fish_cake 🎣 fishing_pole_and_fish ✊ fist 🎏 flags 🔦 flashlight 💾 floppy_disk 🎴 flower_playing_cards 😳 flushed 🌁 foggy 🏈 football 🍴 fork_and_knife ⛲ fountain 🍀 four_leaf_clover 🇫🇷 fr 🆓 free 🍤 fried_shrimp 🍟 fries 🐸 frog 😦 frowning 🖕 fu ⛽ fuelpump 🌕 full_moon 🌝 full_moon_with_face 🎲 game_die 🇬🇧 gb 💎 gem ♊ gemini 👻 ghost 🎁 gift 💝 gift_heart 👧 girl 🌐 globe_with_meridians 🐐 goat ⛳ golf 🍇 grapes 🍏 green_apple 📗 green_book 💚 green_heart ❕ grey_exclamation ❔ grey_question 😬 grimacing 😁 grin 😀 grinning 💂‍♂️ guardsman 🎸 guitar 🔫 gun 💇 haircut 🍔 hamburger 🔨 hammer 🐹 hamster ✋ hand 👜 handbag 💩 hankey 🐥 hatched_chick 🐣 hatching_chick 🎧 headphones 🙉 hear_no_evil ❤️ heart 💟 heart_decoration 😍 heart_eyes 😻 heart_eyes_cat 💓 heartbeat 💗 heartpulse ♥️ hearts ✔️ heavy_check_mark ➗ heavy_division_sign 💲 heavy_dollar_sign ❗ heavy_exclamation_mark ➖ heavy_minus_sign ✖️ heavy_multiplication_x ➕ heavy_plus_sign 🚁 helicopter 🌿 herb 🌺 hibiscus 🔆 high_brightness 👠 high_heel 🔪 hocho 🍯 honey_pot 🐝 honeybee 🐴 horse 🏇 horse_racing 🏥 hospital 🏨 hotel ♨️ hotsprings ⌛ hourglass ⏳ hourglass_flowing_sand 🏠 house 🏡 house_with_garden 😯 hushed 🍨 ice_cream 🍦 icecream 🆔 id 🉐 ideograph_advantage 👿 imp 📥 inbox_tray 📨 incoming_envelope 💁 information_desk_person ℹ️ information_source 😇 innocent ⁉️ interrobang 📱 iphone 🇮🇹 it 🏮 izakaya_lantern 🎃 jack_o_lantern 🗾 japan 🏯 japanese_castle 👺 japanese_goblin 👹 japanese_ogre 👖 jeans 😂 joy 😹 joy_cat 🇯🇵 jp 🔑 key 👘 kimono 💋 kiss 😗 kissing 😽 kissing_cat 😚 kissing_closed_eyes 😘 kissing_heart 😙 kissing_smiling_eyes 🐨 koala 🈁 koko 🇰🇷 kr 🔵 large_blue_circle 🔷 large_blue_diamond 🔶 large_orange_diamond 🌗 last_quarter_moon 🌜 last_quarter_moon_with_face 😆 laughing 🍃 leaves 📒 ledger 🛅 left_luggage ↔️ left_right_arrow ↩️ leftwards_arrow_with_hook 🍋 lemon ♌ leo 🐆 leopard ♎ libra 🚈 light_rail 🔗 link 👄 lips 💄 lipstick 🔒 lock 🔏 lock_with_ink_pen 🍭 lollipop ➿ loop 📢 loudspeaker 🏩 love_hotel 💌 love_letter 🔅 low_brightness Ⓜ️ m 🔍 mag 🔎 mag_right 🀄 mahjong 📫 mailbox 📪 mailbox_closed 📬 mailbox_with_mail 📭 mailbox_with_no_mail 👨 man 👲 man_with_gua_pi_mao 👳‍♂️ man_with_turban 👞 mans_shoe 🍁 maple_leaf 😷 mask 💆 massage 🍖 meat_on_bone 📣 mega 🍈 melon 📝 memo 🚹 mens 🤘 metal 🚇 metro 🎤 microphone 🔬 microscope 🌌 milky_way 🚐 minibus 💽 minidisc 📴 mobile_phone_off 💸 money_with_wings 💰 moneybag 🐒 monkey 🐵 monkey_face 🚝 monorail 🌔 moon 🎓 mortar_board 🗻 mount_fuji 🚵 mountain_bicyclist 🚠 mountain_cableway 🚞 mountain_railway 🐭 mouse 🐁 mouse2 🎥 movie_camera 🗿 moyai 💪 muscle 🍄 mushroom 🎹 musical_keyboard 🎵 musical_note 🎼 musical_score 🔇 mute 💅 nail_care 📛 name_badge 👔 necktie ❎ negative_squared_cross_mark 😐 neutral_face 🆕 new 🌑 new_moon 🌚 new_moon_with_face 📰 newspaper 🆖 ng 🔕 no_bell 🚳 no_bicycles ⛔ no_entry 🚫 no_entry_sign 🙅 no_good 📵 no_mobile_phones 😶 no_mouth 🚷 no_pedestrians 🚭 no_smoking 🚱 non-potable_water 👃 nose 📓 notebook 📔 notebook_with_decorative_cover 🎶 notes 🔩 nut_and_bolt ⭕ o 🅾️ o2 🌊 ocean 🐙 octopus 🍢 oden 🏢 office 🆗 ok 👌 ok_hand 🙆‍♀️ ok_woman 👴 older_man 👵 older_woman 🔛 on 🚘 oncoming_automobile 🚍 oncoming_bus 🚔 oncoming_police_car 🚖 oncoming_taxi 📂 open_file_folder 👐 open_hands 😮 open_mouth ⛎ ophiuchus 📙 orange_book 📤 outbox_tray 🐂 ox 📄 page_facing_up 📃 page_with_curl 📟 pager 🌴 palm_tree 🐼 panda_face 📎 paperclip 🅿️ parking 〽️ part_alternation_mark ⛅ partly_sunny 🛂 passport_control 🐾 paw_prints 🍑 peach 🍐 pear 📝 pencil ✏️ pencil2 🐧 penguin 😔 pensive 🎭 performing_arts 😣 persevere ☎️ phone 🐷 pig 🐽 pig_nose 🐖 pig2 💊 pill 🍍 pineapple ♓ pisces 🍕 pizza 👇 point_down 👈 point_left 👉 point_right ☝️ point_up 👆 point_up_2 🚓 police_car 🐩 poodle 💩 poop 🏣 post_office 📯 postal_horn 📮 postbox 🚰 potable_water 👝 pouch 🍗 poultry_leg 💷 pound 😾 pouting_cat 🙏 pray 👸 princess 👊 punch 💜 purple_heart 👛 purse 📌 pushpin 🚮 put_litter_in_its_place ❓ question 🐰 rabbit 🐇 rabbit2 🐎 racehorse 📻 radio 🔘 radio_button 😡 rage 🚃 railway_car 🌈 rainbow ✋ raised_hand 🙌 raised_hands 🙋 raising_hand 🐏 ram 🍜 ramen 🐀 rat ♻️ recycle 🚗 red_car 🔴 red_circle ®️ registered ☺️ relaxed 😌 relieved 🔁 repeat 🔂 repeat_one 🚻 restroom 💞 revolving_hearts ⏪ rewind 🎀 ribbon 🍚 rice 🍙 rice_ball 🍘 rice_cracker 🎑 rice_scene 💍 ring 🚀 rocket 🎢 roller_coaster 🐓 rooster 🌹 rose 🚨 rotating_light 📍 round_pushpin 🚣 rowboat 🇷🇺 ru 🏉 rugby_football 🏃 runner 🏃 running 🎽 running_shirt_with_sash 🈂️ sa ♐ sagittarius ⛵ sailboat 🍶 sake 👡 sandal 🎅 santa 📡 satellite 😆 satisfied 🎷 saxophone 🏫 school 🎒 school_satchel ✂️ scissors ♏ scorpius 😱 scream 🙀 scream_cat 📜 scroll 💺 seat ㊙️ secret 🙈 see_no_evil 🌱 seedling 🍧 shaved_ice 🐑 sheep 🐚 shell 🚢 ship 👕 shirt 💩 shit 👞 shoe 🚿 shower 📶 signal_strength 🔯 six_pointed_star 🎿 ski 💀 skull 😴 sleeping 😪 sleepy 🎰 slot_machine 🔹 small_blue_diamond 🔸 small_orange_diamond 🔺 small_red_triangle 🔻 small_red_triangle_down 😄 smile 😸 smile_cat 😃 smiley 😺 smiley_cat 😈 smiling_imp 😏 smirk 😼 smirk_cat 🚬 smoking 🐌 snail 🐍 snake 🏂 snowboarder ❄️ snowflake ⛄ snowman 😭 sob ⚽ soccer 🔜 soon 🆘 sos 🔉 sound 👾 space_invader ♠️ spades 🍝 spaghetti 🎇 sparkler ✨ sparkles 💖 sparkling_heart 🙊 speak_no_evil 🔈 speaker 💬 speech_balloon 🚤 speedboat ⭐ star 🌟 star2 🌠 stars 🚉 station 🗽 statue_of_liberty 🚂 steam_locomotive 🍲 stew 📏 straight_ruler 🍓 strawberry 😛 stuck_out_tongue 😝 stuck_out_tongue_closed_eyes 😜 stuck_out_tongue_winking_eye 🌞 sun_with_face 🌻 sunflower 😎 sunglasses ☀️ sunny 🌅 sunrise 🌄 sunrise_over_mountains 🏄 surfer 🍣 sushi 🚟 suspension_railway 😓 sweat 💦 sweat_drops 😅 sweat_smile 🍠 sweet_potato 🏊 swimmer 🔣 symbols 💉 syringe 🎉 tada 🎋 tanabata_tree 🍊 tangerine ♉ taurus 🚕 taxi 🍵 tea ☎️ telephone 📞 telephone_receiver 🔭 telescope 🎾 tennis ⛺ tent 💭 thought_balloon 👎 thumbsdown 👍 thumbsup 🎫 ticket 🐯 tiger 🐅 tiger2 😫 tired_face ™️ tm 🚽 toilet 🗼 tokyo_tower 🍅 tomato 👅 tongue 🔝 top 🎩 tophat 🚜 tractor 🚥 traffic_light 🚋 train 🚆 train2 🚊 tram 🚩 triangular_flag_on_post 📐 triangular_ruler 🔱 trident 😤 triumph 🚎 trolleybus 🏆 trophy 🍹 tropical_drink 🐠 tropical_fish 🚚 truck 🎺 trumpet 👕 tshirt 🌷 tulip 🐢 turtle 📺 tv 🔀 twisted_rightwards_arrows 💕 two_hearts 👬 two_men_holding_hands 👭 two_women_holding_hands 🈹 u5272 🈴 u5408 🈺 u55b6 🈯 u6307 🈷️ u6708 🈶 u6709 🈵 u6e80 🈚 u7121 🈸 u7533 🈲 u7981 🈳 u7a7a 🇬🇧 uk ☔ umbrella: 😒 unamused 🔞 underage 🔓 unlock 🆙 up 🇺🇸 us ✌️ v 🚦 vertical_traffic_light 📼 vhs 📳 vibration_mode 📹 video_camera 🎮 video_game 🎻 violin ♍ virgo 🌋 volcano 🆚 vs 🚶 walking 🌘 waning_crescent_moon 🌖 waning_gibbous_moon ⚠️ warning ⌚ watch 🐃 water_buffalo 🍉 watermelon 👋 wave 〰️ wavy_dash 🌒 waxing_crescent_moon 🌔 waxing_gibbous_moon 🚾 wc 😩 weary 💒 wedding 🐳 whale 🐋 whale2 ♿ wheelchair ✅ white_check_mark ⚪ white_circle 💮 white_flower 🔳 white_square_button 🎐 wind_chime 🍷 wine_glass 😉 wink 🐺 wolf 👩 woman 👚 womans_clothes 👒 womans_hat 🚺 womens 😟 worried 🔧 wrench ❌ x 💛 yellow_heart 💴 yen 😋 yum ⚡ zap 💤 zzz ================================================ FILE: Markdown.Avalonia.Tight/EngineUpg.cs ================================================ using Avalonia.Controls; using Avalonia.Rendering.Composition.Animations; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Net.NetworkInformation; using System.Text; using System.Windows.Input; namespace Markdown.Avalonia { public static class MarkdownEngineExt { public static IMarkdownEngine2 Upgrade(this IMarkdownEngine engine) => engine is IMarkdownEngine2 engine2 ? engine2 : new EngineUpg(engine); class EngineUpg : IMarkdownEngine2 { private IMarkdownEngine Engine { get; } public string AssetPathRoot { get => Engine.AssetPathRoot; set => Engine.AssetPathRoot = value; } public ICommand? HyperlinkCommand { get => Engine.HyperlinkCommand; set => Engine.HyperlinkCommand = value; } public IBitmapLoader? BitmapLoader { get => Engine.BitmapLoader; set => Engine.BitmapLoader = value; } public IContainerBlockHandler? ContainerBlockHandler { get => Engine.ContainerBlockHandler; set => Engine.ContainerBlockHandler = value; } public MdAvPlugins Plugins { get => Engine.Plugins; set => Engine.Plugins = value; } public bool UseResource { get => Engine.UseResource; set => Engine.UseResource = value; } public CascadeDictionary CascadeResources => Engine.CascadeResources; public IResourceDictionary Resources { get => Engine.Resources; set => Engine.Resources = value; } public EngineUpg(IMarkdownEngine engine) { Engine = engine; } public Control Transform(string text) => Engine.Transform(text); public DocumentElement TransformElement(string text) => new UnBlockElement(Engine.Transform(text)); public IEnumerable ParseGamutElement(string? text, ParseStatus status) { foreach (var ctrl in Engine.RunBlockGamut(text, status)) { yield return new UnBlockElement(ctrl); } } public IEnumerable ParseGamutInline(string? text) => Engine.RunSpanGamut(text); } } } ================================================ FILE: Markdown.Avalonia.Tight/Extensions/AlphaExtension.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; namespace Markdown.Avalonia.Extensions { public class AlphaExtension : MarkupExtension { private readonly string _brushName; private readonly float _alpha; public AlphaExtension(string colorKey) : this(colorKey, 1f) { } public AlphaExtension(string colorKey, float alpha) { this._brushName = colorKey; this._alpha = alpha; } public override object ProvideValue(IServiceProvider serviceProvider) { var dyExt = new DynamicResourceExtension(_brushName); var brush = dyExt.ProvideValue(serviceProvider); return new MultiBinding() { Bindings = new BindingBase[] { brush }, Converter = new AlphaConverter(_alpha) }; } class AlphaConverter : IMultiValueConverter { public float Alpha { get; } public AlphaConverter(float alpha) { Alpha = alpha; } public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { Color c; if (values[0] is ISolidColorBrush b) c = b.Color; else if (values[0] is Color col) c = col; else return values[0]; return new SolidColorBrush( Color.FromArgb( (byte)(c.A / 255f * Alpha * 255f), c.R, c.G, c.B)); } } } } ================================================ FILE: Markdown.Avalonia.Tight/Extensions/ComplementaryExtension.cs ================================================ using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; namespace Markdown.Avalonia.Extensions { public class ComplementaryExtension : MarkupExtension { private readonly string _brushName; public ComplementaryExtension(string colorKey) { this._brushName = colorKey; } public override object ProvideValue(IServiceProvider serviceProvider) { var dyExt = new DynamicResourceExtension(_brushName); var brush = dyExt.ProvideValue(serviceProvider); return new MultiBinding() { Bindings = new BindingBase[] { brush }, Converter = new ComplementaryConverter() }; } class ComplementaryConverter : IMultiValueConverter { public ComplementaryConverter() { } public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { Color c; if (values[0] is ISolidColorBrush b) c = b.Color; else if (values[0] is Color col) c = col; else return values[0]; var rgb = new int[] { c.R, c.G, c.B }; var s = rgb.Max() + rgb.Min(); return new SolidColorBrush( Color.FromArgb( c.A, (byte)(s - c.R), (byte)(s - c.G), (byte)(s - c.B))); } } } } ================================================ FILE: Markdown.Avalonia.Tight/Extensions/DivideColorExtension.cs ================================================ using Avalonia; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using Avalonia.Media.Immutable; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Text; namespace Markdown.Avalonia.Extensions { public class DivideColorExtension : MarkupExtension { private readonly string _frmKey; private readonly string _toKey; private readonly double _relate; public DivideColorExtension(string frm, string to, double relate) { this._frmKey = frm; this._toKey = to; this._relate = relate; } public override object ProvideValue(IServiceProvider serviceProvider) { BindingBase left; if (Color.TryParse(_frmKey, out var leftColor)) { var leftBrush = new ImmutableSolidColorBrush(leftColor); left = CompiledBinding.Create(v=>v, leftColor); } else { var lftExt = new DynamicResourceExtension(_frmKey); left = lftExt.ProvideValue(serviceProvider); } BindingBase right; if (Color.TryParse(_toKey, out var rightColor)) { var rightBrush = new ImmutableSolidColorBrush(rightColor); right = CompiledBinding.Create(v=>v, rightColor); } else { var rgtExt = new DynamicResourceExtension(_toKey); right = rgtExt.ProvideValue(serviceProvider); } return new MultiBinding() { Bindings = new BindingBase[] { left, right }, Converter = new DivideConverter(_relate) }; } } class DivideConverter : IMultiValueConverter { public double Relate { get; } public DivideConverter(double relate) { Relate = relate; } public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { Color colL; if (values[0] is ISolidColorBrush bl) colL = bl.Color; else if (values[0] is Color cl) colL = cl; else return AvaloniaProperty.UnsetValue; Color colR; if (values[1] is ISolidColorBrush br) colR = br.Color; else if (values[1] is Color cr) colR = cr; else return new ImmutableSolidColorBrush(colL); static byte Calc(byte l, byte r, double d) => (byte)(l * (1 - d) + r * d); return new SolidColorBrush( Color.FromArgb( Calc(colL.A, colR.A, Relate), Calc(colL.R, colR.R, Relate), Calc(colL.G, colR.G, Relate), Calc(colL.B, colR.B, Relate))); } } } ================================================ FILE: Markdown.Avalonia.Tight/Extensions/MultiplyExtension.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Media; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; namespace Markdown.Avalonia.Extensions { public class MultiplyExtension : MarkupExtension { private readonly string _resourceKey; private readonly double _scale; public MultiplyExtension(string resourceKey) : this(resourceKey, 1) { } public MultiplyExtension(string resourceKey, double scale) { this._resourceKey = resourceKey; this._scale = scale; } public override object ProvideValue(IServiceProvider serviceProvider) { var dyExt = new DynamicResourceExtension(_resourceKey); var brush = dyExt.ProvideValue(serviceProvider); return new MultiBinding() { Bindings = new BindingBase[] { brush }, Converter = new MultiplyConverter(_scale) }; } class MultiplyConverter : IMultiValueConverter { public double Scale { get; } public MultiplyConverter(double scale) { Scale = scale; } public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { return values[0] switch { short s => (short)(s * Scale), int i => (int)(i * Scale), long l => (long)(l * Scale), float f => (float)(f * Scale), double d => (double)(d * Scale), _ => values[0], }; } } } } ================================================ FILE: Markdown.Avalonia.Tight/Header.cs ================================================ using System; namespace Markdown.Avalonia { public class Header : IEquatable
{ public int Level { get; } public string Text { get; } public Header(int lv, string txt) { Level = lv; Text = txt; } public override int GetHashCode() => Level + Text.GetHashCode(); public override bool Equals(object? obj) => obj is Header arg ? Equals(arg) : false; public bool Equals(Header? other) => Level == other.Level && Text == other.Text; public static bool operator !=(Header? left, Header? right) => !(left == right); public static bool operator ==(Header? left, Header? right) => left is not null ? left.Equals(right) : right is not null ? false : true; } } ================================================ FILE: Markdown.Avalonia.Tight/HeaderScrolled.cs ================================================ namespace Markdown.Avalonia { public delegate void HeaderScrolled(object sender, HeaderScrolledEventArgs args); } ================================================ FILE: Markdown.Avalonia.Tight/HeaderScrolledEventArgs.cs ================================================ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; namespace Markdown.Avalonia { public class HeaderScrolledEventArgs : EventArgs, IEquatable { public IReadOnlyList
Tree { get; } public IReadOnlyList
Viewing { get; } public HeaderScrolledEventArgs(IList
tree, IList
viewing) { Tree = new ReadOnlyCollection
(tree); Viewing = new ReadOnlyCollection
(viewing); } public override int GetHashCode() => Tree.Sum(e => e.GetHashCode()) + Viewing.Sum(e => e.GetHashCode()); public override bool Equals(object? obj) => obj is HeaderScrolledEventArgs arg ? Equals(arg) : false; public bool Equals(HeaderScrolledEventArgs? other) { if (other is null) return false; return Enumerable.SequenceEqual(Tree, other.Tree) && Enumerable.SequenceEqual(Viewing, other.Viewing); } public static bool operator !=(HeaderScrolledEventArgs? left, HeaderScrolledEventArgs? right) => !(left == right); public static bool operator ==(HeaderScrolledEventArgs? left, HeaderScrolledEventArgs? right) => left is not null ? left.Equals(right) : right is not null ? false : true; } } ================================================ FILE: Markdown.Avalonia.Tight/IMarkdownEngine.cs ================================================ using Avalonia.Controls; using Avalonia.Rendering.Composition.Animations; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Net.NetworkInformation; using System.Text; using System.Windows.Input; namespace Markdown.Avalonia { public interface IMarkdownEngine : IMarkdownEngineBase { string AssetPathRoot { get; set; } ICommand? HyperlinkCommand { get; set; } [Obsolete("Please use Plugins propety. see https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-migrages-to-ver11")] IBitmapLoader? BitmapLoader { get; set; } IContainerBlockHandler? ContainerBlockHandler { get; set; } MdAvPlugins Plugins { get; set; } bool UseResource { get; set; } CascadeDictionary CascadeResources { get; } public IResourceDictionary Resources { get; set; } Control Transform(string text); /// /// Perform transformations that form block-level tags like paragraphs, headers, and list items. /// IEnumerable RunBlockGamut(string? text, ParseStatus status); /// /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items. /// IEnumerable RunSpanGamut(string? text); } } ================================================ FILE: Markdown.Avalonia.Tight/IMarkdownEngine2.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Windows.Input; namespace Markdown.Avalonia { public interface IMarkdownEngine2 : IMarkdownEngineBase { string AssetPathRoot { get; set; } ICommand? HyperlinkCommand { get; set; } IContainerBlockHandler? ContainerBlockHandler { get; set; } MdAvPlugins Plugins { get; set; } bool UseResource { get; set; } CascadeDictionary CascadeResources { get; } public IResourceDictionary Resources { get; set; } Control Transform(string text); DocumentElement TransformElement(string text); IEnumerable ParseGamutElement(string? text, ParseStatus status); IEnumerable ParseGamutInline(string? text); } } ================================================ FILE: Markdown.Avalonia.Tight/IMarkdownEngineBase.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia { public interface IMarkdownEngineBase { } } ================================================ FILE: Markdown.Avalonia.Tight/Markdown.Avalonia.Tight.csproj ================================================  Library $(PackageTargetFrameworks) Markdown.Avalonia.Tight Markdown.Avalonia Markdown.Avalonia $(PackageVersion) Bevan Arps(original); whistyun Markdown Controls for Avalonia Copyright (c) 2010 Bevan Arps, 2020 whistyun https://github.com/whistyun/Markdown.Avalonia MIT Markdown.Avalonia.Tight.md Markdown Avalonia Avaloniaui Debug;Release 9 enable ================================================ FILE: Markdown.Avalonia.Tight/Markdown.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Styling; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Controls; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Parsers.Builtin; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.Tables; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Windows.Input; namespace Markdown.Avalonia { public class Markdown : AvaloniaObject, IMarkdownEngine, IMarkdownEngine2 { #region const /// /// maximum nested depth of [] and () supported by the transform; implementation detail /// private const int _nestDepth = 6; /// /// Tabs are automatically converted to spaces as part of the transform /// this constant determines how "wide" those tabs become in spaces /// private const int _tabWidth = 4; public const string Heading1Class = ClassNames.Heading1Class; public const string Heading2Class = ClassNames.Heading2Class; public const string Heading3Class = ClassNames.Heading3Class; public const string Heading4Class = ClassNames.Heading4Class; public const string Heading5Class = ClassNames.Heading5Class; public const string Heading6Class = ClassNames.Heading6Class; public const string CodeBlockClass = ClassNames.CodeBlockClass; public const string ContainerBlockClass = ClassNames.ContainerBlockClass; public const string NoContainerClass = ClassNames.NoContainerClass; public const string BlockquoteClass = ClassNames.BlockquoteClass; public const string NoteClass = ClassNames.NoteClass; public const string ParagraphClass = ClassNames.ParagraphClass; public const string TableClass = ClassNames.TableClass; public const string TableHeaderClass = ClassNames.TableHeaderClass; public const string TableFirstRowClass = ClassNames.TableFirstRowClass; public const string TableRowOddClass = ClassNames.TableRowOddClass; public const string TableRowEvenClass = ClassNames.TableRowEvenClass; public const string TableLastRowClass = ClassNames.TableLastRowClass; public const string TableFooterClass = ClassNames.TableFooterClass; public const string ListClass = ClassNames.ListClass; public const string ListMarkerClass = ClassNames.ListMarkerClass; #endregion /// /// when true, bold and italic require non-word characters on either side /// WARNING: this is a significant deviation from the markdown spec /// public bool StrictBoldItalic { get; set; } private string _assetPathRoot; /// public string AssetPathRoot { get => _assetPathRoot; set { _assetPathRoot = value; #pragma warning disable CS0618 if (BitmapLoader is not null) BitmapLoader.AssetPathRoot = value; #pragma warning restore CS0618 if (_setupInfo is not null) _setupInfo.PathResolver.AssetPathRoot = value; } } private string[] _assetAssemblyNames; public IEnumerable AssetAssemblyNames => _assetAssemblyNames; private ICommand? _hyperlinkCommand; /// public ICommand? HyperlinkCommand { get => _hyperlinkCommand ?? _setupInfo?.HyperlinkCommand; set { _hyperlinkCommand = value; } } public MdAvPlugins Plugins { get; set; } [Obsolete] private IBitmapLoader? _loader; /// [Obsolete("Please use Plugins propety. see https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-migrages-to-ver11")] public IBitmapLoader? BitmapLoader { get => _loader; set { _loader = value; if (_loader is not null) { _loader.AssetPathRoot = _assetPathRoot; } } } private IContainerBlockHandler? _containerBlockHandler; public IContainerBlockHandler? ContainerBlockHandler { get => _containerBlockHandler ?? _setupInfo?.ContainerBlock; set { _containerBlockHandler = value; } } public CascadeDictionary CascadeResources { get; } = new CascadeDictionary(); public IResourceDictionary Resources { get => CascadeResources.Owner; set => CascadeResources.Owner = value; } public bool UseResource { get; set; } #region dependencyobject property public static readonly DirectProperty HyperlinkCommandProperty = AvaloniaProperty.RegisterDirect(nameof(HyperlinkCommand), mdEng => mdEng.HyperlinkCommand, (mdEng, command) => mdEng.HyperlinkCommand = command); [Obsolete("Please use Plugins propety. see https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-migrages-to-ver11")] public static readonly DirectProperty BitmapLoaderProperty = AvaloniaProperty.RegisterDirect(nameof(BitmapLoader), mdEng => mdEng.BitmapLoader, (mdEng, loader) => mdEng.BitmapLoader = loader); #endregion #region ParseInfo private SetupInfo _setupInfo; private BlockParser2[] _topBlockParsers; private BlockParser2[] _blockParsers; private InlineParser[] _inlines; private bool _supportTextAlignment; private bool _supportStrikethrough; private bool _supportTextileInline; #endregion public Markdown() { _assetPathRoot = Environment.CurrentDirectory; var stack = new StackTrace(); _assetAssemblyNames = stack.GetFrames() .Select(frm => frm?.GetMethod()?.DeclaringType?.Assembly?.GetName()?.Name) .OfType() .Where(name => !name.Equals("Markdown.Avalonia")) .Distinct() .ToArray(); Plugins = new MdAvPlugins(); _setupInfo = null!; _topBlockParsers = null!; _blockParsers = null!; _inlines = null!; SetupParser(); } private void SetupParser() { var info = Plugins.Info; if (ReferenceEquals(info, _setupInfo)) return; var topBlocks = new List(); var subBlocks = new List(); var inlines = new List(); // top-level block parser topBlocks.Add( info.EnableListMarkerExt ? new ExtListParser() : new CommonListParser()); topBlocks.Add(new FencedCodeBlockParser(info.EnablePreRenderingCodeBlock)); if (info.EnableContainerBlockExt) { topBlocks.Add(new ContainerBlockParser()); } // sub-level block parser subBlocks.Add(new BlockquotesParser(info.EnableTextAlignment)); subBlocks.Add(new SetextHeaderParser()); subBlocks.Add(new AtxHeaderParser()); subBlocks.Add( info.EnableRuleExt ? new ExtHorizontalParser() : new CommonHorizontalParser()); if (info.EnableTableBlock) { subBlocks.Add(new TableParser()); } if (info.EnableNoteBlock) { subBlocks.Add(new NoteParser()); } subBlocks.Add(new IndentCodeBlockParser()); // inline parser inlines.Add(InlineParser.New(_codeSpan, nameof(CodeSpanEvaluator), CodeSpanEvaluator)); inlines.Add(InlineParser.New(_imageOrHrefInline, nameof(ImageOrHrefInlineEvaluator), ImageOrHrefInlineEvaluator)); if (StrictBoldItalic) { inlines.Add(InlineParser.New(_strictBold, nameof(BoldEvaluator), BoldEvaluator)); inlines.Add(InlineParser.New(_strictItalic, nameof(ItalicEvaluator), ItalicEvaluator)); if (info.EnableStrikethrough) inlines.Add(InlineParser.New(_strikethrough, nameof(StrikethroughEvaluator), StrikethroughEvaluator)); } // parser registered by plugin topBlocks.AddRange(info.TopBlock.Select(bp => bp.Upgrade())); subBlocks.AddRange(info.Block.Select(bp => bp.Upgrade())); inlines.AddRange(info.Inline); // inform path info to resolver info.PathResolver.AssetPathRoot = AssetPathRoot; info.PathResolver.CallerAssemblyNames = AssetAssemblyNames; info.Overwrite(_hyperlinkCommand); info.Overwrite(_containerBlockHandler); info.Overwrite(_loader); _topBlockParsers = topBlocks.Select(p => info.Override(p).Upgrade()).ToArray(); _blockParsers = subBlocks.Select(p => info.Override(p).Upgrade()).ToArray(); _inlines = inlines.ToArray(); _supportTextAlignment = info.EnableTextAlignment; _supportStrikethrough = info.EnableStrikethrough; _supportTextileInline = info.EnableTextileInline; _setupInfo = info; } public Control Transform(string? text) { return TransformElement(text).Control; } public DocumentElement TransformElement(string? text) { if (text is null) { throw new ArgumentNullException(nameof(text)); } SetupParser(); text = TextUtil.Normalize(text, _tabWidth); var status = new ParseStatus(true & _supportTextAlignment); var elements = ParseGamutElement(text, status); return new DocumentRootElement(elements); } public IEnumerable ParseGamutElement(string? text, ParseStatus status) { if (text is null) { throw new ArgumentNullException(nameof(text)); } SetupParser(); return PrivateRunBlockGamut(text, status); } public IEnumerable ParseGamutInline(string? text) { if (text is null) { throw new ArgumentNullException(nameof(text)); } SetupParser(); return PrivateRunSpanGamut(text); } public IEnumerable RunBlockGamut(string? text, ParseStatus status) { if (text is null) { throw new ArgumentNullException(nameof(text)); } SetupParser(); text = TextUtil.Normalize(text, _tabWidth); var elements = PrivateRunBlockGamut(text, status); return elements.Select(e => e.Control); } public IEnumerable RunSpanGamut(string? text) { if (text is null) { throw new ArgumentNullException(nameof(text)); } SetupParser(); text = TextUtil.Normalize(text, _tabWidth); return PrivateRunSpanGamut(text); } private IEnumerable PrivateRunBlockGamut(string text, ParseStatus status) { var index = 0; var length = text.Length; var rtn = new List(); var candidates = new List>(); for (; ; ) { candidates.Clear(); foreach (var parser in _topBlockParsers) { var match = parser.Pattern.Match(text, index, length); if (match.Success) candidates.Add(new Candidate(match, parser)); } if (candidates.Count == 0) break; candidates.Sort(); int bestBegin = 0; int bestEnd = 0; IEnumerable? result = null; foreach (var c in candidates) { result = c.Parser.Convert2(text, c.Match, status, this, out bestBegin, out bestEnd); if (result is not null) break; } if (result is null) break; if (bestBegin > index) { RunBlockRest(text, index, bestBegin - index, status, 0, rtn); } rtn.AddRange(result); length -= bestEnd - index; index = bestEnd; } if (index < text.Length) { RunBlockRest(text, index, text.Length - index, status, 0, rtn); } return rtn; void RunBlockRest( string text, int index, int length, ParseStatus status, int parserStart, List outto) { for (; parserStart < _blockParsers.Length; ++parserStart) { var parser = _blockParsers[parserStart]; for (; ; ) { var match = parser.Pattern.Match(text, index, length); if (!match.Success) break; var rslt = parser.Convert2(text, match, status, this, out int parseBegin, out int parserEnd); if (rslt is null) break; if (parseBegin > index) { RunBlockRest(text, index, parseBegin - index, status, parserStart + 1, outto); } outto.AddRange(rslt); length -= parserEnd - index; index = parserEnd; } if (length == 0) break; } if (length != 0) { outto.AddRange(FormParagraphs(text.Substring(index, length), status)); } } } private IEnumerable PrivateRunSpanGamut(string text) { var rtn = new List(); RunSpanRest(text, 0, text.Length, 0, rtn); return rtn; void RunSpanRest( string text, int index, int length, int parserStart, List outto) { for (; parserStart < _inlines.Length; ++parserStart) { var parser = _inlines[parserStart]; for (; ; ) { var match = parser.Pattern.Match(text, index, length); if (!match.Success) break; var rslt = parser.Convert(text, match, this, out int parseBegin, out int parserEnd); if (rslt is null) break; if (parseBegin > index) { RunSpanRest(text, index, parseBegin - index, parserStart + 1, outto); } outto.AddRange(rslt); length -= parserEnd - index; index = parserEnd; } if (length == 0) break; } if (length != 0) { var subtext = text.Substring(index, length); outto.AddRange( StrictBoldItalic ? DoText(subtext) : DoTextDecorations(subtext, s => DoText(s))); } } } #region grammer - paragraph private static readonly Regex _align = new(@"^p([<=>])\.", RegexOptions.Compiled); private static readonly Regex _newlinesLeadingTrailing = new(@"^\n+|\n+\z", RegexOptions.Compiled); private static readonly Regex _newlinesMultiple = new(@"\n{2,}", RegexOptions.Compiled); /// /// splits on two or more newlines, to form "paragraphs"; /// private IEnumerable FormParagraphs(string text, ParseStatus status) { var trimemdText = _newlinesLeadingTrailing.Replace(text, ""); string[] grafs = trimemdText == "" ? new string[0] : _newlinesMultiple.Split(trimemdText); foreach (var g in grafs) { var chip = g; TextAlignment? indiAlignment = null; if (status.SupportTextAlignment) { var alignMatch = _align.Match(chip); if (alignMatch.Success) { chip = chip.Substring(alignMatch.Length); switch (alignMatch.Groups[1].Value) { case "<": indiAlignment = TextAlignment.Left; break; case ">": indiAlignment = TextAlignment.Right; break; case "=": indiAlignment = TextAlignment.Center; break; } } } var inlines = PrivateRunSpanGamut(chip); var ctbox = indiAlignment.HasValue ? new CTextBlockElement(inlines, ParagraphClass, indiAlignment.Value) : new CTextBlockElement(inlines, ParagraphClass); yield return ctbox; } } #endregion #region grammer - image or href private static readonly Regex _imageOrHrefInline = new(string.Format(@" ( # wrap whole match in $1 (!)? # image maker = $2 \[ ({0}) # link text = $3 \] \( # literal paren [ ]* ({1}) # href = $4 [ ]* ( # $5 (['""]) # quote char = $6 (.*?) # title = $7 \6 # matching quote [ ]* # ignore any spaces between closing quote and ) )? # title is optional \) )", GetNestedBracketsPattern(), GetNestedParensPattern()), RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); private CInline ImageOrHrefInlineEvaluator(Match match) { if (String.IsNullOrEmpty(match.Groups[2].Value)) { return TreatsAsHref(match); } else { return TreatsAsImage(match); } } private CInline TreatsAsHref(Match match) { string linkText = match.Groups[3].Value; string url = match.Groups[4].Value; string title = match.Groups[7].Value; var link = new CHyperlink(PrivateRunSpanGamut(linkText)) { Command = (urlTxt) => { if (HyperlinkCommand != null && HyperlinkCommand.CanExecute(urlTxt)) { HyperlinkCommand.Execute(urlTxt); } }, CommandParameter = url }; if (!String.IsNullOrEmpty(title) && !title.Any(ch => !Char.IsLetterOrDigit(ch))) { link.Classes.Add(title); } return link; } private CInline TreatsAsImage(Match match) { string altText = match.Groups[3].Value; string urlTxt = match.Groups[4].Value; string title = match.Groups[7].Value; return LoadImage(urlTxt, title); } private CInline LoadImage(string urlTxt, string title) { if (UseResource && CascadeResources.TryGet(urlTxt, out var resourceVal)) { if (resourceVal is Control control) { return new CInlineUIContainer(control); } CImage? cimg = null; if (resourceVal is Bitmap renderedImage) { cimg = new CImage(renderedImage); } if (resourceVal is IEnumerable byteEnum) { try { using (var memstream = new MemoryStream(byteEnum.ToArray())) { var bitmap = new Bitmap(memstream); cimg = new CImage(bitmap); } } catch { } } if (cimg is not null) { if (!String.IsNullOrEmpty(title) && !title.Any(ch => !Char.IsLetterOrDigit(ch))) { cimg.Classes.Add(title); } return cimg; } } CImage image = _setupInfo.LoadImage(urlTxt); if (!String.IsNullOrEmpty(title) && !title.Any(ch => !Char.IsLetterOrDigit(ch))) { image.Classes.Add(title); } return image; } #endregion #region grammer - code // * You can use multiple backticks as the delimiters if you want to // include literal backticks in the code span. So, this input: // // Just type ``foo `bar` baz`` at the prompt. // // Will translate to: // //

Just type foo `bar` baz at the prompt.

// // There's no arbitrary limit to the number of backticks you // can use as delimters. If you need three consecutive backticks // in your code, use four for delimiters, etc. // // * You can use spaces to get literal backticks at the edges: // // ... type `` `bar` `` ... // // Turns to: // // ... type `bar` ... // private static readonly Regex _codeSpan = new(@" (? /// Turn Markdown *italics* and **bold** into HTML strong and em tags /// private IEnumerable DoTextDecorations(string text, Func> defaultHandler) { var rtn = new List(); var buff = new StringBuilder(); void HandleBefore() { if (buff.Length > 0) { rtn.AddRange(defaultHandler(buff.ToString())); buff.Clear(); } } for (var i = 0; i < text.Length; ++i) { var ch = text[i]; switch (ch) { default: buff.Append(ch); break; case '\\': // escape if (++i < text.Length) { switch (text[i]) { default: buff.Append('\\').Append(text[i]); break; case '\\': // escape case ':': // bold? or italic case '*': // bold? or italic case '~': // strikethrough? case '_': // underline? case '%': // color? buff.Append(text[i]); break; } } else buff.Append('\\'); break; case ':': // emoji? { var nxtI = text.IndexOf(':', i + 1); if (nxtI != -1 && EmojiTable.TryGet(text.Substring(i + 1, nxtI - i - 1), out var emoji)) { buff.Append(emoji); i = nxtI; } else buff.Append(':'); break; } case '*': // bold? or italic { var oldI = i; var inline = ParseAsBoldOrItalic(text, ref i); if (inline == null) { buff.Append(text, oldI, i - oldI + 1); } else { HandleBefore(); rtn.Add(inline); } break; } case '~': // strikethrough? { var oldI = i; var inline = ParseAsStrikethrough(text, ref i); if (inline == null) { buff.Append(text, oldI, i - oldI + 1); } else { HandleBefore(); rtn.Add(inline); } break; } case '_': // underline? { var oldI = i; var inline = ParseAsUnderline(text, ref i); if (inline == null) { buff.Append(text, oldI, i - oldI + 1); } else { HandleBefore(); rtn.Add(inline); } break; } case '%': // color? { var oldI = i; var inline = ParseAsColor(text, ref i); if (inline == null) { buff.Append(text, oldI, i - oldI + 1); } else { HandleBefore(); rtn.Add(inline); } break; } } } if (buff.Length > 0) { rtn.AddRange(defaultHandler(buff.ToString())); } return rtn; } private CUnderline? ParseAsUnderline(string text, ref int start) { var bgnCnt = CountRepeat(text, start, '_'); int last = EscapedIndexOf(text, start + bgnCnt, '_'); int endCnt = last >= 0 ? CountRepeat(text, last, '_') : -1; if (endCnt >= 2 && bgnCnt >= 2) { int cnt = 2; int bgn = start + cnt; int end = last; start = end + cnt - 1; var span = new CUnderline(PrivateRunSpanGamut(text.Substring(bgn, end - bgn))); return span; } else { start += bgnCnt - 1; return null; } } private CStrikethrough? ParseAsStrikethrough(string text, ref int start) { var bgnCnt = CountRepeat(text, start, '~'); int last = EscapedIndexOf(text, start + bgnCnt, '~'); int endCnt = last >= 0 ? CountRepeat(text, last, '~') : -1; if (endCnt >= 2 && bgnCnt >= 2) { int cnt = 2; int bgn = start + cnt; int end = last; start = end + cnt - 1; var span = new CStrikethrough(PrivateRunSpanGamut(text.Substring(bgn, end - bgn))); return span; } else { start += bgnCnt - 1; return null; } } private CInline? ParseAsBoldOrItalic(string text, ref int start) { // count asterisk (bgn) var bgnCnt = CountRepeat(text, start, '*'); int last = EscapedIndexOf(text, start + bgnCnt, '*'); int endCnt = last >= 0 ? CountRepeat(text, last, '*') : -1; if (endCnt >= 1) { int cnt = Math.Min(bgnCnt, endCnt); int bgn = start + cnt; int end = last; switch (cnt) { case 1: // italic { start = end + cnt - 1; var span = new CItalic(PrivateRunSpanGamut(text.Substring(bgn, end - bgn))); return span; } case 2: // bold { start = end + cnt - 1; var span = new CBold(PrivateRunSpanGamut(text.Substring(bgn, end - bgn))); return span; } default: // >3; bold-italic { bgn = start + 3; start = end + 3 - 1; var inline = new CItalic(PrivateRunSpanGamut(text.Substring(bgn, end - bgn))); var span = new CBold(new[] { inline }); return span; } } } else { start += bgnCnt - 1; return null; } } private CInline? ParseAsColor(string text, ref int start) { if (start + 1 >= text.Length) return null; if (text[start + 1] != '{') return null; int end = text.IndexOf('}', start + 1); if (end == -1) return null; var styleTxts = text.Substring(start + 2, end - (start + 2)); int bgnIdx = end + 1; int endIdx = EscapedIndexOf(text, bgnIdx, '%'); CSpan span; if (endIdx == -1) { endIdx = text.Length - 1; span = new CSpan(PrivateRunSpanGamut(text.Substring(bgnIdx))); } else { span = new CSpan(PrivateRunSpanGamut(text.Substring(bgnIdx, endIdx - bgnIdx))); } foreach (var styleTxt in styleTxts.Split(';')) { var nameAndVal = styleTxt.Split(':'); if (nameAndVal.Length != 2) return null; var name = nameAndVal[0].Trim(); var colorLbl = nameAndVal[1].Trim(); switch (name) { case "color": try { var color = colorLbl.StartsWith("#") ? (IBrush?)new BrushConverter().ConvertFrom(colorLbl) : (IBrush?)new BrushConverter().ConvertFromString(colorLbl); span.Foreground = color; } catch { } break; case "background": try { var color = colorLbl.StartsWith("#") ? (IBrush?)new BrushConverter().ConvertFrom(colorLbl) : (IBrush?)new BrushConverter().ConvertFromString(colorLbl); span.Background = color; } catch { } break; default: return null; } } start = endIdx; return span; } private int EscapedIndexOf(string text, int start, char target) { for (var i = start; i < text.Length; ++i) { var ch = text[i]; if (ch == '\\') ++i; else if (ch == target) return i; } return -1; } private int CountRepeat(string text, int start, char target) { var count = 0; for (var i = start; i < text.Length; ++i) { if (text[i] == target) ++count; else break; } return count; } private CItalic ItalicEvaluator(Match match) { var content = match.Groups[3].Value; return new CItalic(PrivateRunSpanGamut(content)); } private CBold BoldEvaluator(Match match) { var content = match.Groups[3].Value; return new CBold(PrivateRunSpanGamut(content)); } private CStrikethrough StrikethroughEvaluator(Match match) { var content = match.Groups[2].Value; return new CStrikethrough(PrivateRunSpanGamut(content)); } private CUnderline UnderlineEvaluator(Match match) { var content = match.Groups[2].Value; return new CUnderline(PrivateRunSpanGamut(content)); } #endregion #region grammer - text private static readonly Regex _eoln = new("\\s+"); private static readonly Regex _lbrk = new(@"\ {2,}\n"); private IEnumerable DoText(string text) { var lines = _lbrk.Split(text); bool first = true; foreach (var line in lines) { if (first) first = false; else yield return new CLineBreak(); var t = _eoln.Replace(line, " "); yield return new CRun() { Text = t }; } } #endregion #region helper - make regex /// /// Reusable pattern to match balanced [brackets]. See Friedl's /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. /// private static string GetNestedBracketsPattern() { // in other words [this] and [this[also]] and [this[also[too]]] // up to _nestDepth return RepeatString(@" (?> # Atomic matching [^\[\]]+ # Anything other than brackets | \[ ", _nestDepth) + RepeatString( @" \] )*" , _nestDepth); } /// /// Reusable pattern to match balanced (parens). See Friedl's /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331. /// private static string GetNestedParensPattern() { // in other words (this) and (this(also)) and (this(also(too))) // up to _nestDepth return RepeatString(@" (?> # Atomic matching [^()\n\t]+? # Anything other than parens or whitespace | \( ", _nestDepth) + RepeatString( @" \) )*?" , _nestDepth); } /// /// this is to emulate what's evailable in PHP /// private static string RepeatString(string text, int count) { var sb = new StringBuilder(text.Length * count); for (int i = 0; i < count; i++) sb.Append(text); return sb.ToString(); } #endregion #region helper - parse private TResult Create(IEnumerable content) where TResult : Panel, new() where TContent : Control { var result = new TResult(); foreach (var c in content) { result.Children.Add(c); } return result; } //private IEnumerable Evaluates( // string text, ParseStatus status, // BlockParser[] primary, // BlockParser[] secondly, // Func> rest // ) //{ // var index = 0; // var length = text.Length; // var rtn = new List(); // // while (true) // { // int bestIndex = Int32.MaxValue; // Match? bestMatch = null; // BlockParser? bestParser = null; // // foreach (var parser in primary) // { // var match = parser.Pattern.Match(text, index, length); // if (match.Success && match.Index < bestIndex) // { // bestIndex = match.Index; // bestMatch = match; // bestParser = parser; // } // } // // if (bestParser is null || bestMatch is null) break; // // var result = bestParser.Convert(text, bestMatch, status, this, out bestIndex, out int newIndex); // // if (bestIndex > index) // { // EvaluateRest(rtn, text, index, bestIndex - index, status, secondly, 0, rest); // } // // rtn.AddRange(result); // // length -= newIndex - index; // index = newIndex; // } // // if (index < text.Length) // { // EvaluateRest(rtn, text, index, text.Length - index, status, secondly, 0, rest); // } // // return rtn; // //} // //private void EvaluateRest( // List resultIn, // string text, int index, int length, // ParseStatus status, // BlockParser[] parsers, int parserStart, // Func> rest) //{ // for (; parserStart < parsers.Length; ++parserStart) // { // var parser = parsers[parserStart]; // // for (; ; ) // { // var match = parser.Pattern.Match(text, index, length); // if (!match.Success) break; // // var result = parser.Convert(text, match, status, this, out var matchStartIndex, out int newIndex); // // if (matchStartIndex > index) // { // EvaluateRest(resultIn, text, index, match.Index - index, status, parsers, parserStart + 1, rest); // } // // resultIn.AddRange(result); // // length -= newIndex - index; // index = newIndex; // } // // if (length == 0) break; // } // // if (length != 0) // { // var suffix = text.Substring(index, length); // resultIn.AddRange(rest(suffix, status)); // } //} #endregion } internal struct Candidate : IComparable> { public Match Match { get; } public T Parser { get; } public Candidate(Match result, T parser) { Match = result; Parser = parser; } public int CompareTo(Candidate other) => Match.Index.CompareTo(other.Match.Index); } internal class UnclosableStream : Stream { private Stream _stream; public UnclosableStream(Stream stream) { _stream = stream; } public override bool CanRead => _stream.CanRead; public override bool CanSeek => _stream.CanSeek; public override bool CanWrite => _stream.CanWrite; public override long Length => _stream.Length; public override long Position { get => _stream.Position; set => _stream.Position = value; } public override void Flush() { } public override void Close() { } public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); public override void SetLength(long value) => _stream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } } } ================================================ FILE: Markdown.Avalonia.Tight/MarkdownScrollViewer.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Styling; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.StyleCollections; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using MdStyle = Markdown.Avalonia.MarkdownStyle; namespace Markdown.Avalonia { public class MarkdownScrollViewer : Control { public static readonly DirectProperty SourceDirectProperty = AvaloniaProperty.RegisterDirect( nameof(Source), o => o.Source, (o, v) => o.Source = v); public static readonly AvaloniaProperty SourceProperty = SourceDirectProperty; private static readonly DirectProperty MarkdownDirectProperty = AvaloniaProperty.RegisterDirect( nameof(Markdown), o => o.Markdown, (o, v) => o.Markdown = v); public static readonly AvaloniaProperty MarkdownProperty = MarkdownDirectProperty; private static readonly AvaloniaProperty MarkdownStyleProperty = AvaloniaProperty.RegisterDirect( nameof(MarkdownStyle), o => o.MarkdownStyle, (o, v) => o.MarkdownStyle = v); public static readonly AvaloniaProperty MarkdownStyleNameProperty = AvaloniaProperty.RegisterDirect( nameof(MarkdownStyleName), o => o.MarkdownStyleName, (o, v) => o.MarkdownStyleName = v); public static readonly AvaloniaProperty AssetPathRootProperty = AvaloniaProperty.RegisterDirect( nameof(AssetPathRoot), o => o.AssetPathRoot, (o, v) => o.AssetPathRoot = v); public static readonly StyledProperty SaveScrollValueWhenContentUpdatedProperty = AvaloniaProperty.Register( nameof(SaveScrollValueWhenContentUpdated), defaultValue: false); public static readonly AvaloniaProperty ScrollValueProperty = AvaloniaProperty.RegisterDirect( nameof(ScrollValue), owner => owner.ScrollValue, (owner, v) => owner.ScrollValue = v); public static readonly StyledProperty SelectionBrushProperty = SelectableTextBlock.SelectionBrushProperty.AddOwner(); public static readonly AvaloniaProperty SelectionEnabledProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionEnabled), owner => owner.SelectionEnabled, (owner, v) => owner.SelectionEnabled = v); public static readonly AvaloniaProperty PaddingProperty = AvaloniaProperty.RegisterDirect( nameof(Padding), owner => owner.Padding, (owner, v) => owner.Padding = v); private static readonly HttpClient s_httpclient = new(); private readonly ScrollViewer _viewer; private SetupInfo _setup; private DocumentElement? _document; private IBrush? _selectionBrush; private Wrapper _wrapper; public MarkdownScrollViewer() { _plugins = new MdAvPlugins(); _setup = Plugins.Info; var md = new Markdown(); md.CascadeResources.SetParent(this); md.UseResource = _useResource; md.Plugins = _plugins; _engine = md; if (nvl(ThemeDetector.IsFluentAvaloniaUsed)) { _markdownStyleName = nameof(MdStyle.FluentAvalonia); _markdownStyle = MdStyle.FluentAvalonia; } else if (nvl(ThemeDetector.IsFluentUsed)) { _markdownStyleName = nameof(MdStyle.FluentTheme); _markdownStyle = MdStyle.FluentTheme; } else if (nvl(ThemeDetector.IsSimpleUsed)) { _markdownStyleName = nameof(MdStyle.SimpleTheme); _markdownStyle = MdStyle.SimpleTheme; } else { _markdownStyleName = nameof(MdStyle.Standard); _markdownStyle = MdStyle.Standard; } Styles.Insert(0, _markdownStyle); TrySetupSelectionBrush(_markdownStyle); _viewer = new ScrollViewer() { // TODO: ScrollViewer does not seem to take Padding into account in 11.0.0-preview1 Padding = new Thickness(0), HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, }; ((ISetLogicalParent)_viewer).SetParent(this); VisualChildren.Add(_viewer); LogicalChildren.Add(_viewer); EditStyle(_markdownStyle); static bool nvl(bool? vl) => vl.HasValue && vl.Value; _viewer.ScrollChanged += (s, e) => OnScrollChanged(); _viewer.PointerPressed += _viewer_PointerPressed; _viewer.PointerMoved += _viewer_PointerMoved; _viewer.PointerReleased += _viewer_PointerReleased; _wrapper = new Wrapper(this); _viewer.Content = _wrapper; } #region text selection private bool _isLeftButtonPressed; private Point _startPoint; private void _viewer_PointerPressed(object? sender, PointerPressedEventArgs e) { if (_document is null) return; if (!SelectionEnabled) return; var point = e.GetCurrentPoint(_document.Control); if (point.Properties.IsLeftButtonPressed && _document is not null) { _isLeftButtonPressed = true; _startPoint = point.Position; _document.Select(_startPoint, point.Position); this.Focus(); } } private void _viewer_PointerMoved(object? sender, PointerEventArgs e) { if (_document is null) return; var point = e.GetCurrentPoint(_document.Control); if (_isLeftButtonPressed && point.Properties.IsLeftButtonPressed) { if (_document is not null) _document.Select(_startPoint, point.Position); } } private void _viewer_PointerReleased(object? sender, PointerReleasedEventArgs e) { if (_document is null) return; var point = e.GetCurrentPoint(_document.Control); if (_isLeftButtonPressed && !point.Properties.IsLeftButtonPressed) { _isLeftButtonPressed = false; if (_document is not null) _document.Select(_startPoint, point.Position); } } protected override void OnKeyDown(KeyEventArgs e) { if (!SelectionEnabled) return; // Ctrl+C if (e.Key == Key.C && e.KeyModifiers == KeyModifiers.Control) { if (_document is not null && TopLevel.GetTopLevel(this) is TopLevel top && top.Clipboard is IClipboard clipboard) { clipboard.SetTextAsync(_document.GetSelectedText()); } } } #endregion public event HeaderScrolled? HeaderScrolled; private List? _headerRects; private HeaderScrolledEventArgs? _eventArgs; private void OnViewportSizeChanged(object? obj, EventArgs arg) { _headerRects = null; _wrapper.Restructure(); } private void OnScrollChanged() { if (HeaderScrolled is null) return; double offsetY = _viewer.Offset.Y; double viewHeight = _viewer.Viewport.Height; if (_headerRects is null) { if (_document is null) return; _headerRects = new List(); foreach (var doc in _document.Children.OfType()) { var t = doc.GetRect(this); var rect = new Rect(t.Left, t.Top + offsetY, t.Width, t.Height); _headerRects.Add(new HeaderRect(rect, doc)); } } var tree = new Header?[5]; var viewing = new List
(); tree[0] = _headerRects.Where(rct => rct.Header.Level == 1) .Select(rct => CreateHeader(rct)) .FirstOrDefault(); foreach (var headerRect in _headerRects) { var boundY = headerRect.BaseBound.Bottom - offsetY; if (boundY < 0) { var header = CreateHeader(headerRect); tree[header.Level - 1] = header; for (var i = header.Level; i < tree.Length; ++i) tree[i] = null; } else if (0 <= boundY && boundY < viewHeight) { viewing.Add(CreateHeader(headerRect)); } else break; } var newEvArg = new HeaderScrolledEventArgs(tree.OfType
().ToList(), viewing); if (_eventArgs != newEvArg) { _eventArgs = newEvArg; HeaderScrolled(this, _eventArgs); } static Header CreateHeader(HeaderRect headerRect) { var header = headerRect.Header; return new Header(header.Level, header.Text); } } private void EditStyle(IStyle mdstyle) { if (mdstyle is INamedStyle nameStyle && !nameStyle.IsEditted && mdstyle is Styles styles) { foreach (var edit in _setup.StyleEdits) edit.Edit(nameStyle.Name, styles); nameStyle.IsEditted = true; } } private void TrySetupSelectionBrush(IStyle style) { _selectionBrush = null; var key = "MarkdownScrollViewer.SelectionBrush"; if (style.TryGetResource(key, null, out var brushObj) && brushObj is IBrush brush) { _selectionBrush = brush; } } private void UpdateMarkdown() { if (_wrapper.Document is null && String.IsNullOrEmpty(Markdown)) return; _document = _engine.TransformElement(Markdown ?? ""); _document.Control.Classes.Add("Markdown_Avalonia_MarkdownViewer"); var ofst = _viewer.Offset; if (_wrapper.Document?.Control is Control oldContentControl) { oldContentControl.SizeChanged -= OnViewportSizeChanged; } _wrapper.Document = _document; if (_wrapper.Document?.Control is Control newContentControl) { newContentControl.SizeChanged += OnViewportSizeChanged; } _headerRects = null; if (SaveScrollValueWhenContentUpdated) _viewer.Offset = ofst; } private IMarkdownEngine2 _engine; public IMarkdownEngineBase Engine { set { if (value is null) throw new ArgumentNullException(nameof(Engine)); if (value is IMarkdownEngine engine1) _engine = engine1.Upgrade(); else if (value is IMarkdownEngine2 engine) _engine = engine; else throw new ArgumentException(); _engine.CascadeResources.SetParent(this); _engine.UseResource = _useResource; _engine.Plugins = _plugins; if (AssetPathRoot is not null) _engine.AssetPathRoot = AssetPathRoot; } get => _engine; } private string? _AssetPathRoot; public string? AssetPathRoot { set { if (value is not null) { _engine.AssetPathRoot = _AssetPathRoot = value; UpdateMarkdown(); } } get => _AssetPathRoot; } public bool SaveScrollValueWhenContentUpdated { set { SetValue(SaveScrollValueWhenContentUpdatedProperty, value); } get { return GetValue(SaveScrollValueWhenContentUpdatedProperty); } } public Vector ScrollValue { set { _viewer.Offset = value; } get { return _viewer.Offset; } } private bool _selectionEnabled; public bool SelectionEnabled { set { Focusable = _selectionEnabled = value; } get => _selectionEnabled; } [Content] public string? HereMarkdown { get { return Markdown; } set { if (String.IsNullOrEmpty(value)) { Markdown = value; } else { // like PHP's flexible_heredoc_nowdoc_syntaxes, // The indentation of the closing tag dictates // the amount of whitespace to strip from each line var lines = Regex.Split(value, "\r\n|\r|\n", RegexOptions.Multiline); // count last line indent int lastIdtCnt = TextUtil.CountIndent(lines.Last()); // count full indent int someIdtCnt = lines .Where(line => !String.IsNullOrWhiteSpace(line)) .Select(line => TextUtil.CountIndent(line)) .Min(); var indentCount = Math.Max(lastIdtCnt, someIdtCnt); Markdown = String.Join( "\n", lines // skip first blank line .Skip(String.IsNullOrWhiteSpace(lines[0]) ? 1 : 0) // strip indent .Select(line => { var realIdx = 0; var viewIdx = 0; while (viewIdx < indentCount && realIdx < line.Length) { var c = line[realIdx]; if (c == ' ') { realIdx += 1; viewIdx += 1; } else if (c == '\t') { realIdx += 1; viewIdx = ((viewIdx >> 2) + 1) << 2; } else break; } return line.Substring(realIdx); }) ); } } } private string? _markdown; public string? Markdown { get { return _markdown; } set { if (SetAndRaise(MarkdownDirectProperty, ref _markdown, value)) { UpdateMarkdown(); } } } private Uri? _source; public Uri? Source { get { return _source; } set { if (!SetAndRaise(SourceDirectProperty, ref _source, value)) return; if (value is null) { _source = value; Markdown = null; return; } if (!value.IsAbsoluteUri) throw new ArgumentException("it is not absolute."); _source = value; switch (_source.Scheme) { case "http": case "https": using (var res = s_httpclient.GetAsync(_source).Result) using (var strm = res.Content.ReadAsStreamAsync().Result) using (var reader = new StreamReader(strm, true)) Markdown = reader.ReadToEnd(); break; case "file": using (var strm = File.OpenRead(_source.LocalPath)) using (var reader = new StreamReader(strm, true)) Markdown = reader.ReadToEnd(); break; case "avares": using (var strm = AssetLoader.Open(_source)) using (var reader = new StreamReader(strm, true)) Markdown = reader.ReadToEnd(); break; default: throw new ArgumentException($"unsupport schema {_source.Scheme}"); } AssetPathRoot = value.Scheme == "file" ? value.LocalPath : value.AbsoluteUri; } } private IStyle _markdownStyle; public IStyle MarkdownStyle { get { return _markdownStyle; } set { if (value is null) throw new ArgumentNullException(nameof(MarkdownStyle)); if (_markdownStyle != value) { EditStyle(value); if (_markdownStyle is not null) Styles.Remove(_markdownStyle); Styles.Insert(0, value); TrySetupSelectionBrush(value); //ResetContent(); } _markdownStyle = value; } } private string? _markdownStyleName; public string? MarkdownStyleName { get { return _markdownStyleName; } set { _markdownStyleName = value; if (_markdownStyleName is null) { MarkdownStyle = nvl(ThemeDetector.IsFluentAvaloniaUsed) ? MdStyle.FluentAvalonia : nvl(ThemeDetector.IsFluentUsed) ? MdStyle.FluentTheme : nvl(ThemeDetector.IsSimpleUsed) ? MdStyle.SimpleTheme : MdStyle.Standard; } else if (_markdownStyleName == "Empty") { MarkdownStyle = new Styles(); } else { var prop = typeof(MarkdownStyle).GetProperty(_markdownStyleName); if (prop is null) return; var propVal = prop.GetValue(null) as IStyle; if (propVal is null) return; MarkdownStyle = propVal; } static bool nvl(bool? vl) => vl.HasValue && vl.Value; } } private MdAvPlugins _plugins; public MdAvPlugins Plugins { get => _plugins; set { _plugins = _engine.Plugins = value; _setup = Plugins.Info; EditStyle(MarkdownStyle); UpdateMarkdown(); } } private bool _useResource; public bool UseResource { get => _useResource; set { _engine.UseResource = value; _useResource = value; UpdateMarkdown(); } } public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); set => SetValue(SelectionBrushProperty, value); } public Thickness Padding { get => _viewer.Padding; set => _viewer.Padding = value; } internal IBrush ComputedSelectionBrush => SelectionBrush ?? _selectionBrush ?? Brushes.Cyan; class HeaderRect { public Rect BaseBound { get; } public HeaderElement Header { get; } public HeaderRect(Rect bound, HeaderElement header) { BaseBound = bound; Header = header; } } class Wrapper : Control, ISelectionRenderHelper { private MarkdownScrollViewer _viewer; private readonly Canvas _canvas; private readonly Dictionary _rects; private DocumentElement? _document; public DocumentElement? Document { get => _document; set { if (_document is not null) { VisualChildren.Remove(_document.Control); LogicalChildren.Remove(_document.Control); _document.Helper = null; Clear(); } _document = value; if (_document is not null) { VisualChildren.Insert(0, _document.Control); LogicalChildren.Insert(0, _document.Control); _document.Helper = this; InvalidateMeasure(); } } } public Wrapper(MarkdownScrollViewer v) { _viewer = v; _canvas = new Canvas(); _canvas.PointerPressed += (s, e) => _document?.UnSelect(); _rects = new Dictionary(); VisualChildren.Add(_canvas); } public void Register(Control control) { if (!_rects.ContainsKey(control)) { var brush = _viewer.ComputedSelectionBrush; var bounds = GetRectInDoc(control); var rect = new Rectangle() { Width = bounds.Value.Width, Height = bounds.Value.Height, Fill = brush, Opacity = .5 }; Canvas.SetLeft(rect, bounds.Value.Left); Canvas.SetTop(rect, bounds.Value.Top); _rects[control] = rect; _canvas.Children.Add(rect); } } public void Unregister(Control control) { if (_rects.TryGetValue(control, out var rct)) { _canvas.Children.Remove(rct); _rects.Remove(control); } } public void Clear() { _canvas.Children.Clear(); _rects.Clear(); } public void Restructure() { foreach (var rct in _rects) { var boundN = GetRectInDoc(rct.Key); if (boundN.HasValue) { var bound = boundN.Value; rct.Value.Width = bound.Width; rct.Value.Height = bound.Height; Canvas.SetLeft(rct.Value, bound.Left); Canvas.SetTop(rct.Value, bound.Top); } } } public Rect? GetRectInDoc(Control control) { if (!LayoutInformation.GetPreviousArrangeBounds(control).HasValue) return null; double driftX = 0; double driftY = 0; StyledElement? c; for (c = control.Parent; c is not null && c is Layoutable layoutable && !ReferenceEquals(_document.Control, layoutable); c = c.Parent) { driftX += layoutable.Bounds.X; driftY += layoutable.Bounds.Y; } return new Rect( control.Bounds.X + driftX, control.Bounds.Y + driftY, control.Bounds.Width, control.Bounds.Height); } } } } ================================================ FILE: Markdown.Avalonia.Tight/MarkdownStyle.cs ================================================ using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; using Markdown.Avalonia.StyleCollections; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Text; namespace Markdown.Avalonia { public static class MarkdownStyle { public static Styles Standard { get => new MarkdownStyleStandard(); } [Obsolete("Use SimpleTheme instead")] public static Styles DefaultTheme { get => SimpleTheme; } public static Styles SimpleTheme { get => new MarkdownStyleDefaultTheme(); } public static Styles FluentTheme { get => new MarkdownStyleFluentTheme(); } public static Styles FluentAvalonia { get => new MarkdownStyleFluentAvalonia(); } public static Styles GithubLike { get => new MarkdownStyleGithubLike(); } } } ================================================ FILE: Markdown.Avalonia.Tight/MdAvPlugins.cs ================================================ using Avalonia.Metadata; using Markdown.Avalonia.Plugins; using Markdown.Avalonia.Utils; using Microsoft.VisualBasic; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using System.Windows.Input; namespace Markdown.Avalonia { public class MdAvPlugins { private SetupInfo? _cache; private IPathResolver? _pathResolver; private IContainerBlockHandler? _containerBlockHandler; private ICommand? _hyperlinkCommand; [Content] public ObservableCollection Plugins { get; } public IPathResolver? PathResolver { get => _pathResolver; set { _cache = null; _pathResolver = value; } } public IContainerBlockHandler? ContainerBlockHandler { get => _containerBlockHandler; set { _cache = null; _containerBlockHandler = value; } } public ICommand? HyperlinkCommand { get => _hyperlinkCommand; set { _cache = null; _hyperlinkCommand = value; } } public SetupInfo Info => _cache ??= CreateInfo(); public MdAvPlugins() { Plugins = new ObservableCollection(); Plugins.CollectionChanged += (s, e) => _cache = null; } protected virtual SetupInfo CreateInfo() { var setupInf = new SetupInfo(); bool hasBuiltin = false; ( IEnumerable orderedPlugin, Dictionary dic ) = ComputeOrderedPlugins(); foreach (var plugin in orderedPlugin) { if (plugin is IMdAvPluginRequestAnother another) { another.Inject(another.DependsOn.Select(dep => dic[dep])); } plugin.Setup(setupInf); hasBuiltin |= plugin.GetType().FullName == SetupInfo.BuiltinTpNm; } if (!hasBuiltin) setupInf.Builtin(); if (PathResolver is not null) setupInf.SetOnce(PathResolver); if (ContainerBlockHandler is not null) setupInf.SetOnce(ContainerBlockHandler); if (HyperlinkCommand is not null) setupInf.SetOnce(HyperlinkCommand); setupInf.Freeze(); return setupInf; } protected (IEnumerable, Dictionary) ComputeOrderedPlugins() { var plugins = new List(Plugins.Count); var dic = Plugins.ToDictionary(p => p.GetType(), p => p); foreach (var plugin in Plugins) { if (plugin is IMdAvPluginRequestAnother anoth) { ComputeDepneds(anoth, plugins, dic); } plugins.Add(plugin); } plugins.Sort(new PluginComparer(dic)); return (plugins, dic); void ComputeDepneds(IMdAvPluginRequestAnother anoth, List plugins, Dictionary dic) { foreach (var dep in anoth.DependsOn) { if (dic.ContainsKey(dep)) continue; var depPlugin = Activator.CreateInstance(dep) as IMdAvPlugin; if (depPlugin is null) throw new NullReferenceException("Failed to create an instance of " + dep); if (depPlugin is IMdAvPluginRequestAnother anothanoth) ComputeDepneds(anothanoth, plugins, dic); plugins.Add(depPlugin); dic[dep] = depPlugin; } } } class PluginComparer : IComparer { private Dictionary _plugins; public PluginComparer(Dictionary plugins) { _plugins = plugins; } public int Compare(IMdAvPlugin? x, IMdAvPlugin? y) { if (x is IMdAvPluginRequestAnother xanoth && y is IMdAvPluginRequestAnother yanoth) { return // dose x request y? ComputeRequest(xanoth, yanoth) ? -1 : // dose y request x? ComputeRequest(yanoth, xanoth) ? 1 : 0; } if (x is not IMdAvPluginRequestAnother && y is not IMdAvPluginRequestAnother) return 0; if (x is IMdAvPluginRequestAnother && y is not IMdAvPluginRequestAnother) return 1; if (x is not IMdAvPluginRequestAnother && y is IMdAvPluginRequestAnother) return -1; throw new NotImplementedException(); } private bool ComputeRequest(IMdAvPluginRequestAnother x, IMdAvPluginRequestAnother y) { foreach (var depType in x.DependsOn) { var depPlugin = _plugins[depType]; if (ReferenceEquals(depPlugin, y)) return true; if (depPlugin is IMdAvPluginRequestAnother depPluginAnoth && ComputeRequest(depPluginAnoth, y)) return true; } return false; } } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/BlockParser.cs ================================================ using Avalonia.Controls; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers { public delegate Control? ParseWithPositionChange(string text, Match match, out int parseTextBegin, out int parseTextEnd); public abstract class BlockParser { public Regex Pattern { get; } public string Name { get; } public BlockParser(Regex pattern, string name) { Pattern = pattern; Name = name; } public abstract IEnumerable? Convert( string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd); public static BlockParser New(Regex pattern, string name, Func v1) => new Single(pattern, name, v1); public static BlockParser New(Regex pattern, string name, Func v2) => new Single2(pattern, name, v2); public static BlockParser New(Regex pattern, string name, Func?> v2) => new Multi(pattern, name, v2); public static BlockParser New(Regex pattern, string name, Func?> v2) => new Multi2(pattern, name, v2); public static BlockParser New(Regex pattern, string name, ParseWithPositionChange v2) => new ParsePosChange(pattern, name, v2); abstract class Wrapper : BlockParser { public Wrapper(Regex pattern, string name) : base(pattern, name) { } public override IEnumerable? Convert( string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; return Convert(firstMatch, status); } public abstract IEnumerable? Convert(Match match, ParseStatus status); } sealed class Single : Wrapper { private readonly Func converter; public Single(Regex pattern, string name, Func converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable? Convert(Match match, ParseStatus status) { return converter(match) is Control ctrl ? new[] { ctrl } : null; } } sealed class Single2 : Wrapper { private readonly Func converter; public Single2(Regex pattern, string name, Func converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable? Convert(Match match, ParseStatus status) { return converter(match, status) is Control ctrl ? new[] { ctrl } : null; } } sealed class Multi : Wrapper { private readonly Func?> converter; public Multi(Regex pattern, string name, Func?> converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable? Convert(Match match, ParseStatus status) => converter(match); } sealed class Multi2 : Wrapper { private readonly Func?> converter; public Multi2(Regex pattern, string name, Func?> converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable? Convert(Match match, ParseStatus status) => converter(match, status); } sealed class ParsePosChange : BlockParser { private ParseWithPositionChange converter; public ParsePosChange(Regex pattern, string name, ParseWithPositionChange converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable? Convert(string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { return converter(text, firstMatch, out parseTextBegin, out parseTextEnd) is Control ctrl ? new[] { ctrl } : null; } } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/BlockParser2.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers { public abstract class BlockParser2 : BlockParser { public BlockParser2(Regex pattern, string name) : base(pattern, name) { } public abstract IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd); public override IEnumerable? Convert(string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { if (Convert2(text, firstMatch, status, engine.Upgrade(), out parseTextBegin, out parseTextEnd) is { } docs) { return docs.Select(d => d.Control); } return null; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/BlockParserExt.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Input; namespace Markdown.Avalonia.Parsers { public static class BlockParserExt { public static BlockParser2 Upgrade(this BlockParser parser) { return parser is BlockParser2 parser2 ? parser2 : new BlockParserUpg(parser); } class BlockParserUpg : BlockParser2 { private BlockParser _parser; public BlockParserUpg(BlockParser parser) : base(parser.Pattern, parser.Name) { _parser = parser; } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine2, out int parseTextBegin, out int parseTextEnd) { IMarkdownEngine engine = engine2 is IMarkdownEngine e ? e : new MarkdownEngineDng(engine2); var rtn = _parser.Convert(text, firstMatch, status, engine, out parseTextBegin, out parseTextEnd); return rtn.Select(c => new UnBlockElement(c)); } } class MarkdownEngineDng : IMarkdownEngine { private IMarkdownEngine2 _engine; public string AssetPathRoot { get => _engine.AssetPathRoot; set => _engine.AssetPathRoot = value; } public ICommand? HyperlinkCommand { get => _engine.HyperlinkCommand; set => _engine.HyperlinkCommand = value; } public IBitmapLoader? BitmapLoader { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public IContainerBlockHandler? ContainerBlockHandler { get => _engine.ContainerBlockHandler; set => _engine.ContainerBlockHandler = value; } public MdAvPlugins Plugins { get => _engine.Plugins; set => _engine.Plugins = value; } public bool UseResource { get => _engine.UseResource; set => _engine.UseResource = value; } public CascadeDictionary CascadeResources => _engine.CascadeResources; public IResourceDictionary Resources { get => _engine.Resources; set => _engine.Resources = value; } public MarkdownEngineDng(IMarkdownEngine2 engine2) { _engine = engine2; } public IEnumerable RunBlockGamut(string? text, ParseStatus status) { if (text is null) { throw new ArgumentNullException(nameof(text)); } return _engine.ParseGamutElement(TextUtil.Normalize(text), status).Select(e => e.Control); } public IEnumerable RunSpanGamut(string? text) { if (text is null) { throw new ArgumentNullException(nameof(text)); } return _engine.ParseGamutInline(TextUtil.Normalize(text)); } public Control Transform(string text) { return _engine.Transform(text); } } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/AbstractHeaderParser.cs ================================================ using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal abstract class AbstractHeaderParser : BlockParser2 { protected AbstractHeaderParser(Regex pattern, string name) : base(pattern, name) { } protected IEnumerable Create(int level, string header, IMarkdownEngine2 engine) { var inlines = engine.ParseGamutInline(header.Trim()); var element = new HeaderElement(inlines, level); return new[] { element }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/AbstractHorizontalParser.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using Markdown.Avalonia.Controls; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal abstract class AbstractHorizontalParser : BlockParser2 { protected AbstractHorizontalParser(Regex pattern) : base(pattern, "RuleEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; return new[] { new UnBlockElement(RuleEvaluator(firstMatch)) }; } public override IEnumerable? Convert(string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; return new[] { RuleEvaluator(firstMatch) }; } private static Rule RuleEvaluator(Match match) { return match.Groups[1].Value switch { "=" => new Rule(RuleType.TwoLines), "*" => new Rule(RuleType.Bold), "_" => new Rule(RuleType.BoldWithSingle), "-" => new Rule(RuleType.Single), _ => new Rule(RuleType.Single), }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/AbstractListParser.cs ================================================ using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Markdown.Avalonia.Parsers.Builtin { internal abstract class AbstractListParser : BlockParser2 { // Unordered List private const string _markerUL_Disc = @"[*]"; private const string _markerUL_Box = @"[+]"; private const string _markerUL_Circle = @"[-]"; private const string _markerUL_Square = @"[=]"; // Ordered List private const string _markerOL_Number = @"\d+[.]"; private const string _markerOL_LetterLower = @"[a-c][.]"; private const string _markerOL_LetterUpper = @"[A-C][.]"; private const string _markerOL_RomanLower = @"[cdilmvx]+[,]"; private const string _markerOL_RomanUpper = @"[CDILMVX]+[,]"; /// /// Maximum number of levels a single list can have. /// In other words, _listDepth - 1 is the maximum number of nested lists. /// private const int _listDepth = 4; private static readonly string _firstListLine = @" ^ (?![ ]{{0,3}}(?[-=*_])([ ]{{0,2}}\k){{2,}}[ ]*\n) # ignore horizontal syntax (?[ ]{{0,{2}}}) # indent (?{0}) # first list item marker [ ]+ (?:[^ \n]|\n) "; private static readonly string _ruleLine = @" \G [ ]{{0,{0}}} (?[-=*_])([ ]{{0,2}}\k){{2,}}[ ]*\n"; private static readonly string _listLine = @" \G (?[ ]{{0,{0}}}) {1} # list marker [ ]+ (?[^\n]*) # content "; private static readonly Regex _startQuoteOrHeader = new(@" \G(\#{1,6}[ ]|>|```)", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); protected AbstractListParser(Regex pattern) : base(pattern, "ListEvaluator") { } protected static Regex CreateWholeListPattern( string firstListMarkerPattern, string subseqListMarkerPattern) { var format = string.Format(_firstListLine, firstListMarkerPattern, subseqListMarkerPattern, _listDepth - 1); return new Regex(format, RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); } protected ListBlockElement ListEvalutor( string text, Match match, string alllistMarkersPattern, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd ) { parseTextBegin = match.Index; // Check text marker style. (TextMarkerStyle textMarker, string markerPattern, int indentAppending) = GetTextMarkerStyle(match.Groups["marker"].Value); // count indent from first marker with indent int countIndent = match.Groups["indent"].Value.Length; var ruleLinePtn = new Regex( String.Format(_ruleLine, countIndent + indentAppending - 1), RegexOptions.IgnorePatternWhitespace); var listLinePtn = new Regex( String.Format(_listLine, countIndent + indentAppending - 1, markerPattern), RegexOptions.IgnorePatternWhitespace); var allListLinePtn = new Regex( String.Format(_listLine, countIndent + indentAppending - 1, alllistMarkersPattern), RegexOptions.IgnorePatternWhitespace); int caret = parseTextBegin; var listItems = new List(); var hasEmptyLine = false; var listBuilder = new StringBuilder(); while (caret < text.Length) { // is it blank line? if (text[caret] == '\n') { listBuilder.Append('\n'); hasEmptyLine = true; ++caret; } // is it horizontal line? else if (ruleLinePtn.IsMatch(text, caret)) { break; } // is it header or blockquote? else if (_startQuoteOrHeader.IsMatch(text, caret)) { break; } else { // Dose it have list marker? var listLineMch = listLinePtn.Match(text, caret); if (listLineMch.Success) { // next list item? if (listBuilder.Length > 0) { TrimEnd(listBuilder); var elements = engine.ParseGamutElement(listBuilder.ToString(), new ParseStatus(false)); listItems.Add(new ListItemElement(elements)); listBuilder.Length = 0; } listBuilder.Append(listLineMch.Groups["content"].Value); caret += listLineMch.Length; if (caret < text.Length && text[caret] == '\n') { ++caret; listBuilder.Append('\n'); } hasEmptyLine = false; } // Dose it have other list marker? else if (allListLinePtn.IsMatch(text, caret)) { break; } else { var st = caret; if (!MoveIndent(text, countIndent + indentAppending, ref st)) { if (hasEmptyLine) break; } caret = st; if (caret < text.Length) { MoveLineEnd(text, ref caret); listBuilder.Append(text, st, caret - st); if (caret < text.Length && text[caret] == '\n') { ++caret; listBuilder.Append('\n'); } } } } } if (listBuilder.Length > 0) { TrimEnd(listBuilder); var elements = engine.ParseGamutElement(listBuilder.ToString(), new ParseStatus(false)); listItems.Add(new ListItemElement(elements)); ; } parseTextEnd = caret; return new ListBlockElement(textMarker.Change(), listItems); } private static void TrimEnd(StringBuilder text) { if (text.Length < 2) return; if (text[text.Length - 1] != '\n') return; int len = text.Length; while (len >= 2) { if (text[len - 2] == '\n') len--; else break; } text.Length = len; } private static bool MoveIndent(string text, int count, ref int caret) { for (var i = 0; i < count; ++i) { if (caret == text.Length) return false; if (text[caret] != ' ') return false; ++caret; } return true; } private static bool MoveLineEnd(string text, ref int caret) { for (; caret < text.Length; ++caret) { if (text[caret] == '\n') { return true; } } return false; } /// /// Get the text marker style based on a specific regex. /// /// list maker (eg. * + 1. a. /// /// 1; return Type. /// 2: match regex pattern /// 3: char length of listmaker /// private static (TextMarkerStyle, string, int) GetTextMarkerStyle(string markerText) { if (Regex.IsMatch(markerText, _markerUL_Disc)) { return (TextMarkerStyle.Disc, _markerUL_Disc, 2); } else if (Regex.IsMatch(markerText, _markerUL_Box)) { return (TextMarkerStyle.Box, _markerUL_Box, 2); } else if (Regex.IsMatch(markerText, _markerUL_Circle)) { return (TextMarkerStyle.Circle, _markerUL_Circle, 2); } else if (Regex.IsMatch(markerText, _markerUL_Square)) { return (TextMarkerStyle.Square, _markerUL_Square, 2); } else if (Regex.IsMatch(markerText, _markerOL_Number)) { return (TextMarkerStyle.Decimal, _markerOL_Number, 3); } else if (Regex.IsMatch(markerText, _markerOL_LetterLower)) { return (TextMarkerStyle.LowerLatin, _markerOL_LetterLower, 3); } else if (Regex.IsMatch(markerText, _markerOL_LetterUpper)) { return (TextMarkerStyle.UpperLatin, _markerOL_LetterUpper, 3); } else if (Regex.IsMatch(markerText, _markerOL_RomanLower)) { return (TextMarkerStyle.LowerRoman, _markerOL_RomanLower, 3); } else if (Regex.IsMatch(markerText, _markerOL_RomanUpper)) { return (TextMarkerStyle.UpperRoman, _markerOL_RomanUpper, 3); } Helper.ThrowInvalidOperation("sorry library manager forget to modify about listmerker."); // dummy return (TextMarkerStyle.Disc, _markerUL_Disc, 2); } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/AtxHeaderParser.cs ================================================ using ColorDocument.Avalonia; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class AtxHeaderParser : AbstractHeaderParser { /// /// # Header 1 /// ## Header 2 /// ## Header 2 with closing hashes ## /// ... /// ###### Header 6 /// private static readonly Regex _headerAtx = new(@" ^(\#{1,6}) # $1 = string of #'s [ ]* (.+?) # $2 = Header text [ ]* \#* # optional closing #'s (not counted) \n+", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); public AtxHeaderParser() : base(_headerAtx, "AtxHeaderEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; string header = firstMatch.Groups[2].Value; int level = firstMatch.Groups[1].Value.Length; return Create(level, header, engine); } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/BlockquotesParser.cs ================================================ using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Markdown.Avalonia.Parsers.Builtin { internal class BlockquotesParser : BlockParser2 { private static readonly Regex _blockquoteFirst = new(@" ^ ([>].*) (\n[>].*)* [\n]* ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); private bool _supportTextAlignment; public BlockquotesParser(bool supportTextAlignment) : base(_blockquoteFirst, "BlockquotesEvaluator") { _supportTextAlignment = supportTextAlignment; } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = firstMatch.Index + firstMatch.Length; // trim '>' var trimmedTxt = string.Join( "\n", firstMatch.Value.Trim().Split('\n') .Select(txt => { if (txt.Length <= 1) return string.Empty; var trimmed = txt.Substring(1); if (trimmed.FirstOrDefault() == ' ') trimmed = trimmed.Substring(1); return trimmed; }) .ToArray() ); var newStatus = new ParseStatus(true & _supportTextAlignment); var blocks = engine.ParseGamutElement(trimmedTxt + "\n", newStatus); return new[] { new BlockquoteElement(blocks) }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/CommonHorizontalParser.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Markdown.Avalonia.Parsers.Builtin { internal class CommonHorizontalParser : AbstractHorizontalParser { private static readonly Regex _horizontalCommonRules = new(@" ^[ ]{0,3} # Leading space ([-*_]) # $1: First marker ([markers]) (?> # Repeated marker group [ ]{0,2} # Zero, one, or two spaces. \1 # Marker character ){2,} # Group repeated at least twice [ ]* # Trailing spaces \n # End of line. ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); public CommonHorizontalParser() : base(_horizontalCommonRules) { } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/CommonListParser.cs ================================================ using ColorDocument.Avalonia; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class CommonListParser : AbstractListParser { private const string _commonListMaker = @"(?:[*+-]|\d+[.])"; private static readonly Regex _commonListNested = CreateWholeListPattern(_commonListMaker, _commonListMaker); public CommonListParser() : base(_commonListNested) { } public override IEnumerable? Convert2( string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { return new DocumentElement[] { ListEvalutor(text, firstMatch, _commonListMaker, engine, out parseTextBegin, out parseTextEnd) }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/ContainerBlockParser.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class ContainerBlockParser : BlockParser2 { private static readonly Regex _containerBlockFirst = new(@" ^ # Character before opening [ ]{0,3} (:{3,}) # $1 = Opening run of ` ([^\n`]*) # $2 = The container type \n ((.|\n)+?) # $3 = The code block \n[ ]* \1 (?!:)[\n]+", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); public ContainerBlockParser() : base(_containerBlockFirst, "ContainerBlockEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; var name = firstMatch.Groups[2].Value; var content = firstMatch.Groups[3].Value; var gen = engine.ContainerBlockHandler; if (gen?.ProvideControl(engine.AssetPathRoot, name, content) is { } container) { container.Classes.Add(Markdown.ContainerBlockClass); return new[] { new UnBlockElement(container) }; } else { Border border = FencedCodeBlockParser.Create(firstMatch.Value); border.Classes.Add(Markdown.NoContainerClass); return new[] { new UnBlockElement(border) }; } } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/ExtHorizontalParser.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Markdown.Avalonia.Parsers.Builtin { internal class ExtHorizontalParser : AbstractHorizontalParser { /// /// Turn Markdown horizontal rules into HTML hr tags /// /// /// *** /// * * * /// --- /// - - - /// private static readonly Regex _horizontalRules = new(@" ^[ ]{0,3} # Leading space ([-=*_]) # $1: First marker ([markers]) (?> # Repeated marker group [ ]{0,2} # Zero, one, or two spaces. \1 # Marker character ){2,} # Group repeated at least twice [ ]* # Trailing spaces \n # End of line. ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); public ExtHorizontalParser() : base(_horizontalRules) { } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/ExtListParser.cs ================================================ using ColorDocument.Avalonia; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class ExtListParser : AbstractListParser { // `alphabet order` and `roman number` must start 'a.'~'c.' and 'i,'~'iii,'. // This restrict is avoid to treat "Yes," as list marker. private const string _extFirstListMaker = @"(?:[*+=-]|\d+[.]|[a-c][.]|[i]{1,3}[,]|[A-C][.]|[I]{1,3}[,])"; private const string _extSubseqListMaker = @"(?:[*+=-]|\d+[.]|[a-c][.]|[cdilmvx]+[,]|[A-C][.]|[CDILMVX]+[,])"; private static readonly Regex _extListNested = CreateWholeListPattern(_extFirstListMaker, _extSubseqListMaker); public ExtListParser() : base(_extListNested) { } public override IEnumerable? Convert2( string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { return new DocumentElement[] { ListEvalutor(text, firstMatch, _extSubseqListMaker, engine, out parseTextBegin, out parseTextEnd) }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/FencedCodeBlockParser.cs ================================================ using Avalonia.Controls.Primitives; using Avalonia.Controls; using Avalonia.Media; using System.Collections.Generic; using System.Text.RegularExpressions; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; namespace Markdown.Avalonia.Parsers.Builtin { internal class FencedCodeBlockParser : BlockParser2 { private static readonly Regex _codeBlockBegin = new(@" ^ # Character before opening [ ]{0,3} (`{3,}) # $1 = Opening run of ` ([^\n`]*) # $2 = The code lang \n", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); private bool _enablePreRenderingCodeBlock; public FencedCodeBlockParser(bool enablePreRenderingCodeBlock) : base(_codeBlockBegin, "CodeBlocksWithLangEvaluator") { _enablePreRenderingCodeBlock = enablePreRenderingCodeBlock; } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { var closeTagPattern = new Regex($"\n[ ]*{firstMatch.Groups[1].Value}[ ]*\n"); var closeTagMatch = closeTagPattern.Match(text, firstMatch.Index + firstMatch.Length); int codeEndIndex; if (closeTagMatch.Success) { codeEndIndex = closeTagMatch.Index; parseTextEnd = closeTagMatch.Index + closeTagMatch.Length; } else if (_enablePreRenderingCodeBlock) { codeEndIndex = text.Length; parseTextEnd = text.Length; } else { parseTextBegin = parseTextEnd = -1; return null; } parseTextBegin = firstMatch.Index; string code = text.Substring(firstMatch.Index + firstMatch.Length, codeEndIndex - (firstMatch.Index + firstMatch.Length)); var border = Create(code); return new[] { new UnBlockElement(border) }; } public static Border Create(string code) { var ctxt = new TextBlock() { Text = code, TextWrapping = TextWrapping.NoWrap }; ctxt.Classes.Add(Markdown.CodeBlockClass); var scrl = new ScrollViewer(); scrl.Classes.Add(Markdown.CodeBlockClass); scrl.Content = ctxt; scrl.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; var border = new Border(); border.Classes.Add(Markdown.CodeBlockClass); border.Child = scrl; return border; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/IndentCodeBlockParser.cs ================================================ using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class IndentCodeBlockParser : BlockParser2 { private static readonly Regex _newlinesLeadingTrailing = new(@"^\n+|\n+\z", RegexOptions.Compiled); private static readonly Regex _indentCodeBlock = new(@" (?:\A|^[ ]*\n) ( [ ]{4}.+ (\n([ ]{4}.+|[ ]*))* \n? ) ", RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline | RegexOptions.Compiled); public IndentCodeBlockParser() : base(_indentCodeBlock, "CodeBlocksWithoutLangEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = firstMatch.Index + firstMatch.Length; var detentTxt = String.Join("\n", firstMatch.Groups[1].Value.Split('\n').Select(line => TextUtil.DetentLineBestEffort(line, 4))); var border = FencedCodeBlockParser.Create(_newlinesLeadingTrailing.Replace(detentTxt, "")); return new[] { new UnBlockElement(border) }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/NoteParser.cs ================================================ using Avalonia.Media; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Markdown.Avalonia.Parsers.Builtin { internal class NoteParser : BlockParser2 { /// /// Turn Markdown into HTML paragraphs. /// /// /// < Note /// private static readonly Regex _note = new(@" ^(\<) # $1 = starting marker < [ ]* (.+?) # $2 = Header text [ ]* \>* # optional closing >'s (not counted) \n+ ", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); private static readonly Regex _align = new(@"^p([<=>])\.", RegexOptions.Compiled); public NoteParser() : base(_note, "NoteEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; string content = firstMatch.Groups[2].Value; TextAlignment? indiAlignment = null; if (status.SupportTextAlignment) { var alignMatch = _align.Match(content); if (alignMatch.Success) { content = content.Substring(alignMatch.Length); switch (alignMatch.Groups[1].Value) { case "<": indiAlignment = TextAlignment.Left; break; case ">": indiAlignment = TextAlignment.Right; break; case "=": indiAlignment = TextAlignment.Center; break; } } } return new[] { new NoteBlockElement(engine.ParseGamutInline(content), indiAlignment) }; } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/SetextHeaderParser.cs ================================================ using ColorDocument.Avalonia; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class SetextHeaderParser : AbstractHeaderParser { /// /// Turn Markdown headers into HTML header tags /// /// /// Header 1 /// ======== /// /// Header 2 /// -------- /// private static readonly Regex _headerSetext = new(@" ^(.+?) [ ]* \n (=+|-+) # $1 = string of ='s or -'s [ ]* \n+", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled); public SetextHeaderParser() : base(_headerSetext, "SetextHeaderEvaluator") { } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; string header = firstMatch.Groups[1].Value; int level = firstMatch.Groups[2].Value.StartsWith("=") ? 1 : 2; return Create(level, header, engine); } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/Builtin/TableParser.cs ================================================ using Avalonia.Layout; using Avalonia.Media; using ColorDocument.Avalonia; using ColorDocument.Avalonia.DocumentElements; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers.Builtin { internal class TableParser : BlockParser2 { private static readonly Regex _table = new(@" ( # whole table [ \n]* (? # table header ([^\n\|]*\|[^\n]+) ) [ ]*\n[ ]* (? # column style \|?([ ]*:?-+:?[ ]*(\||$))+ ) (? # table row ( [ ]*\n[ ]* ([^\n\|]*\|[^\n]+) )+ ) )", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled | RegexOptions.ExplicitCapture); public TableParser() : base(_table, "TableEvalutor") { } public override IEnumerable? Convert2( string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; return new[] { TableEvalutor(firstMatch, engine) }; } private TableBlockElement TableEvalutor(Match match, IMarkdownEngine2 engine) { Dictionary styleMt = ExtractCoverBar(match.Groups["col"].Value.Trim()) .Split('|') .Select((styleText, idx) => { var text = styleText.Trim(); var firstChar = text[0]; var lastChar = text[text.Length - 1]; return firstChar == ':' && lastChar == ':' ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Center) : lastChar == ':' ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Right) : firstChar == ':' ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Left) : Tuple.Create(idx, (TextAlignment?)null); }) .Where(tpl => tpl.Item2.HasValue) .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2!.Value); int colOffset = 0; TableCellElement[][] headerCells = new[] { CreateRow(styleMt, match.Groups["hdr"].Value, engine, true) }; List detailCells = new(); foreach (var cellline in match.Groups["row"].Value.Trim().Split('\n')) { detailCells.Add(CreateRow(styleMt, cellline, engine, false)); } return new TableBlockElement(headerCells, detailCells.ToArray(), Array.Empty(), true); } private TableCellElement[] CreateRow(Dictionary styleMt, string txt, IMarkdownEngine2 engine, bool ignoreRowSpan) { int colOffset = 0; List cells = new(); foreach (var celltxt in ExtractCoverBar(txt.Trim()).Split('|')) { var cell = CreateCell(celltxt, engine); if (ignoreRowSpan) cell.RowSpan = 1; // apply text align if (styleMt.TryGetValue(colOffset, out var style)) cell.Horizontal = style; cells.Add(cell); colOffset += cell.ColSpan; } return cells.ToArray(); } private TableCellElement CreateCell(string txt, IMarkdownEngine2 engine) { int colspan = 1; int rowspan = 1; TextAlignment? horizontal = null; VerticalAlignment? vertical = null; int idx = txt.IndexOf('.'); if (idx != -1) { var styleTxt = txt.Substring(0, idx); for (var i = 0; i < styleTxt.Length; ++i) { switch (styleTxt[i]) { case '/': // /2 rowspan ++i; var numTxt = ContinueToNum(styleTxt, ref i); if (numTxt.Length == 0) goto default; rowspan = int.Parse(numTxt); break; case '\\': // \2 colspan ++i; numTxt = ContinueToNum(styleTxt, ref i); if (numTxt.Length == 0) goto default; colspan = int.Parse(numTxt); break; case '<': // < left align horizontal = TextAlignment.Left; break; case '>': // > right align horizontal = TextAlignment.Right; break; case '=': // = center align horizontal = TextAlignment.Center; break; case '^': // ^ top align vertical = VerticalAlignment.Top; break; case '~': // ~ bottom align vertical = VerticalAlignment.Bottom; break; default: rowspan = 1; colspan = 1; horizontal = null; vertical = null; goto endparse; } } txt = txt.Substring(idx + 1); endparse:; } var sb = new StringBuilder(); for (var i = 0; i < txt.Length; ++i) { var c = txt[i]; if (c == '\\') { if (++i < txt.Length) { if (txt[i] == 'n') sb.Append(" \n"); // \n => linebreak else sb.Append('\\').Append(txt[i]); } else sb.Append('\\'); } else sb.Append(c); } return new TableCellElement(new CTextBlockElement(engine.ParseGamutInline(sb.ToString().Trim()))) { ColSpan = colspan, RowSpan = rowspan, Horizontal = horizontal, Vertical = vertical, }; } private static string ExtractCoverBar(string txt) { if (txt[0] == '|') txt = txt.Substring(1); if (string.IsNullOrEmpty(txt)) return txt; if (txt[txt.Length - 1] == '|') txt = txt.Substring(0, txt.Length - 1); return txt; } private static string ContinueToNum(string charSource, ref int idx) { var builder = new StringBuilder(); for (; idx < charSource.Length; ++idx) { var c = charSource[idx]; if ('0' <= c && c <= '9') builder.Append(c); else break; } --idx; return builder.ToString(); } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/InlineParser.cs ================================================ using Avalonia.Controls; using ColorTextBlock.Avalonia; using System; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Parsers { public abstract class InlineParser { public Regex Pattern { get; } public string Name { get; } public InlineParser(Regex pattern, string name) { Pattern = pattern; Name = name; } public abstract IEnumerable Convert( string text, Match firstMatch, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd); public static InlineParser New(Regex pattern, string name, Func v1) => new Single(pattern, name, v1); public static InlineParser New(Regex pattern, string name, Func> v2) => new Multi(pattern, name, v2); abstract class Wrapper : InlineParser { public Wrapper(Regex pattern, string name) : base(pattern, name) { } public override IEnumerable Convert( string text, Match firstMatch, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { parseTextBegin = firstMatch.Index; parseTextEnd = parseTextBegin + firstMatch.Length; return Convert(firstMatch); } public abstract IEnumerable Convert(Match match); } sealed class Single : Wrapper { private readonly Func converter; public Single(Regex pattern, string name, Func converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable Convert(Match match) { yield return converter(match); } } sealed class Multi : Wrapper { private readonly Func> converter; public Multi(Regex pattern, string name, Func> converter) : base(pattern, name) { this.converter = converter; } public override IEnumerable Convert(Match match) => converter(match); } } } ================================================ FILE: Markdown.Avalonia.Tight/Parsers/ParseStatus.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.Parsers { public struct ParseStatus { public static readonly ParseStatus Init = new ParseStatus(true); public bool SupportTextAlignment { get; } public ParseStatus(bool supportTextAlignment) { SupportTextAlignment = supportTextAlignment; } } } ================================================ FILE: Markdown.Avalonia.Tight/Plugins/BlockOverride2.cs ================================================ using Avalonia.Controls; using ColorDocument.Avalonia; using Markdown.Avalonia.Parsers; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Plugins { public abstract class BlockOverride2 : IBlockOverride { private readonly string _parserName; public string ParserName => _parserName; public BlockOverride2(string parserName) { _parserName = parserName; } public abstract IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd); public IEnumerable? Convert(string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) { if (Convert2(text, firstMatch, status, engine.Upgrade(), out parseTextBegin, out parseTextEnd) is { } docs) { return docs.Select(d => d.Control); } return null; } } } ================================================ FILE: Markdown.Avalonia.Tight/Plugins/IBlockOverrider.cs ================================================ using Avalonia.Controls; using Markdown.Avalonia.Parsers; using System.Collections.Generic; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Plugins { /// /// The interface to change the parsing method for block elements. /// // ブロック要素の解析方法を変更するためのインターフェイス public interface IBlockOverride { /// /// The target name to override. /// string ParserName { get; } /// /// Translates markdown to visual elements. /// // Markdownを表示要素に変換します。 IEnumerable? Convert( string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd); } } ================================================ FILE: Markdown.Avalonia.Tight/Plugins/IMdAvPlugin.cs ================================================ using Avalonia.Controls; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Plugins { public interface IMdAvPlugin { void Setup(SetupInfo info); } public interface IMdAvPluginRequestAnother : IMdAvPlugin { IEnumerable DependsOn { get; } void Inject(IEnumerable plugin); } } ================================================ FILE: Markdown.Avalonia.Tight/Plugins/IStyleEditor.cs ================================================ using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.Plugins { public interface IStyleEdit { void Edit(string styleName, Styles style); } } ================================================ FILE: Markdown.Avalonia.Tight/Plugins/SetupInfo.cs ================================================ using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using ColorDocument.Avalonia; using ColorTextBlock.Avalonia; using Markdown.Avalonia.Parsers; using Markdown.Avalonia.Utils; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Design; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Input; using static System.Net.Mime.MediaTypeNames; namespace Markdown.Avalonia.Plugins { public class SetupInfo { internal const string BuiltinAsmNm = "Markdown.Avalonia.SyntaxHigh"; internal const string BuiltinTpNm = "Markdown.Avalonia.SyntaxHigh.SyntaxHighlight"; private bool _isFreezed = false; private bool _builtinCalled = false; private ICommand? _overwriteHyperlink; private ICommand? _command; private ICommand? _defaultHyperlink; private IContainerBlockHandler? _overwriteHandler; private IContainerBlockHandler? _containerBlock; private IPathResolver? _pathResolver; private IPathResolver? _defaultPathResolver; private ImageLoader _imageLoader; private bool _EnableNoteBlock = true; private bool _EnableTableBlock = true; private bool _EnableRuleExt = true; private bool _EnableTextAlignment = true; private bool _EnableStrikethrough = true; private bool _EnableListMarkerExt = true; private bool _EnableContainerBlockExt = true; private bool _EnableTextileInline = true; private bool _EnablePreRenderingCodeBlock = false; internal List BlockOverrides { get; } internal List TopBlock { get; } internal List Block { get; } internal List Inline { get; } internal List StyleEdits { get; } internal List ImageResolvers { get; } internal ICommand HyperlinkCommand => _overwriteHyperlink ?? _command ?? (_defaultHyperlink ??= new DefaultHyperlinkCommand()); internal IContainerBlockHandler? ContainerBlock => _overwriteHandler ?? _containerBlock; internal IPathResolver PathResolver => _pathResolver ?? (_defaultPathResolver ??= new DefaultPathResolver()); public SetupInfo() { _imageLoader = new(this); BlockOverrides = new(); TopBlock = new(); Block = new(); Inline = new(); StyleEdits = new(); ImageResolvers = new(); } public bool EnableNoteBlock { get => _EnableNoteBlock; set { CheckChangeable(); _EnableNoteBlock = value; } } public bool EnableTableBlock { get => _EnableTableBlock; set { CheckChangeable(); _EnableTableBlock = value; } } public bool EnableRuleExt { get => _EnableRuleExt; set { CheckChangeable(); _EnableRuleExt = value; } } public bool EnableTextAlignment { get => _EnableTextAlignment; set { CheckChangeable(); _EnableTextAlignment = value; } } public bool EnableStrikethrough { get => _EnableStrikethrough; set { CheckChangeable(); _EnableStrikethrough = value; } } public bool EnableListMarkerExt { get => _EnableListMarkerExt; set { CheckChangeable(); _EnableListMarkerExt = value; } } public bool EnableContainerBlockExt { get => _EnableContainerBlockExt; set { CheckChangeable(); _EnableContainerBlockExt = value; } } public bool EnableTextileInline { get => _EnableTextileInline; set { CheckChangeable(); _EnableTextileInline = value; } } public bool EnablePreRenderingCodeBlock { get => _EnablePreRenderingCodeBlock; set { CheckChangeable(); _EnablePreRenderingCodeBlock = value; } } public void Register(IBlockOverride overrider) { CheckChangeable(); BlockOverrides.Add(overrider); } public void RegisterTop(BlockParser parser) { CheckChangeable(); TopBlock.Add(parser); } public void RegisterSecond(BlockParser parser) { CheckChangeable(); Block.Add(parser); } public void Register(InlineParser parser) { CheckChangeable(); Inline.Add(parser); } public void Register(IStyleEdit editor) { CheckChangeable(); StyleEdits.Add(editor); } public void SetOnce(ICommand command) { CheckChangeable(); if (_command is not null) { throw new InvalidOperationException("IContainerBlockHandler is already set. Please check Markdown.Avalonia plugins"); } _command = command; } public void SetOnce(IContainerBlockHandler handler) { CheckChangeable(); if (_containerBlock is not null) { throw new InvalidOperationException("IContainerBlockHandler is already set. Please check Markdown.Avalonia plugins"); } _containerBlock = handler; } public void SetOnce(IPathResolver resolver) { CheckChangeable(); if (_pathResolver is not null) { throw new InvalidOperationException("IPathResolver is already set. Please check Markdown.Avalonia plugins"); } _pathResolver = resolver; } public void Register(IImageResolver resolver) { CheckChangeable(); ImageResolvers.Add(resolver); } internal SetupInfo Builtin() { if (_builtinCalled) return this; CheckChangeable(); Assembly asm; try { asm = Assembly.Load(BuiltinAsmNm); var type = asm.GetType(BuiltinTpNm); if (type != null && Activator.CreateInstance(type) is IMdAvPlugin plugin) { plugin.Setup(this); } } catch { return this; } _builtinCalled = true; return this; } internal BlockParser Override(BlockParser parser) { var overrider = BlockOverrides.FirstOrDefault(b => b.ParserName == parser.Name); if (overrider is null) return parser; if (overrider is BlockOverride2 override2) return new BlockParserOverride2(parser.Pattern, parser.Name, override2); else return new BlockParserOverride(parser.Pattern, parser.Name, overrider); } internal void Overwrite(ICommand? hyperlink) { _overwriteHyperlink = hyperlink; } internal void Overwrite(IContainerBlockHandler? handler) { _overwriteHandler = handler; } #pragma warning disable CS0618 internal void Overwrite(IBitmapLoader? loader) #pragma warning restore CS0618 { _imageLoader.BitmapLoader = loader; } public void Freeze() { _isFreezed = true; } public CImage LoadImage(string urlTxt) => _imageLoader.Load(urlTxt); internal void CheckChangeable() { if (_isFreezed) throw new InvalidOperationException(); } class BlockParserOverride : BlockParser { private IBlockOverride _overrider; public BlockParserOverride(Regex pattern, string name, IBlockOverride overrider) : base(pattern, name) { _overrider = overrider; } public override IEnumerable? Convert( string text, Match firstMatch, ParseStatus status, IMarkdownEngine engine, out int parseTextBegin, out int parseTextEnd) => _overrider.Convert(text, firstMatch, status, engine, out parseTextBegin, out parseTextEnd); } class BlockParserOverride2 : BlockParser2 { private BlockOverride2 _overrider; public BlockParserOverride2(Regex pattern, string name, BlockOverride2 overrider) : base(pattern, name) { _overrider = overrider; } public override IEnumerable? Convert2(string text, Match firstMatch, ParseStatus status, IMarkdownEngine2 engine, out int parseTextBegin, out int parseTextEnd) => _overrider.Convert2(text, firstMatch, status, engine, out parseTextBegin, out parseTextEnd); } class ImageLoader { private SetupInfo _setupInfo; private Dictionary> _cache; private Lazy _imageNotFound; #pragma warning disable CS0618 public IBitmapLoader? BitmapLoader { get; set; } #pragma warning restore CS0618 public ImageLoader(SetupInfo info) { _setupInfo = info; _cache = new(); _imageNotFound = new Lazy(() => { using var strm = AssetLoader.Open(new Uri($"avares://Markdown.Avalonia/Assets/ImageNotFound.bmp")); return new Bitmap(strm); }); } public CImage Load(string urlTxt) { if (_cache.TryGetValue(urlTxt, out var bitmapRef) && bitmapRef.TryGetTarget(out var cachedBitmap)) { return new CImage(cachedBitmap ?? _imageNotFound.Value); } else { #pragma warning disable CS0618 var imageTask = BitmapLoader is not null ? Task.Run(() => (IImage?)BitmapLoader?.Get(urlTxt)) : #pragma warning restore CS0618 LoadImageByPlugin(urlTxt); return new CImage(imageTask, _imageNotFound.Value); } } private async Task LoadImageByPlugin(string urlTxt) { foreach (var key in _cache.Keys.ToArray()) { if (_cache[key].TryGetTarget(out var _)) _cache.Remove(key); } var streamTask = _setupInfo.PathResolver.ResolveImageResource(urlTxt); if (streamTask is null) { _cache[urlTxt] = new WeakReference(_imageNotFound.Value); return null; } using var stream = await streamTask; if (stream is null) { _cache[urlTxt] = new WeakReference(_imageNotFound.Value); return null; } Stream seekableStream; if (!stream.CanSeek) { seekableStream = new MemoryStream(); await stream.CopyToAsync(seekableStream); } else { seekableStream = stream; } var reuseStream = new UnclosableStream(seekableStream); foreach (var imageResolver in _setupInfo.ImageResolvers) { reuseStream.Position = 0; var image = await imageResolver.Load(reuseStream); if (image is not null) { _cache[urlTxt] = new WeakReference(image); return image; } } try { var image = new Bitmap(reuseStream); _cache[urlTxt] = new WeakReference(image); return image; } catch { _cache[urlTxt] = new WeakReference(_imageNotFound.Value); return null; } } } } } ================================================ FILE: Markdown.Avalonia.Tight/Properties/AssemblyInfo.cs ================================================ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia.Tight", "Markdown.Avalonia")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia.Tight", "mdxaml")] [assembly: XmlnsDefinition("https://github.com/whistyun/Markdown.Avalonia.Tight/Styles", "Markdown.Avalonia.StyleCollections")] [assembly: XmlnsPrefix("https://github.com/whistyun/Markdown.Avalonia.Tight/Styles", "mdstyles")] ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/INamedStyle.cs ================================================ namespace Markdown.Avalonia.StyleCollections { internal interface INamedStyle { string Name { get; } bool IsEditted { get; set; } } } ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleDefaultTheme.axaml ================================================ ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleDefaultTheme.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.StyleCollections { public class MarkdownStyleDefaultTheme : Styles, INamedStyle { public string Name => nameof(MarkdownStyle.SimpleTheme); public bool IsEditted { get; set; } public MarkdownStyleDefaultTheme() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleFluentAvalonia.axaml ================================================ ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleFluentAvalonia.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.StyleCollections { public class MarkdownStyleFluentAvalonia : Styles, INamedStyle { public string Name => nameof(MarkdownStyle.FluentAvalonia); public bool IsEditted { get; set; } public MarkdownStyleFluentAvalonia() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleFluentTheme.axaml ================================================ ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleFluentTheme.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.StyleCollections { public class MarkdownStyleFluentTheme : Styles, INamedStyle { public string Name => nameof(MarkdownStyle.FluentTheme); public bool IsEditted { get; set; } public MarkdownStyleFluentTheme() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleGithubLike.axaml ================================================ ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleGithubLike.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.StyleCollections { public class MarkdownStyleGithubLike : Styles, INamedStyle { public string Name => nameof(MarkdownStyle.GithubLike); public bool IsEditted { get; set; } public MarkdownStyleGithubLike() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleStandard.axaml ================================================ ================================================ FILE: Markdown.Avalonia.Tight/StyleCollections/MarkdownStyleStandard.axaml.cs ================================================ using Avalonia.Markup.Xaml; using Avalonia.Styling; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.StyleCollections { public class MarkdownStyleStandard : Styles, INamedStyle { public string Name => nameof(MarkdownStyle.Standard); public bool IsEditted { get; set; } public MarkdownStyleStandard() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: Markdown.Avalonia.Tight/Tables/ITable.cs ================================================ using System.Collections.Generic; namespace Markdown.Avalonia.Tables { interface ITable { List Header { get; } List> Details { get; } int ColCount { get; } int RowCount { get; } } } ================================================ FILE: Markdown.Avalonia.Tight/Tables/ITableCell.cs ================================================ using Avalonia.Media; using Avalonia.Layout; namespace Markdown.Avalonia.Tables { interface ITableCell { int ColumnIndex { get; } string? RawText { get; } string? Text { get; } int RowSpan { get; } int ColSpan { get; } TextAlignment? Horizontal { get; } VerticalAlignment? Vertical { get; } } } ================================================ FILE: Markdown.Avalonia.Tight/Tables/TextileTable.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Linq; using Avalonia.Media; using Avalonia.Layout; namespace Markdown.Avalonia.Tables { class TextileTable : ITable { public List Header { get; } public List> Details { get; } public int ColCount { get; } public int RowCount { get; } public TextileTable( string[] header, string[] styles, IList details) { Header = header.Select(txt => { var cell = new TextileTableCell(txt); // Ignore Header Row-span cell.RowSpan = 1; return cell; }).ToList(); Details = details.Select(row => row.Select(txt => new TextileTableCell(txt)).ToList() ).ToList(); // column-idx vs text-alignment Dictionary styleMt = styles .Select((txt, idx) => { var firstChar = txt[0]; var lastChar = txt[txt.Length - 1]; return (firstChar == ':' && lastChar == ':') ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Center) : (lastChar == ':') ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Right) : (firstChar == ':') ? Tuple.Create(idx, (TextAlignment?)TextAlignment.Left) : Tuple.Create(idx, (TextAlignment?)null); }) .Where(tpl => tpl.Item2.HasValue) .ToDictionary(tpl => tpl.Item1, tpl => tpl.Item2!.Value); var styleColumnCount = styleMt.Count; // apply cell style to header var headerColumnCount = 0; { var colOffset = 0; foreach (TextileTableCell cell in Header) { cell.ColumnIndex = colOffset; // apply text align if (styleMt.TryGetValue(colOffset, out var style)) { cell.Horizontal = style; } colOffset += cell.ColSpan; } headerColumnCount = colOffset; } // apply cell style to header var colCntAtDetail = new List(); var maxColCntInDetails = 1; { var multiRowsAtColIdx = new Dictionary(); for (var rowIdx = 0; rowIdx < Details.Count; ++rowIdx) { List row = Details[rowIdx]; var hasAnyCell = false; var colOffset = 0; var rowspansColOffset = multiRowsAtColIdx .Select(ent => ent.Value.ColSpan) .Sum(); /* * In this row, is space exists to insert cell? * * eg. has space * __________________________________ * | 2x1 cell | 1x1 cell | 1x1 cell | * -> | |‾‾‾‾‾‾‾‾‾‾|‾‾‾‾‾‾‾‾‾‾| * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * * eg. has no space: multi-rows occupy all space in this row. * __________________________________ * | 2x1 cell | 2x2 cell | * -> | | | * ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ * */ if (rowspansColOffset < maxColCntInDetails) { int colIdx; for (colIdx = 0; colIdx < row.Count;) { int colSpan; if (multiRowsAtColIdx.TryGetValue(colOffset, out var span)) { colSpan = span.ColSpan; } else { hasAnyCell = true; var cell = (TextileTableCell)row[colIdx]; cell.ColumnIndex = colOffset; // apply text align if (!cell.Horizontal.HasValue && styleMt.TryGetValue(colOffset, out var style)) { cell.Horizontal = style; } colSpan = cell.ColSpan; if (cell.RowSpan > 1) { multiRowsAtColIdx[colOffset] = new MdSpan(cell.RowSpan, cell.ColSpan); } ++colIdx; } colOffset += colSpan; } foreach (var left in multiRowsAtColIdx.Where(tpl => tpl.Key >= colOffset) .OrderBy(tpl => tpl.Key)) { while (colOffset < left.Key) { var cell = new TextileTableCell(null); cell.ColumnIndex = colOffset++; row.Add(cell); } colOffset += left.Value.ColSpan; } } colOffset += multiRowsAtColIdx .Where(ent => ent.Key >= colOffset) .Select(ent => ent.Value.ColSpan) .Sum(); foreach (var spanEntry in multiRowsAtColIdx.ToArray()) { if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } colCntAtDetail.Add(colOffset); maxColCntInDetails = Math.Max(maxColCntInDetails, colOffset); if (!hasAnyCell) { Details.Insert(rowIdx, new List()); } } // if any multirow is left, insert an empty row. while (multiRowsAtColIdx.Count > 0) { var row = new List(); Details.Add(row); var colOffset = 0; foreach (var spanEntry in multiRowsAtColIdx.OrderBy(tpl => tpl.Key)) { while (colOffset < spanEntry.Key) { var cell = new TextileTableCell(null); cell.ColumnIndex = colOffset++; row.Add(cell); } colOffset += spanEntry.Value.ColSpan; if (--spanEntry.Value.Life == 0) { multiRowsAtColIdx.Remove(spanEntry.Key); } } colCntAtDetail.Add(colOffset); } } ColCount = Math.Max(Math.Max(headerColumnCount, styleColumnCount), maxColCntInDetails); RowCount = Details.Count; // insert cell for the shortfall for (var retry = Header.Sum(cell => cell.ColSpan); retry < ColCount; ++retry) { var cell = new TextileTableCell(null); cell.ColumnIndex = retry; Header.Add(cell); } for (var rowIdx = 0; rowIdx < Details.Count; ++rowIdx) { for (var retry = colCntAtDetail[rowIdx]; retry < ColCount; ++retry) { var cell = new TextileTableCell(null); cell.ColumnIndex = retry; Details[rowIdx].Add(cell); } } } class MdSpan { public int Life { set; get; } public int ColSpan { set; get; } public MdSpan(int l, int c) { Life = l; ColSpan = c; } } } } ================================================ FILE: Markdown.Avalonia.Tight/Tables/TextileTableCell.cs ================================================ using System; using System.Text; using Avalonia.Media; using Avalonia.Layout; namespace Markdown.Avalonia.Tables { class TextileTableCell : ITableCell { public int ColumnIndex { set; get; } public string? RawText { get; } public string? Text { get; } public int RowSpan { set; get; } public int ColSpan { set; get; } public TextAlignment? Horizontal { set; get; } public VerticalAlignment? Vertical { set; get; } public TextileTableCell(string? txt) { RawText = txt; RowSpan = 1; ColSpan = 1; Horizontal = null; Vertical = null; if (txt is null) return; txt = ParseFormatFrom(txt); var sb = new StringBuilder(); for (var i = 0; i < txt.Length; ++i) { var c = txt[i]; if (c == '\\') { if (++i < txt.Length) { if (txt[i] == 'n') sb.Append(" \n"); // \n => linebreak else sb.Append('\\').Append(txt[i]); } else sb.Append('\\'); } else sb.Append(c); } Text = sb.ToString(); } private string ParseFormatFrom(string txt) { int idx = txt.IndexOf('.'); if (idx == -1) { return txt.Trim(); } else { var styleTxt = txt.Substring(0, idx); for (var i = 0; i < styleTxt.Length; ++i) { var c = styleTxt[i]; switch (c) { case '/': // /2 rowspan ++i; var numTxt = ContinueToNum(styleTxt, ref i); if (numTxt.Length == 0) goto default; RowSpan = Int32.Parse(numTxt); break; case '\\': // \2 colspan ++i; numTxt = ContinueToNum(styleTxt, ref i); if (numTxt.Length == 0) goto default; ColSpan = Int32.Parse(numTxt); break; case '<': // < left align Horizontal = TextAlignment.Left; break; case '>': // > right align Horizontal = TextAlignment.Right; break; case '=': // = center align Horizontal = TextAlignment.Center; break; case '^': // ^ top align Vertical = VerticalAlignment.Top; break; case '~': // ~ bottom align Vertical = VerticalAlignment.Bottom; break; default: RowSpan = 1; ColSpan = 1; Horizontal = null; Vertical = null; return txt.Trim(); } } return txt.Substring(idx + 1).Trim(); } } private static string ContinueToNum(string charSource, ref int idx) { var builder = new StringBuilder(); for (; idx < charSource.Length; ++idx) { var c = charSource[idx]; if ('0' <= c && c <= '9') builder.Append(c); else break; } --idx; return builder.ToString(); } } } ================================================ FILE: Markdown.Avalonia.Tight/TextMarkerStyle.cs ================================================ using ColorDocument.Avalonia.DocumentElements; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia { public enum TextMarkerStyle { Box, Circle, Decimal, Disc, LowerLatin, LowerRoman, UpperLatin, UpperRoman, Square, } public static class MarkdownStyleExt { public static ColorDocument.Avalonia.DocumentElements.TextMarkerStyle Change(this TextMarkerStyle style) { return style switch { TextMarkerStyle.Box => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.Box, TextMarkerStyle.Circle => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.Circle, TextMarkerStyle.Decimal => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.Decimal, TextMarkerStyle.Disc => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.Disc, TextMarkerStyle.LowerLatin => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.LowerLatin, TextMarkerStyle.LowerRoman => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.LowerRoman, TextMarkerStyle.UpperLatin => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.UpperLatin, TextMarkerStyle.UpperRoman => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.UpperRoman, TextMarkerStyle.Square => ColorDocument.Avalonia.DocumentElements.TextMarkerStyle.Square, }; } public static string CreateMakerText(this TextMarkerStyle textMarker, int index) => textMarker.Change().CreateMakerText(index); } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/DefaultBitmapLoader.cs ================================================ using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Utils { [Obsolete("see https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-migrages-to-ver11")] public class DefaultBitmapLoader : IBitmapLoader { private static readonly HttpClient _httpclient = new(); public string AssetPathRoot { set; private get; } private string[] AssetAssemblyNames { get; } private ConcurrentDictionary> Cache; public DefaultBitmapLoader() { AssetPathRoot = Environment.CurrentDirectory; var stack = new StackTrace(); this.AssetAssemblyNames = stack.GetFrames() .Select(frm => frm?.GetMethod()?.DeclaringType?.Assembly?.GetName()?.Name) .OfType() .Where(name => !name.Equals("Markdown.Avalonia")) .Distinct() .ToArray(); Cache = new ConcurrentDictionary>(); } private void Compact() { foreach (var entry in Cache.ToArray()) { if (!entry.Value.TryGetTarget(out var _)) { ((IDictionary>)Cache).Remove(entry.Key); } } } public Task GetAsync(string urlTxt) { return Task.Run(() => Get(urlTxt)); } public Bitmap? Get(string urlTxt) { Bitmap? imgSource = null; // check network if (Uri.TryCreate(urlTxt, UriKind.Absolute, out var url)) { imgSource = Get(url); } // check resources if (imgSource is null) { foreach (var asmNm in AssetAssemblyNames) { var assetUrl = new Uri($"avares://{asmNm}/{urlTxt}"); imgSource = Get(assetUrl); if (imgSource != null) break; } } // check filesystem if (imgSource is null && AssetPathRoot != null) { try { if (Uri.IsWellFormedUriString(AssetPathRoot, UriKind.Absolute)) { imgSource = Get(new Uri(new Uri(AssetPathRoot), urlTxt)); } else { using (var strm = File.OpenRead(Path.Combine(AssetPathRoot, urlTxt))) imgSource = new Bitmap(strm); } } catch { } } return imgSource; } public Bitmap? Get(Uri url) { if (Cache.TryGetValue(url, out var reference)) { if (reference.TryGetTarget(out var image)) { return image; } } Compact(); Bitmap imgSource; try { switch (url.Scheme) { case "http": case "https": using (var res = _httpclient.GetAsync(url).Result) using (var strm = res.Content.ReadAsStreamAsync().Result) imgSource = new Bitmap(strm); break; case "file": if (!File.Exists(url.LocalPath)) return null; using (var strm = File.OpenRead(url.LocalPath)) imgSource = new Bitmap(strm); break; case "avares": using (var strm = AssetLoader.Open(url)) imgSource = new Bitmap(strm); break; default: throw new InvalidDataException($"unsupport scheme '{url.Scheme}'"); } } catch { return null; } Cache[url] = new WeakReference(imgSource); return imgSource; } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/DefaultHyperlinkCommand.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using System.Windows.Input; namespace Markdown.Avalonia.Utils { public class DefaultHyperlinkCommand : ICommand { public event EventHandler? CanExecuteChanged; private bool _isExecutable = true; public bool IsExecutable { get => _isExecutable; set { if (_isExecutable != value) { _isExecutable = value; CanExecuteChanged?.Invoke(this, new EventArgs()); } } } public bool CanExecute(object? parameter) { return parameter is string; } public void Execute(object? parameter) { var url = parameter as string; GoTo(url ?? ""); } public static void GoTo(string url) { // https://github.com/dotnet/runtime/issues/17938 if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Process.Start(new ProcessStartInfo(url) { UseShellExecute = true, Verb = "open" }); else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) Process.Start("xdg-open", url); else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) Process.Start("open", url); } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/DefaultPathResolver.cs ================================================ using Avalonia.Controls; using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Utils { public class DefaultPathResolver : IPathResolver { private static readonly HttpClient s_httpclient = new(); public string? AssetPathRoot { set; private get; } public IEnumerable? CallerAssemblyNames { set; private get; } public async Task? ResolveImageResource(string relativeOrAbsolutePath) { // absolute path? // ( http://... and C:\... ) if (Uri.TryCreate(relativeOrAbsolutePath, UriKind.Absolute, out var aburl)) { var stream = await OpenStream(aburl); if (stream is not null) return stream; } if (CallerAssemblyNames is not null) { foreach (var asmNm in CallerAssemblyNames) { var assetUrl = new Uri($"avares://{asmNm}/{relativeOrAbsolutePath}"); var stream = await OpenStream(assetUrl); if (stream is not null) return stream; } } if (AssetPathRoot is not null) { Uri pathUri; if (Path.IsPathRooted(AssetPathRoot)) { pathUri = new Uri(Path.Combine(AssetPathRoot, relativeOrAbsolutePath)); } else { pathUri = new Uri(new Uri(AssetPathRoot), relativeOrAbsolutePath); } var stream = await OpenStream(pathUri); if (stream is not null) return stream; } throw new NotImplementedException(); } private async Task OpenStream(Uri url) { switch (url.Scheme) { case "http": case "https": var response = await s_httpclient.GetAsync(url); return await response.Content.ReadAsStreamAsync(); case "file": if (!File.Exists(url.LocalPath)) return null; return File.OpenRead(url.LocalPath); case "avares": if (!AssetLoader.Exists(url)) return null; return AssetLoader.Open(url); default: throw new InvalidDataException($"unsupport scheme '{url.Scheme}'"); } } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/Helper.cs ================================================ using Avalonia; using Avalonia.Platform; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.Utils { static class Helper { public static void ThrowInvalidOperation(string msg) { throw new InvalidOperationException(msg); } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/IBitmapLoader.cs ================================================ using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.Collections.Generic; using System.Text; namespace Markdown.Avalonia.Utils { [Obsolete("see https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-migrages-to-ver11")] public interface IBitmapLoader { /// /// local file root path or url, the default is current directory. /// string AssetPathRoot { set; } Bitmap? Get(string urlTxt); } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/IContainerBlockHandler.cs ================================================ using Avalonia.Controls; namespace Markdown.Avalonia.Utils { /// /// support for Container blocks /// https://talk.commonmark.org/t/generic-directives-plugins-syntax/444 /// public interface IContainerBlockHandler { /// /// Custom text parsing & content generation /// Based on https://talk.commonmark.org/t/generic-directives-plugins-syntax/444 /// /// Asset Path Root /// Block Name /// Text to parse /// Controls; or null when no container founds Border? ProvideControl( string assetPathRoot, string blockName, string lines ); } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/IImageResolver.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Utils { /// /// Derivered classes create IImage from image resources. /// public interface IImageResolver { /// /// Open image /// /// image resource. Stream is seek-able and close-able. /// /// Images. /// Return null if derived class cannot create IImage, e.g. because resource is not supportted. /// Task Load(Stream stream); } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/IPathResolver.cs ================================================ using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace Markdown.Avalonia.Utils { /// /// Derived classes open an image resources from relative or absolute paths. /// Relative paths may be converted to absolute paths in derived classes. /// public interface IPathResolver { string? AssetPathRoot { set; } IEnumerable? CallerAssemblyNames { set; } /// /// Open an image resource. /// /// The image path /// /// The task of the image resource. /// Returns null if the image does not exist or cannot be opened. /// If the result of the asynchronous search cannot be resolved, Task.Result returns null. /// Task? ResolveImageResource(string relativeOrAbsolutePath); } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/InterassemblyUtil.cs ================================================ using System; using System.Collections.Generic; using System.Reflection; using System.Text; namespace Markdown.Avalonia.Utils { static class InterassemblyUtil { public static T? InvokeInstanceMethodToGetProperty(string asmNm, string typeNm, string methodNm, params object[] methodArgs) where T : class { var asm = Assembly.Load(asmNm); var setupTp = asm.GetType(typeNm); if (setupTp is null) throw new NullReferenceException($"Failed to load '{typeNm}' in '{asm.FullName}'."); var method = setupTp.GetMethod(methodNm); if (method is null) throw new NullReferenceException($"'{methodNm}' method dosen't exist in '{typeNm}'."); if (method.IsStatic) return null; if (method.GetParameters().Length != methodArgs.Length) return null; var setupObj = Activator.CreateInstance(setupTp); var result = method.Invoke(setupObj, methodArgs); return result as T; } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/TextUtil.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace Markdown.Avalonia.Utils { static class TextUtil { /// /// Count the number of leading whilte-spaces. /// tab is treated as 4-spaces. /// public static int CountIndent(string line) { var count = 0; foreach (var c in line) { if (c == ' ') count += 1; else if (c == '\t') { // In default in vs, tab is treated as four-spaces. count = ((count >> 2) + 1) << 2; } else break; } return count; } /// /// Removes the leading white-space. the number of removed spaces is `indentCount`. /// /// If the leading white-space is too short than `indentCount`, /// this method removes all leading white-spaces. /// public static string DetentLineBestEffort(string line, int indentCount) { // this index count tab as 1: for String.Substring var realIdx = 0; // this index count tab as 4: for human (I think most text-editor treats tab as 4spaces) var viewIdx = 0; while (viewIdx < indentCount && realIdx < line.Length) { var c = line[realIdx]; if (c == ' ') { realIdx += 1; viewIdx += 1; } else if (c == '\t') { realIdx += 1; // when mixing space and tab (ex: space space tab), some space should be ignored. viewIdx = ((viewIdx >> 2) + 1) << 2; } // give up ded else break; } return line.Substring(realIdx); } /// /// Removes the leading white-space. the number of removed spaces is `indentCount`. /// /// If the leading white-space is too short than `indentCount`, /// this method return 'false' and `detendedLine` is set null. /// public static bool TryDetendLine(string line, int indentCount, out string detendedLine) { // this index count tab as 1: for String.Substring var realIdx = 0; // this index count tab as 4: for human (I think most text-editor treats tab as 4spaces) var viewIdx = 0; while (viewIdx < indentCount && realIdx < line.Length) { var c = line[realIdx]; if (c == ' ') { realIdx += 1; viewIdx += 1; } else if (c == '\t') { realIdx += 1; // when mixing space and tab (ex: space space tab), some space should be ignored. viewIdx = ((viewIdx >> 2) + 1) << 2; } // give up ded else { detendedLine = string.Empty; return false; } } detendedLine = line.Substring(realIdx); return true; } /// /// convert all tabs to _tabWidth spaces; /// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF); /// makes sure text ends with a couple of newlines; /// removes any blank lines (only spaces) in the text /// public static string Normalize(string text, int tabWidth = 4) { if (text is null) { throw new ArgumentNullException(nameof(text)); } var output = new StringBuilder(text.Length); var line = new StringBuilder(); bool valid = false; for (int i = 0; i < text.Length; i++) { switch (text[i]) { case '\n': if (valid) output.Append(line); output.Append('\n'); line.Length = 0; valid = false; break; case '\r': if ((i < text.Length - 1) && (text[i + 1] != '\n')) { if (valid) output.Append(line); output.Append('\n'); line.Length = 0; valid = false; } break; case '\t': int width = (tabWidth - line.Length % tabWidth); for (int k = 0; k < width; k++) line.Append(' '); break; case '\x1A': break; default: if (!valid && text[i] != ' ') valid = true; line.Append(text[i]); break; } } if (valid) output.Append(line); if (output.Length >= 1 && output[output.Length - 1] != '\n') output.Append('\n'); // add two newlines to the end before return return output.ToString(); } } } ================================================ FILE: Markdown.Avalonia.Tight/Utils/ThemeDetector.cs ================================================ using Avalonia; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; namespace Markdown.Avalonia.Utils { static class ThemeDetector { private static readonly string s_SimpleThemeFQCN = "Avalonia.Themes.Simple.SimpleTheme"; private static readonly string s_FluentThemeFQCN = "Avalonia.Themes.Fluent.FluentTheme"; private static readonly string s_FluentAvaloniaFQCN = "FluentAvalonia.Styling.FluentAvaloniaTheme"; private static readonly string s_SimpleThemeHost = "Avalonia.Themes.Simple"; private static readonly string s_FluentThemeHost = "Avalonia.Themes.Fluent"; private static readonly string s_FluentAvaloniaHost = "FluentAvalonia"; static bool? s_isSimpleUsed; static bool? s_isFluentUsed; static bool? s_isFluentAvaloniaUsed; private static bool? CheckApplicationCurrentStyle(string themeFQCN, string avaresHost) { if (Application.Current is null || Application.Current.Styles is null) return null; foreach (var style in Application.Current.Styles) { if (style.GetType().FullName == themeFQCN) { return true; } if (style is StyleInclude incld) { var uri = incld.Source; if (uri is null) return false; if (!uri.IsAbsoluteUri) return false; try { return uri.Host == avaresHost; } catch { return false; } } else return false; } return false; } public static bool? IsSimpleUsed { get { if (s_isSimpleUsed.HasValue) return s_isSimpleUsed; return s_isSimpleUsed = CheckApplicationCurrentStyle(s_SimpleThemeFQCN, s_SimpleThemeHost); } } public static bool? IsFluentUsed { get { if (s_isFluentUsed.HasValue) return s_isFluentUsed; return s_isFluentUsed = CheckApplicationCurrentStyle(s_FluentThemeFQCN, s_FluentThemeHost); } } public static bool? IsFluentAvaloniaUsed { get { if (s_isFluentAvaloniaUsed.HasValue) return s_isFluentAvaloniaUsed; return s_isFluentAvaloniaUsed = CheckApplicationCurrentStyle(s_FluentAvaloniaFQCN, s_FluentAvaloniaHost); } } } } ================================================ FILE: Markdown.Avalonia.props ================================================ net8.0;net10.0 net8.0;net10.0 net8.0;net10.0 12.0.0 12.0.0 enabled 12.0.0 12.0.0.1 $(AVA_VER) $(AvaloniaVersion) 12.0.0-a3 ================================================ FILE: Markdown.Avalonia.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33110.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.Avalonia.Tight", "Markdown.Avalonia.Tight\Markdown.Avalonia.Tight.csproj", "{B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorTextBlock.Avalonia", "ColorTextBlock.Avalonia\ColorTextBlock.Avalonia.csproj", "{E4F9286F-A812-476E-A37D-EE2B8ED5D958}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.AvaloniaDemo", "demos\Markdown.AvaloniaDemo\Markdown.AvaloniaDemo.csproj", "{51318A0F-CB37-4DA2-8AE1-BB985B0A4028}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.AvaloniaFluentDemo", "demos\Markdown.AvaloniaFluentDemo\Markdown.AvaloniaFluentDemo.csproj", "{BC6B016B-B353-4E73-A98E-556DC70E5850}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AD84CF32-2208-43FD-95CE-31DF8DFEB2CD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest.Base", "tests\UnitTest.Base\UnitTest.Base.csproj", "{B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest.CTxt", "tests\UnitTest.CTxt\UnitTest.CTxt.csproj", "{A3A5D851-4041-403A-95F9-F4ADBA9602A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest.Md", "tests\UnitTest.Md\UnitTest.Md.csproj", "{C763BC05-5587-479E-8225-AA33F1F8F55B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.Avalonia", "Markdown.Avalonia\Markdown.Avalonia.csproj", "{29F1D11A-4490-4E50-AE72-72DBBA0157BE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest.MdSyntax", "tests\UnitTest.MdSyntax\UnitTest.MdSyntax.csproj", "{BC356FD7-5D47-495A-9255-A4199AA8E3A1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.Avalonia.SyntaxHigh", "Markdown.Avalonia.SyntaxHigh\Markdown.Avalonia.SyntaxHigh.csproj", "{B3438E36-4598-49FB-8E52-65C231978E4D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.AvaloniaFluentAvaloniaDemo", "demos\Markdown.AvaloniaFluentAvaloniaDemo\Markdown.AvaloniaFluentAvaloniaDemo.csproj", "{58E76A9C-D28E-45BD-85F2-885FF3E4B363}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.Avalonia.Svg", "Markdown.Avalonia.Svg\Markdown.Avalonia.Svg.csproj", "{0514B7E7-23D6-4B9D-8943-995AE57493C1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demos", "demos", "{8F902BB3-7573-4180-A911-6CC019E999CC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Markdown.Avalonia.Html", "Markdown.Avalonia.Html\Markdown.Avalonia.Html.csproj", "{CDAC497D-BF0F-48F0-A156-8DE2DAB15492}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest.MdHtml", "tests\UnitTest.MdHtml\UnitTest.MdHtml.csproj", "{6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorDocument.Avalonia", "ColorDocument.Avalonia\ColorDocument.Avalonia.csproj", "{008A6F1F-A1D9-45ED-B3CF-3106A59A4417}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmark", "benchmark", "{F5ED37EA-8110-4262-9310-3990EE6D9BFE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MdAvBench", "benchmark\MdAvBench\MdAvBench.csproj", "{57CC28E5-9915-4AFA-A5E5-B7977BBF8589}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|x64.ActiveCfg = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|x64.Build.0 = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|x86.ActiveCfg = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Debug|x86.Build.0 = Debug|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|Any CPU.Build.0 = Release|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|x64.ActiveCfg = Release|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|x64.Build.0 = Release|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|x86.ActiveCfg = Release|Any CPU {B5FBF669-6012-4A62-9EAC-9ECB7BD599FC}.Release|x86.Build.0 = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|Any CPU.Build.0 = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|x64.ActiveCfg = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|x64.Build.0 = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|x86.ActiveCfg = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Debug|x86.Build.0 = Debug|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|Any CPU.Build.0 = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|x64.ActiveCfg = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|x64.Build.0 = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|x86.ActiveCfg = Release|Any CPU {E4F9286F-A812-476E-A37D-EE2B8ED5D958}.Release|x86.Build.0 = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|Any CPU.Build.0 = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|x64.ActiveCfg = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|x64.Build.0 = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|x86.ActiveCfg = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Debug|x86.Build.0 = Debug|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|Any CPU.ActiveCfg = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|Any CPU.Build.0 = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|x64.ActiveCfg = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|x64.Build.0 = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|x86.ActiveCfg = Release|Any CPU {51318A0F-CB37-4DA2-8AE1-BB985B0A4028}.Release|x86.Build.0 = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|x64.ActiveCfg = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|x64.Build.0 = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|x86.ActiveCfg = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Debug|x86.Build.0 = Debug|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|Any CPU.Build.0 = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|x64.ActiveCfg = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|x64.Build.0 = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|x86.ActiveCfg = Release|Any CPU {BC6B016B-B353-4E73-A98E-556DC70E5850}.Release|x86.Build.0 = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|x64.ActiveCfg = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|x64.Build.0 = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|x86.ActiveCfg = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Debug|x86.Build.0 = Debug|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|Any CPU.Build.0 = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|x64.ActiveCfg = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|x64.Build.0 = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|x86.ActiveCfg = Release|Any CPU {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4}.Release|x86.Build.0 = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|x64.ActiveCfg = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|x64.Build.0 = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|x86.ActiveCfg = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Debug|x86.Build.0 = Debug|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|Any CPU.Build.0 = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|x64.ActiveCfg = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|x64.Build.0 = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|x86.ActiveCfg = Release|Any CPU {A3A5D851-4041-403A-95F9-F4ADBA9602A0}.Release|x86.Build.0 = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|x64.ActiveCfg = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|x64.Build.0 = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|x86.ActiveCfg = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Debug|x86.Build.0 = Debug|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|Any CPU.Build.0 = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|x64.ActiveCfg = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|x64.Build.0 = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|x86.ActiveCfg = Release|Any CPU {C763BC05-5587-479E-8225-AA33F1F8F55B}.Release|x86.Build.0 = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|x64.ActiveCfg = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|x64.Build.0 = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|x86.ActiveCfg = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Debug|x86.Build.0 = Debug|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|Any CPU.Build.0 = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|x64.ActiveCfg = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|x64.Build.0 = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|x86.ActiveCfg = Release|Any CPU {29F1D11A-4490-4E50-AE72-72DBBA0157BE}.Release|x86.Build.0 = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|x64.ActiveCfg = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|x64.Build.0 = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|x86.ActiveCfg = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Debug|x86.Build.0 = Debug|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|Any CPU.Build.0 = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|x64.ActiveCfg = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|x64.Build.0 = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|x86.ActiveCfg = Release|Any CPU {BC356FD7-5D47-495A-9255-A4199AA8E3A1}.Release|x86.Build.0 = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|x64.ActiveCfg = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|x64.Build.0 = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|x86.ActiveCfg = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Debug|x86.Build.0 = Debug|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|Any CPU.Build.0 = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|x64.ActiveCfg = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|x64.Build.0 = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|x86.ActiveCfg = Release|Any CPU {B3438E36-4598-49FB-8E52-65C231978E4D}.Release|x86.Build.0 = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|Any CPU.Build.0 = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|x64.ActiveCfg = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|x64.Build.0 = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|x86.ActiveCfg = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Debug|x86.Build.0 = Debug|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|Any CPU.ActiveCfg = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|Any CPU.Build.0 = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|x64.ActiveCfg = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|x64.Build.0 = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|x86.ActiveCfg = Release|Any CPU {58E76A9C-D28E-45BD-85F2-885FF3E4B363}.Release|x86.Build.0 = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|x64.ActiveCfg = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|x64.Build.0 = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|x86.ActiveCfg = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Debug|x86.Build.0 = Debug|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|Any CPU.Build.0 = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|x64.ActiveCfg = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|x64.Build.0 = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|x86.ActiveCfg = Release|Any CPU {0514B7E7-23D6-4B9D-8943-995AE57493C1}.Release|x86.Build.0 = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|x64.ActiveCfg = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|x64.Build.0 = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|x86.ActiveCfg = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Debug|x86.Build.0 = Debug|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|Any CPU.Build.0 = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|x64.ActiveCfg = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|x64.Build.0 = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|x86.ActiveCfg = Release|Any CPU {CDAC497D-BF0F-48F0-A156-8DE2DAB15492}.Release|x86.Build.0 = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|x64.ActiveCfg = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|x64.Build.0 = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|x86.ActiveCfg = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Debug|x86.Build.0 = Debug|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|Any CPU.Build.0 = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|x64.ActiveCfg = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|x64.Build.0 = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|x86.ActiveCfg = Release|Any CPU {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF}.Release|x86.Build.0 = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|Any CPU.Build.0 = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|x64.ActiveCfg = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|x64.Build.0 = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|x86.ActiveCfg = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Debug|x86.Build.0 = Debug|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|Any CPU.ActiveCfg = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|Any CPU.Build.0 = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|x64.ActiveCfg = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|x64.Build.0 = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|x86.ActiveCfg = Release|Any CPU {008A6F1F-A1D9-45ED-B3CF-3106A59A4417}.Release|x86.Build.0 = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|Any CPU.Build.0 = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|x64.ActiveCfg = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|x64.Build.0 = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|x86.ActiveCfg = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Debug|x86.Build.0 = Debug|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|Any CPU.ActiveCfg = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|Any CPU.Build.0 = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|x64.ActiveCfg = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|x64.Build.0 = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|x86.ActiveCfg = Release|Any CPU {57CC28E5-9915-4AFA-A5E5-B7977BBF8589}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {51318A0F-CB37-4DA2-8AE1-BB985B0A4028} = {8F902BB3-7573-4180-A911-6CC019E999CC} {BC6B016B-B353-4E73-A98E-556DC70E5850} = {8F902BB3-7573-4180-A911-6CC019E999CC} {B5F20CDD-025F-4BD8-A1AF-0F3EC9B6E8E4} = {AD84CF32-2208-43FD-95CE-31DF8DFEB2CD} {A3A5D851-4041-403A-95F9-F4ADBA9602A0} = {AD84CF32-2208-43FD-95CE-31DF8DFEB2CD} {C763BC05-5587-479E-8225-AA33F1F8F55B} = {AD84CF32-2208-43FD-95CE-31DF8DFEB2CD} {BC356FD7-5D47-495A-9255-A4199AA8E3A1} = {AD84CF32-2208-43FD-95CE-31DF8DFEB2CD} {58E76A9C-D28E-45BD-85F2-885FF3E4B363} = {8F902BB3-7573-4180-A911-6CC019E999CC} {6034AA93-6F76-4F4B-B8AC-0A20E3BD35FF} = {AD84CF32-2208-43FD-95CE-31DF8DFEB2CD} {57CC28E5-9915-4AFA-A5E5-B7977BBF8589} = {F5ED37EA-8110-4262-9310-3990EE6D9BFE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A7580602-9393-4620-89A0-6BFD34FFB7E0} EndGlobalSection EndGlobal ================================================ FILE: Packages.ps1 ================================================ dotnet pack -c Release -o .\Packages ================================================ FILE: README.md ================================================ # Markdown.Avalonia Markdown.Avalonia is a portted version of [MdXaml](https://github.com/whistyun/MdXaml) for [Avalonia UI](https://www.avaloniaui.net/). It can render Markdown with avalonia. ## Sample ![img1](docs/img.demo/scrn1.png) ![img1](docs/img.demo/scrn2.png) ![img1](docs/img.demo/scrn3.png) ![img1](docs/img.demo/scrn4.png) ## Nuget https://www.nuget.org/packages/Markdown.Avalonia/ https://www.nuget.org/packages/Markdown.Avalonia.Tight/ The table of compability with Avalonia's version. | AvaloniaUI | Markdown.Avalonia | |--------------------|-------------------| | 12.0.0 | 12.0.0-a1 | | 11.1.0-beta1 | 11.0.3-a1 | | 11.0.0 | 11.0.0~11.0.3-a1 | | 11.0.0-rc1.1 | 11.0.0-d1~d2 | | 11.0.0-preview8 | 11.0.0-c1 | | 11.0.0-preview7 | 11.0.0-b2 | | 11.0.0-preview6 | 11.0.0-a10, b1 | | 11.0.0-preview5 | 11.0.0-a9 | | 11.0.0-preview4 | 11.0.0-a6, a8 | | 11.0.0-preview3 | 11.0.0-a5 | | 11.0.0-preview1~2 | 11.0.0-a1~a4 | | 0.10.1~0.10.6 | 0.10.4 | | 0.10.0 | 0.10.1~0.10.3 | | 0.9.11 | 0.9.0-a1~a7 | ## [How to use](https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-use) I make some document in [wiki](https://github.com/whistyun/Markdown.Avalonia/wiki) ## License Markdown.Avalonia is licensed under the MIT license. ## Dependencies (Runtime) * Markdown.Avalonia.Tight * Avalonia (MIT) https://github.com/AvaloniaUI/Avalonia * Markdown.Avalonia.SyntaxHigh * AvaloniaEdit (MIT) https://github.com/AvaloniaUI/AvaloniaEdit * MdXaml.Html * HtmlAgilityPack (MIT) https://github.com/zzzprojects/html-agility-pack * Markdown.Avalonia.Svg * Avalonia.Svg (MIT) https://github.com/wieslawsoltes/Svg.Skia ================================================ FILE: benchmark/MdAvBench/Apps/App.axaml ================================================  ================================================ FILE: benchmark/MdAvBench/Apps/App.axaml.cs ================================================ using Avalonia; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Controls; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.Markup.Xaml; using System; using Avalonia.Controls.ApplicationLifetimes; using System.Collections.Generic; using System.Threading; using System.Diagnostics; namespace MdAvBench.Apps { public class App : Application { internal static bool ApplicationStarted = false; internal static Window MainWindow { private set; get; } public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { Debug.Print("OnFrameworkInitializationCompleted Called"); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { Debug.Print("Lifetime is ClassicDesktop"); MainWindow = new Window(); MainWindow.Loaded += (s, e) => Loaded(); desktop.MainWindow = MainWindow ; } else Loaded(); base.OnFrameworkInitializationCompleted(); } private void Loaded() { Debug.Print("MainWindowLoaded"); ApplicationStarted = true; } public static IDisposable Start() { var starter = new AppStarter(); var th = new Thread(starter.Start); th.Start(); return starter; } public static void StartOnThread() { var starter = new AppStarter(); starter.Start(); } } class AppStarter : IDisposable { ClassicDesktopStyleApplicationLifetime lifetime; public void Start() { var builder = AppBuilder.Configure(); builder.UsePlatformDetect(); var ags = new string[0]; lifetime = new ClassicDesktopStyleApplicationLifetime() { Args = ags, ShutdownMode = ShutdownMode.OnMainWindowClose }; builder.SetupWithLifetime(lifetime); lifetime.Start(ags); } public void Dispose() { try { lifetime.Shutdown(); } finally { lifetime.Dispose(); } } } } ================================================ FILE: benchmark/MdAvBench/BenchmarkOfCTextBlock.cs ================================================ using Avalonia; using Avalonia.Media.Imaging; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using System; using System.IO; using System.Threading; using UnitTest.CTxt.Xamls; using MdAvBench.Apps; using Avalonia.Threading; using MdAvBench.Xamls; namespace MdAvBench { [SimpleJob] public class BenchmarkOfCTextBlock { private RenderTargetBitmap _bitmapCTB; private RenderTargetBitmap _bitmapTB; public BenchmarkOfCTextBlock() { App.Start(); while (!App.ApplicationStarted) Thread.Sleep(100); while (App.MainWindow is null) Thread.Sleep(100); Dispatcher.UIThread.Invoke(() => { var dpi = new Vector(96, 96); var size = new Size(400, 400); _bitmapCTB = new RenderTargetBitmap(PixelSize.FromSizeWithDpi(size, dpi), dpi); _bitmapTB = new RenderTargetBitmap(PixelSize.FromSizeWithDpi(size, dpi), dpi); App.MainWindow.Width = 400; }); } [Benchmark] public void DispatcherDelay() { Dispatcher.UIThread.Invoke(() => { }); } [Benchmark] public void RenderByCTextBlock() { CTextBlockData? control = null; Dispatcher.UIThread.Invoke(() => { control = new CTextBlockData(); App.MainWindow.Content = control; }); while (control is null || !control.IsLoaded) ; Dispatcher.UIThread.Invoke(() => { _bitmapCTB.Render(control); }); } [Benchmark] public void RenderByTextBlock() { TextBlockData? control = null; Dispatcher.UIThread.Invoke(() => { control = new TextBlockData(); App.MainWindow.Content = control; }); while (control is null || !control.IsLoaded) ; Dispatcher.UIThread.Invoke(() => { _bitmapTB.Render(control); }); } [GlobalCleanup] public void Cleanup() { string path; path = $@"D:\debugs\renderingCTB_{DateTime.Now.Ticks}.png"; if (!File.Exists(path)) using (var strm = File.OpenWrite(path)) _bitmapCTB.Save(strm); path = $@"D:\debugs\renderingTB_{DateTime.Now.Ticks}.png"; if (!File.Exists(path)) using (var strm = File.OpenWrite(path)) _bitmapTB.Save(strm); } } } ================================================ FILE: benchmark/MdAvBench/MdAvBench.csproj ================================================  Exe net10.0 false Code %(Filename) CTextBlockData.axaml Designer ================================================ FILE: benchmark/MdAvBench/Program.cs ================================================ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; using Microsoft.CodeAnalysis.CSharp.Syntax; using ReverseMarkdown.Converters; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using MdAvBench.Apps; using System.Threading; namespace MdAvBench { class Program { public static void Main(string[] args) { var summaries = BenchmarkRunner.Run(typeof(Program).Assembly); var exporter = AsciiDocExporter.Default; var logger = new StringLogger(); foreach (var summary in summaries) { exporter.ExportToLog(summary, logger); logger.WriteLine(); } File.WriteAllText("summary.md", logger.ToString()); } } } ================================================ FILE: benchmark/MdAvBench/StringLogger.cs ================================================ using BenchmarkDotNet.Loggers; using System.Text; namespace MdAvBench { internal class StringLogger : ILogger { private StringBuilder _builder = new StringBuilder(); public string Id => "StringLogger"; public int Priority => 0; public StringLogger() { } public void Flush() { } public void Write(LogKind logKind, string text) { _builder.Append(text); } public void WriteLine() { _builder.AppendLine(); } public void WriteLine(LogKind logKind, string text) { _builder.AppendLine(text); } public override string ToString() => _builder.ToString(); } } ================================================ FILE: benchmark/MdAvBench/Xamls/CTextBlockData.axaml ================================================  ================================================ FILE: benchmark/MdAvBench/Xamls/CTextBlockData.axaml.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using ColorTextBlock.Avalonia; namespace UnitTest.CTxt.Xamls { public partial class CTextBlockData : UserControl { public CTextBlockData() { this.InitializeComponent(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } } } ================================================ FILE: benchmark/MdAvBench/Xamls/TextBlockData.axaml ================================================ ================================================ FILE: benchmark/MdAvBench/Xamls/TextBlockData.axaml.cs ================================================ using Avalonia.Controls; namespace MdAvBench.Xamls { public partial class TextBlockData : UserControl { public TextBlockData() { InitializeComponent(); } } } ================================================ FILE: demos/Markdown.AvaloniaDemo/App.axaml ================================================  ================================================ FILE: demos/Markdown.AvaloniaDemo/App.axaml.cs ================================================ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Markdown.AvaloniaDemo.ViewModels; using Markdown.AvaloniaDemo.Views; namespace Markdown.AvaloniaDemo { public class App : Application { public override void Initialize() { AvaloniaXamlLoader.Load(this); } public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(), }; } base.OnFrameworkInitializationCompleted(); } } } ================================================ FILE: demos/Markdown.AvaloniaDemo/Assets/AppendingStyles.axaml ================================================ ================================================ FILE: demos/Markdown.AvaloniaDemo/Assets/Pegasus-Mode.xshd ================================================  TODO FIXME HACK UNDONE \{ \} \#error \#parse @namespace @accessibility @classname @ignorecase @resources @start @trace @using @members -memoize -lexical -export -public ///(?!/) // /\* \*/ \[ \] " " ' ' @" " \$" " \{ \} [?,.;(){}+\-/%*<>^+~!|&]+ ================================================ FILE: demos/Markdown.AvaloniaDemo/Assets/XamlTemplate.txt ================================================  ================================================ FILE: demos/Markdown.AvaloniaDemo/DynamicStyleBehavior.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; using Markdown.Avalonia; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Windows.Input; namespace Markdown.AvaloniaDemo { public class DynamicStyleBehavior { public static readonly AttachedProperty MyIdProperty = AvaloniaProperty.RegisterAttached("MyId"); public static readonly AttachedProperty MyLastParsedProperty = AvaloniaProperty.RegisterAttached("MyLastParsed"); public static readonly AttachedProperty XamlTextProperty = AvaloniaProperty.RegisterAttached( "XamlText", coerce: Validate); public static readonly AttachedProperty ValidationResultProperty = AvaloniaProperty.RegisterAttached( "ValidationResult"); public static string Validate(AvaloniaObject obj, string xamlTxt) { var ctrl = obj as MarkdownScrollViewer; try { var old = ctrl.GetValue(MyLastParsedProperty); if (old == xamlTxt) return xamlTxt; var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xamlTxt); style.SetValue(MyIdProperty, nameof(DynamicStyleBehavior)); foreach (var exists in ctrl.Styles.ToArray()) if (exists is Styles existsStyle) if (existsStyle.GetValue(MyIdProperty) == nameof(DynamicStyleBehavior)) ctrl.Styles.Remove(exists); ctrl.SetValue(MyLastParsedProperty, xamlTxt); ctrl.Styles.Add(style); var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(null); } catch (Exception e) { string message = e.GetType().FullName + "\r\n" + e.Message; var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(message); } return xamlTxt; } public static void SetXamlText(MarkdownScrollViewer ctrl, string xamlTxt) => ctrl.SetValue(XamlTextProperty, xamlTxt); public static string GetXamlText(MarkdownScrollViewer ctrl) => ctrl.GetValue(XamlTextProperty); public static void SetValidationResult(MarkdownScrollViewer ctrl, ICommand cmd) => ctrl.SetValue(ValidationResultProperty, cmd); public static ICommand GetValidationResult(MarkdownScrollViewer ctrl) => ctrl.GetValue(ValidationResultProperty); } } ================================================ FILE: demos/Markdown.AvaloniaDemo/MainWindow.md ================================================ # MdXaml # MdXaml is a modify version of Markdown.Xaml. Markdown XAML is a port of the popular *MarkdownSharp* Markdown processor, but with one very significant difference: Instead of rendering to a string containing HTML, it renders to a FlowDocument suitable for embedding into a WPF window or usercontrol. ## The Example Of ... ## ### Text decolation [included original enhance] ### *italic*, **bold**, ***bold-italic***, ~~strikethrough~~, __underline__ and %{color:red}color%. %{color:blue}***~~__Mixing Text__~~***% ### Link ### Links [Go to Google!](https://www.google.com) Links with title [Go to Google!](https://www.google.com "google.") Links with image [![faviicon](https://www.google.com/favicon.ico)](https://www.google.com "google favicon") ### Image ### #### Remote images #### ![image1](https://github.com/whistyun/Markdown.Avalonia/raw/master/docs/img.demo/scrn1.png) #### Local and resource images #### ![localimage](LocalPath.png) ![ResourceImage](Assets/ResourceImage.png) ![VectorImage](Assets/Vector.svg) ### List ### #### ul * one * two #### ol 1. one 2. two #### alphabet-ol [original enhance] a. one b. two #### roman-ol [original enhance] i, one ii, two ### Table [included original enhance] ### |a|b|c|d| |:-:|:-|-:| |a1234567890|b1234567890|c1234567890|d1234567890| |a|/2.b|c|d| |A|\2.C| |1|2|3|4| |あ|い|う|え| ### Code ### Markdown.Xaml support ```inline code ``` and block code. ```c #include int main() { // printf() displays the string inside quotation printf("Hello, World!"); return 0; } ``` ### Note ### < notetext >

. notetext ### Separator ### single line --- two lines === bold line *** bold with single ___ ### Blockquote ### > ## Features ## > MarkDown.Xaml has a number of convenient features > > * The engine itself is a single file, for easy inclusion in your own projects > * Code for the engine is structured in the same manner as the original MarkdownSharp > * Includes a `TextToFlowDocumentConverter` to make it easy to bind Markdown text ### Text Alignment [original enhance] ### MdXaml parse a head of paragraph. If 'p[<=>].' is found, apply text alignment to it. > p<. left alignment text > > p>. right alignment text > > p=. center alignment ## What is this Demo? ## This demo application shows MdXaml in use - as you make changes to the left pane, the rendered MarkDown will appear in the right pane. ### Source ### Review the source for this demo to see how MdXaml works in practice, how to use the MarkdownScrollViewer, and how to style the output to appear the way you desire. ================================================ FILE: demos/Markdown.AvaloniaDemo/Markdown.AvaloniaDemo.csproj ================================================  WinExe false $(DemoAppTargetFrameworks) 9 disable true PreserveNewest PreserveNewest ================================================ FILE: demos/Markdown.AvaloniaDemo/MyConverter.cs ================================================ using Avalonia.Controls; using Avalonia.Data.Converters; using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace Markdown.AvaloniaDemo { public class MyConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is ContentControl ctrl) { return ctrl.Content?.ToString(); } return value?.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } ================================================ FILE: demos/Markdown.AvaloniaDemo/Program.cs ================================================ using System; using System.Diagnostics; using Avalonia; using Avalonia.Logging; using Serilog; namespace Markdown.AvaloniaDemo { class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. public static void Main(string[] args) { Log.Logger = new LoggerConfiguration() .WriteTo.File("log.txt") .CreateLogger(); try { BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } catch (Exception e) { Log.Fatal(e, "Something very bad happened"); } } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect(); } } ================================================ FILE: demos/Markdown.AvaloniaDemo/ViewLocator.cs ================================================ using System; using Avalonia.Controls; using Avalonia.Controls.Templates; using Markdown.AvaloniaDemo.ViewModels; namespace Markdown.AvaloniaDemo { public class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; public Control Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); var type = Type.GetType(name); if (type != null) { return (Control)Activator.CreateInstance(type); } else { return new TextBlock { Text = "Not Found: " + name }; } } public bool Match(object data) { return data is ViewModelBase; } } } ================================================ FILE: demos/Markdown.AvaloniaDemo/ViewModels/MainWindowViewModel.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.Themes.Simple; using Markdown.Avalonia; using ReactiveUI; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace Markdown.AvaloniaDemo.ViewModels { public class MainWindowViewModel : ViewModelBase { private string _text; public string Text { get => _text; set => this.RaiseAndSetIfChanged(ref _text, value); } private string _edittingStyleXamlText; public string EdittingStyleXamlText { get => _edittingStyleXamlText; set => this.RaiseAndSetIfChanged(ref _edittingStyleXamlText, value); } private string _appendStyleXamlText; public string AppendStyleXamlText { get => _appendStyleXamlText; set => this.RaiseAndSetIfChanged(ref _appendStyleXamlText, value); } private StyleViewModel _selectedStyle; public StyleViewModel SelectedStyle { get => _selectedStyle; set => this.RaiseAndSetIfChanged(ref _selectedStyle, value); } private string _ErrorInfo; public string ErrorInfo { get => _ErrorInfo; set => this.RaiseAndSetIfChanged(ref _ErrorInfo, value); } public List Styles { set; get; } public void XamlParseResult(string result) => ErrorInfo = result; public void TryParse() => AppendStyleXamlText = EdittingStyleXamlText; public MainWindowViewModel() { try { using (var stream = new FileStream("MainWindow.md", FileMode.Open)) using (var reader = new StreamReader(stream)) { Text = reader.ReadToEnd(); } } catch { } Styles = new List { new StyleViewModel() { Name = nameof(MarkdownStyle.Standard) }, new StyleViewModel() { Name = nameof(MarkdownStyle.SimpleTheme) }, new StyleViewModel() { Name = nameof(MarkdownStyle.GithubLike) } }; SelectedStyle = Styles[1]; using (var strm = AssetLoader.Open(new Uri("avares://Markdown.AvaloniaDemo/Assets/XamlTemplate.txt"))) using (var reader = new StreamReader(strm)) { EdittingStyleXamlText = reader.ReadToEnd(); } } } public class StyleViewModel { public string Name { get; set; } } } ================================================ FILE: demos/Markdown.AvaloniaDemo/ViewModels/ViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Text; using ReactiveUI; namespace Markdown.AvaloniaDemo.ViewModels { public class ViewModelBase : ReactiveObject { } } ================================================ FILE: demos/Markdown.AvaloniaDemo/Views/MainWindow.axaml ================================================  Append Styles ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/Assets/XamlTemplate.txt ================================================  ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/Assets2/MainWindow.md ================================================ ### Image ### #### Remote images #### ![image1](http://placehold.it/300x25) ![imageleft](http://placehold.it/150x25/0000FF "blue")![imageright](http://placehold.it/150x25/00FFFF "cyan") #### Local and resource images #### ![localimage](Assets2/ResourceImage.png) ![ResourceImage](Assets/ResourceImage.png) ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/DynamicStyleBehavior.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; using Markdown.Avalonia; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Windows.Input; namespace Markdown.AvaloniaFluentAvaloniaDemo { public class DynamicStyleBehavior { public static readonly AttachedProperty MyIdProperty = AvaloniaProperty.RegisterAttached("MyId"); public static readonly AttachedProperty MyLastParsedProperty = AvaloniaProperty.RegisterAttached("MyLastParsed"); public static readonly AttachedProperty XamlTextProperty = AvaloniaProperty.RegisterAttached( "XamlText", coerce: Validate); public static readonly AttachedProperty ValidationResultProperty = AvaloniaProperty.RegisterAttached( "ValidationResult"); public static string Validate(AvaloniaObject obj, string xamlTxt) { var ctrl = obj as MarkdownScrollViewer; try { var old = ctrl.GetValue(MyLastParsedProperty); if (old == xamlTxt) return xamlTxt; var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xamlTxt); style.SetValue(MyIdProperty, nameof(DynamicStyleBehavior)); foreach (var exists in ctrl.Styles.ToArray()) if (exists is Styles existsStyle) if (existsStyle.GetValue(MyIdProperty) == nameof(DynamicStyleBehavior)) ctrl.Styles.Remove(exists); ctrl.SetValue(MyLastParsedProperty, xamlTxt); ctrl.Styles.Add(style); var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(null); } catch (Exception e) { string message = e.GetType().FullName + "\r\n" + e.Message; var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(message); } return xamlTxt; } public static void SetXamlText(MarkdownScrollViewer ctrl, string xamlTxt) => ctrl.SetValue(XamlTextProperty, xamlTxt); public static string GetXamlText(MarkdownScrollViewer ctrl) => ctrl.GetValue(XamlTextProperty); public static void SetValidationResult(MarkdownScrollViewer ctrl, ICommand cmd) => ctrl.SetValue(ValidationResultProperty, cmd); public static ICommand GetValidationResult(MarkdownScrollViewer ctrl) => ctrl.GetValue(ValidationResultProperty); } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/MainWindow.md ================================================ # MdXaml # MdXaml is a modify version of Markdown.Xaml. Markdown XAML is a port of the popular *MarkdownSharp* Markdown processor, but with one very significant difference: Instead of rendering to a string containing HTML, it renders to a FlowDocument suitable for embedding into a WPF window or usercontrol. ## The Example Of ... ## ### Text decolation [included original enhance] ### *italic*, **bold**, ***bold-italic***, ~~strikethrough~~, __underline__ and %{color:red}color%. %{color:blue}***~~__Mixing Text__~~***% ### Link ### Links [Go to Google!](https://www.google.com) Links with title [Go to Google!](https://www.google.com "google.") Links with image [![faviicon](https://www.google.com/favicon.ico)](https://www.google.com "google favicon") ### Image ### #### Remote images #### ![image1](https://github.com/whistyun/Markdown.Avalonia/raw/master/docs/img.demo/scrn1.png) #### Local and resource images #### ![localimage](LocalPath.png) ![ResourceImage](Assets/ResourceImage.png) ### List ### #### ul * one * two #### ol 1. one 2. two #### alphabet-ol [original enhance] a. one b. two #### roman-ol [original enhance] i, one ii, two ### Table [included original enhance] ### |a|b|c|d| |:-:|:-|-:| |a1234567890|b1234567890|c1234567890|d1234567890| |a|/2.b|c|d| |A|\2.C| |1|2|3|4| |あ|い|う|え| ### Code ### Markdown.Xaml support ```inline code ``` and block code. ```c #include int main() { // printf() displays the string inside quotation printf("Hello, World!"); return 0; } ``` ### Note ### < notetext >

. notetext ### Separator ### single line --- two lines === bold line *** bold with single ___ ### Blockquote ### > ## Features ## > MarkDown.Xaml has a number of convenient features > > * The engine itself is a single file, for easy inclusion in your own projects > * Code for the engine is structured in the same manner as the original MarkdownSharp > * Includes a `TextToFlowDocumentConverter` to make it easy to bind Markdown text ### Text Alignment [original enhance] ### MdXaml parse a head of paragraph. If 'p[<=>].' is found, apply text alignment to it. > p<. left alignment text > > p>. right alignment text > > p=. center alignment ## What is this Demo? ## This demo application shows MdXaml in use - as you make changes to the left pane, the rendered MarkDown will appear in the right pane. ### Source ### Review the source for this demo to see how MdXaml works in practice, how to use the MarkdownScrollViewer, and how to style the output to appear the way you desire. ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/Markdown.AvaloniaFluentAvaloniaDemo.csproj ================================================  WinExe false $(DemoAppTargetFrameworks) 9 disable PreserveNewest PreserveNewest ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/MyConverter.cs ================================================ using Avalonia.Controls; using Avalonia.Data.Converters; using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace Markdown.AvaloniaFluentAvaloniaDemo { public class MyConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is ContentControl ctrl) { return ctrl.Content?.ToString(); } return value?.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/Program.cs ================================================ using System; using Avalonia; namespace Markdown.AvaloniaFluentAvaloniaDemo { class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect(); } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/ViewLocator.cs ================================================ using System; using Avalonia.Controls; using Avalonia.Controls.Templates; using Markdown.AvaloniaFluentAvaloniaDemo.ViewModels; namespace Markdown.AvaloniaFluentAvaloniaDemo { public class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; public Control Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); var type = Type.GetType(name); if (type != null) { return (Control)Activator.CreateInstance(type); } else { return new TextBlock { Text = "Not Found: " + name }; } } public bool Match(object data) { return data is ViewModelBase; } } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/ViewModels/MainWindowViewModel.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using Avalonia.Styling; using Markdown.Avalonia; using ReactiveUI; using System; using System.Collections.Generic; using System.IO; namespace Markdown.AvaloniaFluentAvaloniaDemo.ViewModels { public class MainWindowViewModel : ViewModelBase { private string _text; public string Text { get => _text; set => this.RaiseAndSetIfChanged(ref _text, value); } private string _edittingStyleXamlText; public string EdittingStyleXamlText { get => _edittingStyleXamlText; set => this.RaiseAndSetIfChanged(ref _edittingStyleXamlText, value); } private string _appendStyleXamlText; public string AppendStyleXamlText { get => _appendStyleXamlText; set => this.RaiseAndSetIfChanged(ref _appendStyleXamlText, value); } private string _ErrorInfo; public string ErrorInfo { get => _ErrorInfo; set => this.RaiseAndSetIfChanged(ref _ErrorInfo, value); } private string _AssetPathRootText; public string AssetPathRootText { get => _AssetPathRootText; set => this.RaiseAndSetIfChanged(ref _AssetPathRootText, value); } private string _SourceText; public string SourceText { get => _SourceText; set => this.RaiseAndSetIfChanged(ref _SourceText, value); } private string _AssetPathRoot; public string AssetPathRoot { get => _AssetPathRoot; set { _AssetPathRoot = value; this.RaisePropertyChanged(); } } private Uri _Source; public Uri Source { get => _Source; set { _Source = value; this.RaisePropertyChanged(); } } public void XamlParseResult(string result) => ErrorInfo = result; public void TryParse() => AppendStyleXamlText = EdittingStyleXamlText; public MainWindowViewModel() { using (var stream = new FileStream("MainWindow.md", FileMode.Open)) using (var reader = new StreamReader(stream)) { Text = reader.ReadToEnd(); } using (var strm = AssetLoader.Open(new Uri("avares://Markdown.AvaloniaFluentAvaloniaDemo/Assets/XamlTemplate.txt"))) using (var reader = new StreamReader(strm)) { EdittingStyleXamlText = reader.ReadToEnd(); } } public void ApplyAssetPathRoot() => AssetPathRoot = AssetPathRootText; public void ApplySource() => Source = new Uri(SourceText); } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/ViewModels/ViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Text; using ReactiveUI; namespace Markdown.AvaloniaFluentAvaloniaDemo.ViewModels { public class ViewModelBase : ReactiveObject { } } ================================================ FILE: demos/Markdown.AvaloniaFluentAvaloniaDemo/Views/MainWindow.axaml ================================================  Append Styles ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/Assets/XamlTemplate.txt ================================================  ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/Assets2/MainWindow.md ================================================ ### Image ### #### Remote images #### ![image1](http://placehold.it/300x25) ![imageleft](http://placehold.it/150x25/0000FF "blue")![imageright](http://placehold.it/150x25/00FFFF "cyan") #### Local and resource images #### ![localimage](Assets2/ResourceImage.png) ![ResourceImage](Assets/ResourceImage.png) ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/DynamicStyleBehavior.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.Styling; using Avalonia.Styling; using Markdown.Avalonia; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Windows.Input; namespace Markdown.AvaloniaFluentDemo { public class DynamicStyleBehavior { public static readonly AttachedProperty MyIdProperty = AvaloniaProperty.RegisterAttached("MyId"); public static readonly AttachedProperty MyLastParsedProperty = AvaloniaProperty.RegisterAttached("MyLastParsed"); public static readonly AttachedProperty XamlTextProperty = AvaloniaProperty.RegisterAttached( "XamlText", coerce: Validate); public static readonly AttachedProperty ValidationResultProperty = AvaloniaProperty.RegisterAttached( "ValidationResult"); public static string Validate(AvaloniaObject obj, string xamlTxt) { var ctrl = obj as MarkdownScrollViewer; try { var old = ctrl.GetValue(MyLastParsedProperty); if (old == xamlTxt) return xamlTxt; var style = (Styles)AvaloniaRuntimeXamlLoader.Load(xamlTxt); style.SetValue(MyIdProperty, nameof(DynamicStyleBehavior)); foreach (var exists in ctrl.Styles.ToArray()) if (exists is Styles existsStyle) if (existsStyle.GetValue(MyIdProperty) == nameof(DynamicStyleBehavior)) ctrl.Styles.Remove(exists); ctrl.SetValue(MyLastParsedProperty, xamlTxt); ctrl.Styles.Add(style); var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(null); } catch (Exception e) { string message = e.GetType().FullName + "\r\n" + e.Message; var resultMsgr = ctrl.GetValue(ValidationResultProperty); resultMsgr?.Execute(message); } return xamlTxt; } public static void SetXamlText(MarkdownScrollViewer ctrl, string xamlTxt) => ctrl.SetValue(XamlTextProperty, xamlTxt); public static string GetXamlText(MarkdownScrollViewer ctrl) => ctrl.GetValue(XamlTextProperty); public static void SetValidationResult(MarkdownScrollViewer ctrl, ICommand cmd) => ctrl.SetValue(ValidationResultProperty, cmd); public static ICommand GetValidationResult(MarkdownScrollViewer ctrl) => ctrl.GetValue(ValidationResultProperty); } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/MainWindow.md ================================================ # MdXaml # MdXaml is a modify version of Markdown.Xaml. Markdown XAML is a port of the popular *MarkdownSharp* Markdown processor, but with one very significant difference: Instead of rendering to a string containing HTML, it renders to a FlowDocument suitable for embedding into a WPF window or usercontrol. ## The Example Of ... ## ### Text decolation [included original enhance] ### *italic*, **bold**, ***bold-italic***, ~~strikethrough~~, __underline__ and %{color:red}color%. %{color:blue}***~~__Mixing Text__~~***% ### Link ### Links [Go to Google!](https://www.google.com) Links with title [Go to Google!](https://www.google.com "google.") Links with image [![faviicon](https://www.google.com/favicon.ico)](https://www.google.com "google favicon") ### Image ### #### Remote images #### ![image1](https://github.com/whistyun/Markdown.Avalonia/raw/master/docs/img.demo/scrn1.png) #### Local and resource images #### ![localimage](LocalPath.png) ![ResourceImage](Assets/ResourceImage.png) ![Svg](Assets/Vector.svg) ### List ### #### ul * one * two #### ol 1. one 2. two #### alphabet-ol [original enhance] a. one b. two #### roman-ol [original enhance] i, one ii, two ### Table [included original enhance] ### |a|b|c|d| |:-:|:-|-:| |a1234567890|b1234567890|c1234567890|d1234567890| |a|/2.b|c|d| |A|\2.C| |1|2|3|4| |あ|い|う|え| ### Code ### Markdown.Xaml support ```inline code ``` and block code. ```c #include int main() { // printf() displays the string inside quotation printf("Hello, World!"); return 0; } ``` ### Note ### < notetext >

. notetext ### Separator ### single line --- two lines === bold line *** bold with single ___ ### Blockquote ### > ## Features ## > MarkDown.Xaml has a number of convenient features > > * The engine itself is a single file, for easy inclusion in your own projects > * Code for the engine is structured in the same manner as the original MarkdownSharp > * Includes a `TextToFlowDocumentConverter` to make it easy to bind Markdown text ### Text Alignment [original enhance] ### MdXaml parse a head of paragraph. If 'p[<=>].' is found, apply text alignment to it. > p<. left alignment text > > p>. right alignment text > > p=. center alignment ## What is this Demo? ## This demo application shows MdXaml in use - as you make changes to the left pane, the rendered MarkDown will appear in the right pane. ### Source ### Review the source for this demo to see how MdXaml works in practice, how to use the MarkdownScrollViewer, and how to style the output to appear the way you desire. ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/Markdown.AvaloniaFluentDemo.csproj ================================================  WinExe false $(DemoAppTargetFrameworks) 9 disable PreserveNewest PreserveNewest ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/MyConverter.cs ================================================ using Avalonia.Controls; using Avalonia.Data.Converters; using System; using System.Collections.Generic; using System.Globalization; using System.Text; namespace Markdown.AvaloniaFluentDemo { public class MyConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is ContentControl ctrl) { return ctrl.Content?.ToString(); } return value?.ToString(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/Program.cs ================================================ using System; using Avalonia; namespace Markdown.AvaloniaFluentDemo { class Program { // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect(); } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/ViewLocator.cs ================================================ using System; using Avalonia.Controls; using Avalonia.Controls.Templates; using Markdown.AvaloniaFluentDemo.ViewModels; namespace Markdown.AvaloniaFluentDemo { public class ViewLocator : IDataTemplate { public bool SupportsRecycling => false; public Control Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); var type = Type.GetType(name); if (type != null) { return (Control)Activator.CreateInstance(type); } else { return new TextBlock { Text = "Not Found: " + name }; } } public bool Match(object data) { return data is ViewModelBase; } } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/ViewModels/MainWindowViewModel.cs ================================================ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; using Avalonia.Styling; using Markdown.Avalonia; using ReactiveUI; using System; using System.Collections.Generic; using System.IO; namespace Markdown.AvaloniaFluentDemo.ViewModels { public class MainWindowViewModel : ViewModelBase { private string _text; public string Text { get => _text; set => this.RaiseAndSetIfChanged(ref _text, value); } private string _edittingStyleXamlText; public string EdittingStyleXamlText { get => _edittingStyleXamlText; set => this.RaiseAndSetIfChanged(ref _edittingStyleXamlText, value); } private string _appendStyleXamlText; public string AppendStyleXamlText { get => _appendStyleXamlText; set => this.RaiseAndSetIfChanged(ref _appendStyleXamlText, value); } private string _ErrorInfo; public string ErrorInfo { get => _ErrorInfo; set => this.RaiseAndSetIfChanged(ref _ErrorInfo, value); } private string _AssetPathRootText; public string AssetPathRootText { get => _AssetPathRootText; set => this.RaiseAndSetIfChanged(ref _AssetPathRootText, value); } private string _SourceText; public string SourceText { get => _SourceText; set => this.RaiseAndSetIfChanged(ref _SourceText, value); } private string _AssetPathRoot; public string AssetPathRoot { get => _AssetPathRoot; set { _AssetPathRoot = value; this.RaisePropertyChanged(); } } private Uri _Source; public Uri Source { get => _Source; set { _Source = value; this.RaisePropertyChanged(); } } public void XamlParseResult(string result) => ErrorInfo = result; public void TryParse() => AppendStyleXamlText = EdittingStyleXamlText; public MainWindowViewModel() { using (var stream = new FileStream("MainWindow.md", FileMode.Open)) using (var reader = new StreamReader(stream)) { Text = reader.ReadToEnd(); } using (var strm = AssetLoader.Open(new Uri("avares://Markdown.AvaloniaFluentDemo/Assets/XamlTemplate.txt"))) using (var reader = new StreamReader(strm)) { EdittingStyleXamlText = reader.ReadToEnd(); } } public void ApplyAssetPathRoot() => AssetPathRoot = AssetPathRootText; public void ApplySource() => Source = new Uri(SourceText); } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/ViewModels/ViewModelBase.cs ================================================ using System; using System.Collections.Generic; using System.Text; using ReactiveUI; namespace Markdown.AvaloniaFluentDemo.ViewModels { public class ViewModelBase : ReactiveObject { } } ================================================ FILE: demos/Markdown.AvaloniaFluentDemo/Views/MainWindow.axaml ================================================  Append Styles