diff --git a/.github/workflows/build-net40.yml b/.github/workflows/build-net40.yml
index 52a0d9ba..8bf4ec04 100644
--- a/.github/workflows/build-net40.yml
+++ b/.github/workflows/build-net40.yml
@@ -43,5 +43,8 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: wpfdevelopers-net40
- path: OutPut/**/WPFDevelopers.dll
+ path: |
+ OutPut/**/WPFDevelopers.dll
+ OutPut/**/en-US/WPFDevelopers.resources.dll
+ OutPut/**/ko-KR/WPFDevelopers.resources.dll
if-no-files-found: error
diff --git a/.github/workflows/build-net45x.yml b/.github/workflows/build-net45x.yml
index 7c773fcd..fc5a6c48 100644
--- a/.github/workflows/build-net45x.yml
+++ b/.github/workflows/build-net45x.yml
@@ -54,9 +54,33 @@ jobs:
}
}
+ - name: Rename windows folders to windows7
+ shell: pwsh
+ run: |
+ if (-not (Test-Path OutPut)) {
+ exit 0
+ }
+
+ Get-ChildItem OutPut -Directory |
+ Where-Object { $_.Name -match '^net\d+\.\d+-windows$' } |
+ ForEach-Object {
+ $source = $_.FullName
+ $alias = Join-Path $_.Parent.FullName ($_.Name + '7')
+
+ if (Test-Path $alias) {
+ Remove-Item $alias -Recurse -Force
+ }
+
+ Move-Item $source $alias
+ Write-Host "Renamed folder: $source -> $alias"
+ }
+
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: wpfdevelopers-net45x
- path: OutPut/**/WPFDevelopers.dll
+ path: |
+ OutPut/**/WPFDevelopers.dll
+ OutPut/**/en-US/WPFDevelopers.resources.dll
+ OutPut/**/ko-KR/WPFDevelopers.resources.dll
if-no-files-found: error
diff --git a/README.md b/README.md
index d47ca0a1..ebaadbc5 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,16 @@ xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
`xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"`
+----------
+### MarkdownViewer
+
+
+
+----------
+### SwitchTheme
+
+
+
----------
### CircleMenu
diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md
index 1b41409c..506aa3b6 100644
--- a/docs/en/getting-started.md
+++ b/docs/en/getting-started.md
@@ -98,6 +98,24 @@ Then use WD controls:
```
+### MarkdownViewer Example
+
+`wd:MarkdownViewer` is used to render Markdown content (headings, lists, quotes, code blocks, tables, links, and images).
+
+```xml
+
+
+
+```
+
+You can find the complete sample here:
+
+- `src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml`
+- `src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml.cs`
+
---
## Three Usage Patterns
diff --git a/docs/zh/getting-started.md b/docs/zh/getting-started.md
index e7cc2d99..691920e5 100644
--- a/docs/zh/getting-started.md
+++ b/docs/zh/getting-started.md
@@ -98,6 +98,24 @@ xmlns:wd="https://github.com/WPFDevelopersOrg/WPFDevelopers"
```
+### MarkdownViewer 示例
+
+`wd:MarkdownViewer` 用于渲染 Markdown 内容(标题、列表、引用、代码块、表格、链接、图片等)。
+
+```xml
+
+
+
+```
+
+完整示例可参考:
+
+- `src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml`
+- `src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml.cs`
+
---
## 三种使用方式
diff --git a/src/Package.props b/src/Package.props
index 89725a14..b5e56924 100644
--- a/src/Package.props
+++ b/src/Package.props
@@ -7,10 +7,10 @@
true
snupkg
Copyright © WPFDevelopersOrg 2026
- 0.1.0.0
- 0.1.0.0
- 0.1.0.0
+ 0.2.0.0
+ 0.2.0.0
+ 0.2.0.0
https://github.com/WPFDevelopersOrg/WPFDevelopers
Debug;Release;Debug-.NET40;Release-.NET40
-
\ No newline at end of file
+
diff --git a/src/WPFDevelopers.Net40/WPFDevelopers.Net40.csproj b/src/WPFDevelopers.Net40/WPFDevelopers.Net40.csproj
index e5b3510a..89c3cb42 100644
--- a/src/WPFDevelopers.Net40/WPFDevelopers.Net40.csproj
+++ b/src/WPFDevelopers.Net40/WPFDevelopers.Net40.csproj
@@ -3,7 +3,6 @@
PackageReference
net40
- True
@@ -23,4 +22,4 @@
-
\ No newline at end of file
+
diff --git a/src/WPFDevelopers.Net45x/WPFDevelopers.Net45x.csproj b/src/WPFDevelopers.Net45x/WPFDevelopers.Net45x.csproj
index ce8fd42d..d1305be6 100644
--- a/src/WPFDevelopers.Net45x/WPFDevelopers.Net45x.csproj
+++ b/src/WPFDevelopers.Net45x/WPFDevelopers.Net45x.csproj
@@ -3,7 +3,6 @@
true
- True
@@ -17,4 +16,4 @@
-
\ No newline at end of file
+
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/ThemeExample.xaml b/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/ThemeExample.xaml
index cec219f2..640780d7 100644
--- a/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/ThemeExample.xaml
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/ThemeExample.xaml
@@ -55,13 +55,32 @@
FontSize="20"
Text="Light | Dark" />
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/WindowBasicExample.xaml.cs b/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/WindowBasicExample.xaml.cs
deleted file mode 100644
index 48add32a..00000000
--- a/src/WPFDevelopers.Samples.Shared/ExampleViews/Basics/WindowBasicExample.xaml.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Windows;
-using System.Windows.Controls;
-
-namespace WPFDevelopers.Samples.ExampleViews.Basics
-{
- public partial class WindowBasicExample : UserControl
- {
- public WindowBasicExample() { InitializeComponent(); }
- private void Button_Click(object sender, RoutedEventArgs e) => new ToolWindow().Show();
- private void ButtonNone_Click(object sender, RoutedEventArgs e) => new NoneTitleBarWindow().Show();
- }
-}
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/GaugeExample.xaml b/src/WPFDevelopers.Samples.Shared/ExampleViews/GaugeExample.xaml
index dd5b1a23..e3898aae 100644
--- a/src/WPFDevelopers.Samples.Shared/ExampleViews/GaugeExample.xaml
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/GaugeExample.xaml
@@ -82,6 +82,34 @@
Maximum="90"
Minimum="30" />
+
+
+
+
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/MainWindow.xaml b/src/WPFDevelopers.Samples.Shared/ExampleViews/MainWindow.xaml
index 877322d9..808ceea5 100644
--- a/src/WPFDevelopers.Samples.Shared/ExampleViews/MainWindow.xaml
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/MainWindow.xaml
@@ -20,19 +20,51 @@
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -40,9 +72,9 @@
/// MainWindow.xaml 的交互逻辑
///
- public partial class MainWindow
+ public partial class MainWindow
{
public MainWindow()
{
@@ -29,6 +31,15 @@ private void OnMainWindow_Loaded(object sender, RoutedEventArgs e)
ThemeColorPicker.SelectedColor = ThemeManager.Instance.PrimaryColor;
}
+ private void ThemeSwitchBtn_Click(object sender, RoutedEventArgs e)
+ {
+ var targetTheme = App.Theme == ThemeType.Dark ? ThemeType.Light : ThemeType.Dark;
+ ThemeManager.Instance.SwitchTheme(ThemeSwitchBtn, targetTheme);
+ App.Theme = targetTheme;
+ SetThemeSource();
+ ThemeSwitchBtn.IsChecked = targetTheme == ThemeType.Dark;
+ }
+
private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (MessageBox.Show("是否退出当前系统?", "询问", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
@@ -63,7 +74,7 @@ private void Grayscale_Click(object sender, RoutedEventArgs e)
menuItemGrayscale.Header = "关闭灰度";
CreateGrayscale(0);
}
-
+
else
{
menuItemGrayscale.Header = "开启灰度";
@@ -167,13 +178,13 @@ private void ScreenCaptureExt_SnapCompleted(System.Windows.Media.Imaging.BitmapS
private void ColorPicker_Apply(object sender, RoutedEventArgs e)
{
- ThemeSwitchBtn.IsChecked = false;
+ ColorSwitchBtn.IsChecked = false;
ThemeManager.Instance.SetColor(ThemeColorPicker.SelectedColor);
}
private void ColorPicker_Cancel(object sender, RoutedEventArgs e)
{
- ThemeSwitchBtn.IsChecked = false;
+ ColorSwitchBtn.IsChecked = false;
}
}
}
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml b/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml
new file mode 100644
index 00000000..1a726fe1
--- /dev/null
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml.cs b/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml.cs
new file mode 100644
index 00000000..eee13f47
--- /dev/null
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/MarkdownViewerExample.xaml.cs
@@ -0,0 +1,202 @@
+using Microsoft.Win32;
+using System;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using WPFDevelopers;
+
+namespace WPFDevelopers.Samples.ExampleViews
+{
+ public partial class MarkdownViewerExample : UserControl
+ {
+ private const double MinZoom = 0.5;
+ private const double MaxZoom = 2.0;
+ private const double ZoomStep = 0.15;
+ private const double BasePreviewFontSize = 13.0;
+
+ private string _currentFilePath = string.Empty;
+ private double _fontScale = 1.0;
+
+ private const string InitialMarkdown =
+@"# WPFDevelopers MarkdownViewer
+
+`MarkdownViewer` supports common Markdown syntax and follows the current WD theme.
+
+## Inline Styles
+- **Bold text**
+- *Italic text*
+- ***Bold italic text***
+- ~~Strikethrough~~
+- `Inline code`
+- [WPFDevelopers GitHub](https://github.com/WPFDevelopersOrg/WPFDevelopers)
+
+## Quote
+> Theme brushes come from WD dynamic resources, so light/dark and primary color switch automatically.
+
+## Code Block
+```csharp
+public static string Hello(string name)
+{
+ return ""Hello, "" + name + ""!"";
+}
+```
+
+```python
+def fibonacci(n):
+ a, b = 0, 1
+ for _ in range(n):
+ yield a
+ a, b = b, a + b
+
+print(list(fibonacci(10)))
+```
+
+```javascript
+const greeting = ""Hello, World!"";
+console.log(greeting);
+
+function sum(a, b) {
+ return a + b;
+}
+```
+
+```typescript
+type User = {
+ id: number;
+ name: string;
+};
+
+const user: User = { id: 1, name: ""WD"" };
+console.log(user.name);
+```
+
+```xaml
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Ordered List
+1. Edit markdown on the left.
+2. Preview updates on the right.
+3. Change WD theme and colors to verify follow-up.
+
+## Table
+| Shortcut | Action |
+| :-- | :-- |
+| Ctrl+O | Open file |
+| Ctrl+S | Save file |
+| Ctrl++ | Zoom in |
+| Ctrl+- | Zoom out |
+
+## Images
+
+";
+
+ public MarkdownViewerExample()
+ {
+ InitializeComponent();
+ EditorTextBox.Text = InitialMarkdown;
+ PreviewViewer.Markdown = InitialMarkdown;
+ ApplyZoom();
+ }
+
+ private void EditorTextBox_OnTextChanged(object sender, TextChangedEventArgs e)
+ {
+ RefreshPreview();
+ }
+
+ private void OpenFile_Click(object sender, RoutedEventArgs e)
+ {
+ var dlg = new OpenFileDialog
+ {
+ Filter = "Markdown Files (*.md)|*.md|All Files (*.*)|*.*"
+ };
+
+ if (dlg.ShowDialog() == true && File.Exists(dlg.FileName))
+ {
+ EditorTextBox.Text = File.ReadAllText(dlg.FileName);
+ _currentFilePath = dlg.FileName;
+ }
+ }
+
+ private void SaveFile_Click(object sender, RoutedEventArgs e)
+ {
+ var dlg = new SaveFileDialog
+ {
+ Filter = "Markdown Files (*.md)|*.md|All Files (*.*)|*.*",
+ FileName = string.IsNullOrWhiteSpace(_currentFilePath) ? "untitled.md" : _currentFilePath
+ };
+
+ if (dlg.ShowDialog() == true)
+ {
+ File.WriteAllText(dlg.FileName, EditorTextBox.Text ?? string.Empty);
+ _currentFilePath = dlg.FileName;
+ }
+ }
+
+ private void LoadSample_Click(object sender, RoutedEventArgs e)
+ {
+ EditorTextBox.Text = InitialMarkdown;
+ RefreshPreview();
+ }
+
+ private void ZoomIn_Click(object sender, RoutedEventArgs e)
+ {
+ _fontScale = Math.Min(_fontScale + ZoomStep, MaxZoom);
+ ApplyZoom();
+ }
+
+ private void ZoomOut_Click(object sender, RoutedEventArgs e)
+ {
+ _fontScale = Math.Max(_fontScale - ZoomStep, MinZoom);
+ ApplyZoom();
+ }
+
+ private void ApplyZoom()
+ {
+ PreviewViewer.FontSize = BasePreviewFontSize * _fontScale;
+ ZoomTextBlock.Text = ((int)Math.Round(_fontScale * 100.0)).ToString() + "%";
+ ForceRefreshPreviewVisual();
+ }
+
+ private void RefreshPreview()
+ {
+ if (PreviewViewer == null)
+ return;
+
+ var markdown = EditorTextBox == null ? string.Empty : (EditorTextBox.Text ?? string.Empty);
+ if (string.Equals(PreviewViewer.Markdown, markdown, StringComparison.Ordinal))
+ PreviewViewer.Markdown = string.Empty;
+
+ PreviewViewer.Markdown = markdown;
+ ForceRefreshPreviewVisual();
+ }
+
+ private void ForceRefreshPreviewVisual()
+ {
+ if (PreviewViewer == null)
+ return;
+
+ PreviewViewer.InvalidateMeasure();
+ PreviewViewer.InvalidateArrange();
+ PreviewViewer.InvalidateVisual();
+ PreviewViewer.UpdateLayout();
+ }
+ }
+}
diff --git a/src/WPFDevelopers.Samples.Shared/ExampleViews/SegmentedExample.xaml b/src/WPFDevelopers.Samples.Shared/ExampleViews/SegmentedExample.xaml
index 5cee0498..4485f908 100644
--- a/src/WPFDevelopers.Samples.Shared/ExampleViews/SegmentedExample.xaml
+++ b/src/WPFDevelopers.Samples.Shared/ExampleViews/SegmentedExample.xaml
@@ -1,4 +1,4 @@
-
diff --git a/src/WPFDevelopers.Samples.Shared/Helpers/MenuEnum.cs b/src/WPFDevelopers.Samples.Shared/Helpers/MenuEnum.cs
index 0b2fddc5..281918f7 100644
--- a/src/WPFDevelopers.Samples.Shared/Helpers/MenuEnum.cs
+++ b/src/WPFDevelopers.Samples.Shared/Helpers/MenuEnum.cs
@@ -19,7 +19,6 @@ public enum MenuEnum
CycleLoading,
RollLoading,
//CutImage,
- Window,
CropAvatar,
AnimationAudio,
AMap,
@@ -98,6 +97,7 @@ public enum MenuEnum
AnimationGrid,
AllPathIcon,
Dial,
+ MarkdownViewer,
SvgViewer,
DataGridFilter,
GrayscaleEffect,
@@ -109,7 +109,7 @@ public enum MenuEnum
AcrylicBlur,
Theme,
Menu,
- BasicWindow,
+ Window,
BasicLoading,
Button,
RadioButton,
diff --git a/src/WPFDevelopers.Samples.Shared/ViewModels/MainVM.cs b/src/WPFDevelopers.Samples.Shared/ViewModels/MainVM.cs
index 27b5b112..2d029759 100644
--- a/src/WPFDevelopers.Samples.Shared/ViewModels/MainVM.cs
+++ b/src/WPFDevelopers.Samples.Shared/ViewModels/MainVM.cs
@@ -4,9 +4,7 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
-using System.Windows.Controls;
using System.Windows.Input;
-using WPFDevelopers.Controls;
using WPFDevelopers.Helpers;
using WPFDevelopers.Sample.ExampleViews;
using WPFDevelopers.Samples.ExampleViews;
@@ -28,7 +26,7 @@ public class MainVM : ViewModelBase
{
nameof(MenuEnum.Theme),
nameof(MenuEnum.Menu),
- nameof(MenuEnum.BasicWindow),
+ nameof(MenuEnum.Window),
nameof(MenuEnum.BasicLoading),
nameof(MenuEnum.Button),
nameof(MenuEnum.RadioButton),
@@ -193,8 +191,8 @@ void MenuItemSelection(string _menuName)
case MenuEnum.Menu:
ControlPanel = new MenuExample();
break;
- case MenuEnum.BasicWindow:
- ControlPanel = new WindowBasicExample();
+ case MenuEnum.Window:
+ ControlPanel = new WindowExample();
break;
case MenuEnum.BasicLoading:
ControlPanel = new LoadingBasicExample();
@@ -283,9 +281,6 @@ void MenuItemSelection(string _menuName)
//case MenuEnum.CutImage:
// ControlPanel = new CutImageExample();
// break;
- case MenuEnum.Window:
- ControlPanel = new WindowExample();
- break;
case MenuEnum.CropAvatar:
ControlPanel = new CropAvatarExample();
break;
@@ -551,6 +546,9 @@ void MenuItemSelection(string _menuName)
case MenuEnum.Segmented:
ControlPanel = new SegmentedExample();
break;
+ case MenuEnum.MarkdownViewer:
+ ControlPanel = new MarkdownViewerExample();
+ break;
//新的添加到上方
case MenuEnum.VirtualizingWrapPanel:
ControlPanel = new VirtualizingWrapPanel();
diff --git a/src/WPFDevelopers.Samples.Shared/WPFDevelopers.Samples.Shared.projitems b/src/WPFDevelopers.Samples.Shared/WPFDevelopers.Samples.Shared.projitems
index d1332efa..8b690353 100644
--- a/src/WPFDevelopers.Samples.Shared/WPFDevelopers.Samples.Shared.projitems
+++ b/src/WPFDevelopers.Samples.Shared/WPFDevelopers.Samples.Shared.projitems
@@ -362,6 +362,9 @@
GestureUnlockExample.xaml
+
+ MarkdownViewerExample.xaml
+
SvgViewerExample.xaml
@@ -565,9 +568,6 @@
MenuExample.xaml
-
- WindowBasicExample.xaml
-
LoadingBasicExample.xaml
@@ -1063,6 +1063,10 @@
MSBuild:Compile
Designer
+
+ MSBuild:Compile
+ Designer
+
MSBuild:Compile
Designer
@@ -1279,10 +1283,6 @@
MSBuild:Compile
Designer
-
- MSBuild:Compile
- Designer
-
MSBuild:Compile
Designer
@@ -1374,4 +1374,4 @@
-
\ No newline at end of file
+
diff --git a/src/WPFDevelopers.Samples40/WPFDevelopers.Samples40.csproj b/src/WPFDevelopers.Samples40/WPFDevelopers.Samples40.csproj
index 74253c68..ca47d4d9 100644
--- a/src/WPFDevelopers.Samples40/WPFDevelopers.Samples40.csproj
+++ b/src/WPFDevelopers.Samples40/WPFDevelopers.Samples40.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/WPFDevelopers.Samples45x/WPFDevelopers.Samples45x.csproj b/src/WPFDevelopers.Samples45x/WPFDevelopers.Samples45x.csproj
index 3f998c57..376a1a77 100644
--- a/src/WPFDevelopers.Samples45x/WPFDevelopers.Samples45x.csproj
+++ b/src/WPFDevelopers.Samples45x/WPFDevelopers.Samples45x.csproj
@@ -2,7 +2,7 @@
-
+
diff --git a/src/WPFDevelopers.SamplesCode/WPFDevelopers.SamplesCode.csproj b/src/WPFDevelopers.SamplesCode/WPFDevelopers.SamplesCode.csproj
index 3adcb1a0..7d0aa58a 100644
--- a/src/WPFDevelopers.SamplesCode/WPFDevelopers.SamplesCode.csproj
+++ b/src/WPFDevelopers.SamplesCode/WPFDevelopers.SamplesCode.csproj
@@ -170,6 +170,7 @@
+
@@ -188,7 +189,6 @@
-
@@ -598,6 +598,9 @@
ExampleViews\GestureUnlockExample.xaml
+
+ ExampleViews\MarkdownViewerExample.xaml
+
ExampleViews\AnimationGridExample.xaml
@@ -637,9 +640,6 @@
ExampleViews\Basics\MenuExample.xaml
-
- ExampleViews\Basics\WindowBasicExample.xaml
-
ExampleViews\Basics\LoadingBasicExample.xaml
@@ -701,4 +701,4 @@
ExampleViews\Basics\TabControlExample.xaml
-
\ No newline at end of file
+
diff --git a/src/WPFDevelopers.Shared/Controls/CircleMenu/CircleMenu.cs b/src/WPFDevelopers.Shared/Controls/CircleMenu/CircleMenu.cs
index 3d4cd04f..be09e1aa 100644
--- a/src/WPFDevelopers.Shared/Controls/CircleMenu/CircleMenu.cs
+++ b/src/WPFDevelopers.Shared/Controls/CircleMenu/CircleMenu.cs
@@ -21,6 +21,7 @@ public class CircleMenu : ItemsControl
private const string EllipseGeometryTemplateName = "PART_EllipseGeometry";
private const string ItemsPresenterTemplateName = "PART_ItemsPresenter";
private const string ThumbTemplateName = "PART_Thumb";
+ private const string ThumbEllipseTemplateName = "PART_ThumbEllipse";
public static readonly DependencyProperty SelectedIndexProperty =
DependencyProperty.Register(nameof(SelectedIndex), typeof(int), typeof(CircleMenu),
@@ -58,7 +59,7 @@ public class CircleMenu : ItemsControl
public static readonly DependencyProperty CanDragProperty =
DependencyProperty.Register(nameof(CanDrag), typeof(bool), typeof(CircleMenu),
- new PropertyMetadata(true));
+ new PropertyMetadata(true, OnCanDragChanged));
public static readonly RoutedEvent ItemClickEvent =
@@ -74,6 +75,7 @@ public event RoutedEventHandler ItemClick
private EllipseGeometry _ellipseGeometry;
private ItemsPresenter _itemsPresenter;
private Thumb _thumb;
+ private FrameworkElement _thumbEllipse;
private Storyboard _openStoryboard;
private Storyboard _closeStoryboard;
private TranslateTransform _translateTransform;
@@ -144,22 +146,27 @@ public override void OnApplyTemplate()
{
base.OnApplyTemplate();
+ var oldThumb = _thumb;
+ if (oldThumb != null)
+ DetachThumbDragHandlers(oldThumb);
+
_ellipseGeometry = GetTemplateChild(EllipseGeometryTemplateName) as EllipseGeometry;
_itemsPresenter = GetTemplateChild(ItemsPresenterTemplateName) as ItemsPresenter;
_thumb = GetTemplateChild(ThumbTemplateName) as Thumb;
- if (_thumb != null && CanDrag)
+ if (_thumb != null)
{
- _translateTransform = new TranslateTransform();
- RenderTransform = _translateTransform;
- _thumb.DragStarted -= OnThumb_DragStarted;
- _thumb.DragStarted += OnThumb_DragStarted;
- _thumb.DragDelta -= OnThumb_DragDelta;
- _thumb.DragDelta += OnThumb_DragDelta;
- _thumb.DragCompleted -= OnThumb_DragCompleted;
- _thumb.DragCompleted += OnThumb_DragCompleted;
+ _thumb.ApplyTemplate();
+ _thumbEllipse = _thumb.Template?.FindName(ThumbEllipseTemplateName, _thumb) as FrameworkElement;
}
+ else
+ {
+ _thumbEllipse = null;
+ }
+
+ UpdateDragBehavior();
+ MouseLeftButtonDown -= OnCircleMenu_MouseLeftButtonDown;
MouseLeftButtonDown += OnCircleMenu_MouseLeftButtonDown;
UpdateCenter();
BuildStoryboards();
@@ -333,6 +340,57 @@ private static void OnIsExpandedChanged(DependencyObject d, DependencyPropertyCh
menu._closeStoryboard?.Begin();
}
+ private static void OnCanDragChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var menu = (CircleMenu)d;
+ menu.UpdateDragBehavior();
+ }
+
+ private void UpdateDragBehavior()
+ {
+ EnsureTranslateTransform();
+
+ if (_thumb == null)
+ return;
+
+ DetachThumbDragHandlers(_thumb);
+ if (CanDrag)
+ AttachThumbDragHandlers(_thumb);
+ }
+
+ private void EnsureTranslateTransform()
+ {
+ if (_translateTransform != null)
+ return;
+
+ _translateTransform = RenderTransform as TranslateTransform;
+ if (_translateTransform == null)
+ {
+ _translateTransform = new TranslateTransform();
+ RenderTransform = _translateTransform;
+ }
+ }
+
+ private void AttachThumbDragHandlers(Thumb thumb)
+ {
+ if (thumb == null)
+ return;
+
+ thumb.DragStarted += OnThumb_DragStarted;
+ thumb.DragDelta += OnThumb_DragDelta;
+ thumb.DragCompleted += OnThumb_DragCompleted;
+ }
+
+ private void DetachThumbDragHandlers(Thumb thumb)
+ {
+ if (thumb == null)
+ return;
+
+ thumb.DragStarted -= OnThumb_DragStarted;
+ thumb.DragDelta -= OnThumb_DragDelta;
+ thumb.DragCompleted -= OnThumb_DragCompleted;
+ }
+
private void OnCircleMenu_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
@@ -394,6 +452,7 @@ private void BuildStoryboards()
_openStoryboard = new Storyboard();
_openStoryboard.Children.Add(openRx);
_openStoryboard.Children.Add(openRy);
+ _openStoryboard.Completed += OnOpenStoryboardCompleted;
var closeRx = new DoubleAnimation
{
@@ -418,18 +477,68 @@ private void BuildStoryboards()
_closeStoryboard.Children.Add(closeRy);
}
+ private void OnOpenStoryboardCompleted(object sender, EventArgs e)
+ {
+ if (!IsExpanded)
+ return;
+
+ ClampToParentVisibleBounds();
+ }
+
private void OnThumb_DragStarted(object sender, DragStartedEventArgs e)
{
Cursor = Cursors.SizeAll;
var parent = FindStableParent();
if (parent == null) return;
+
+ var visibleBounds = GetVisibleBoundsInSelf();
var visualPos = TranslatePoint(new Point(0, 0), parent);
var layoutX = visualPos.X - _translateTransform.X;
var layoutY = visualPos.Y - _translateTransform.Y;
- _minX = -layoutX;
- _maxX = parent.ActualWidth - ActualWidth - layoutX;
- _minY = -layoutY;
- _maxY = parent.ActualHeight - ActualHeight - layoutY;
+ _minX = -layoutX - visibleBounds.X;
+ _maxX = parent.ActualWidth - layoutX - (visibleBounds.X + visibleBounds.Width);
+ _minY = -layoutY - visibleBounds.Y;
+ _maxY = parent.ActualHeight - layoutY - (visibleBounds.Y + visibleBounds.Height);
+ }
+
+ private Rect GetVisibleBoundsInSelf()
+ {
+ var bounds = Rect.Empty;
+
+ if (_thumbEllipse != null && _thumbEllipse.ActualWidth > 0 && _thumbEllipse.ActualHeight > 0)
+ {
+ var thumbRect = new Rect(0, 0, _thumbEllipse.ActualWidth, _thumbEllipse.ActualHeight);
+ var thumbBounds = _thumbEllipse.TransformToAncestor(this).TransformBounds(thumbRect);
+ bounds = bounds.IsEmpty ? thumbBounds : Rect.Union(bounds, thumbBounds);
+ }
+
+ if (_itemsPresenter != null && _ellipseGeometry != null)
+ {
+ var radiusX = Math.Max(0, _ellipseGeometry.RadiusX);
+ var radiusY = Math.Max(0, _ellipseGeometry.RadiusY);
+ if (radiusX > 0 && radiusY > 0)
+ {
+ var clipRect = new Rect(
+ _ellipseGeometry.Center.X - radiusX,
+ _ellipseGeometry.Center.Y - radiusY,
+ radiusX * 2,
+ radiusY * 2);
+
+ var presenterRect = new Rect(0, 0, _itemsPresenter.ActualWidth, _itemsPresenter.ActualHeight);
+ var visiblePresenterRect = Rect.Intersect(clipRect, presenterRect);
+
+ if (!visiblePresenterRect.IsEmpty && visiblePresenterRect.Width > 0 && visiblePresenterRect.Height > 0)
+ {
+ var presenterBounds = _itemsPresenter.TransformToAncestor(this).TransformBounds(visiblePresenterRect);
+ bounds = bounds.IsEmpty ? presenterBounds : Rect.Union(bounds, presenterBounds);
+ }
+ }
+ }
+
+ if (bounds.IsEmpty || bounds.Width <= 0 || bounds.Height <= 0)
+ return new Rect(0, 0, ActualWidth, ActualHeight);
+
+ return bounds;
}
private FrameworkElement FindStableParent()
@@ -444,6 +553,42 @@ private FrameworkElement FindStableParent()
return null;
}
+ private void ClampToParentVisibleBounds()
+ {
+ if (_translateTransform == null)
+ return;
+
+ var parent = FindStableParent();
+ if (parent == null)
+ return;
+
+ var visibleBounds = GetVisibleBoundsInSelf();
+ var visibleBoundsInParent = TransformToAncestor(parent).TransformBounds(visibleBounds);
+
+ double offsetX;
+ if (visibleBoundsInParent.Width >= parent.ActualWidth)
+ offsetX = -visibleBoundsInParent.Left;
+ else if (visibleBoundsInParent.Left < 0)
+ offsetX = -visibleBoundsInParent.Left;
+ else if (visibleBoundsInParent.Right > parent.ActualWidth)
+ offsetX = parent.ActualWidth - visibleBoundsInParent.Right;
+ else
+ offsetX = 0;
+
+ double offsetY;
+ if (visibleBoundsInParent.Height >= parent.ActualHeight)
+ offsetY = -visibleBoundsInParent.Top;
+ else if (visibleBoundsInParent.Top < 0)
+ offsetY = -visibleBoundsInParent.Top;
+ else if (visibleBoundsInParent.Bottom > parent.ActualHeight)
+ offsetY = parent.ActualHeight - visibleBoundsInParent.Bottom;
+ else
+ offsetY = 0;
+
+ _translateTransform.X += offsetX;
+ _translateTransform.Y += offsetY;
+ }
+
private void OnThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
_translateTransform.X = Math.Max(_minX, Math.Min(_maxX, _translateTransform.X + e.HorizontalChange));
diff --git a/src/WPFDevelopers.Shared/Controls/CropAvatar/CropAvatar.cs b/src/WPFDevelopers.Shared/Controls/CropAvatar/CropAvatar.cs
index 67d17a0c..a07acf81 100644
--- a/src/WPFDevelopers.Shared/Controls/CropAvatar/CropAvatar.cs
+++ b/src/WPFDevelopers.Shared/Controls/CropAvatar/CropAvatar.cs
@@ -333,6 +333,7 @@ protected virtual void Dispose(bool disposing)
_image.MouseUp -= OnImage_MouseUp;
_image.MouseLeave -= OnImage_MouseLeave;
}
+ _image.UpdateLayout();
if (_replaceButton != null)
{
_replaceButton.Click -= OnReplaceButton_Click;
@@ -353,4 +354,4 @@ protected virtual void Dispose(bool disposing)
Dispose(false);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/WPFDevelopers.Shared/Controls/CropImage/CropImage.cs b/src/WPFDevelopers.Shared/Controls/CropImage/CropImage.cs
index eb42200f..e0c38c9e 100644
--- a/src/WPFDevelopers.Shared/Controls/CropImage/CropImage.cs
+++ b/src/WPFDevelopers.Shared/Controls/CropImage/CropImage.cs
@@ -16,7 +16,7 @@ namespace WPFDevelopers.Controls
[TemplatePart(Name = RectangleRightTemplateName, Type = typeof(Rectangle))]
[TemplatePart(Name = RectangleBottomTemplateName, Type = typeof(Rectangle))]
[TemplatePart(Name = BorderTemplateName, Type = typeof(Border))]
- public class CropImage : Control
+ public class CropImage : Control, IDisposable
{
private const string CanvasTemplateName = "PART_Canvas";
private const string RectangleLeftTemplateName = "PART_RectangleLeft";
@@ -81,6 +81,8 @@ private static bool IsValueValid(double value)
private ScreenCutAdorner _screenCutAdorner;
private bool _isInitialized = false;
private bool _isUnloaded = false;
+ private int _lastRenderTick;
+ private const int RenderThrottleMs = 16;
static CropImage()
{
@@ -206,21 +208,19 @@ private void CleanupResources(bool fullCleanup = false)
brush.ImageSource = null;
_canvas.Background = null;
}
+ _canvas.UpdateLayout();
}
if (CurrentAreaBitmap != null)
{
CurrentAreaBitmap = null;
}
-
_border = null;
_canvas = null;
_isInitialized = false;
_isDragging = false;
}
- GC.Collect();
- GC.WaitForPendingFinalizers();
}
catch (Exception ex)
{
@@ -374,8 +374,24 @@ private void Border_MouseMove(object sender, MouseEventArgs e)
Canvas.SetLeft(draggableControl, x);
Canvas.SetTop(draggableControl, y);
+ RequestRender();
+ }
+ }
+
+ private void RequestRender(bool force = false)
+ {
+ if (force)
+ {
Render();
+ return;
}
+
+ var now = Environment.TickCount;
+ if (unchecked(now - _lastRenderTick) < RenderThrottleMs)
+ return;
+
+ _lastRenderTick = now;
+ Render();
}
private void Render()
@@ -415,7 +431,7 @@ private void UpdateMaskRectangles(double cy, double borderLeft)
private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{
- Render();
+ RequestRender(true);
}
private WriteableBitmap CutBitmap()
@@ -438,13 +454,18 @@ private WriteableBitmap CutBitmap()
if (width <= 0 || height <= 0) return null;
- var croppedBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null);
- var stride = (int)(_bitmapFrame.PixelWidth * (_bitmapFrame.Format.BitsPerPixel / 8.0));
- var pixelData = new byte[stride * _bitmapFrame.PixelHeight];
- _bitmapFrame.CopyPixels(pixelData, stride, 0);
+ var bitsPerPixel = _bitmapFrame.Format.BitsPerPixel;
+ if (bitsPerPixel <= 0) return null;
+
+ var bytesPerPixel = (bitsPerPixel + 7) / 8;
+ var rowStride = width * bytesPerPixel;
+ var pixelData = new byte[rowStride * height];
+
+ _bitmapFrame.CopyPixels(new Int32Rect(left, top, width, height), pixelData, rowStride, 0);
- var sourceOffset = left * (_bitmapFrame.Format.BitsPerPixel / 8) + top * stride;
- croppedBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixelData, stride, sourceOffset);
+ var croppedBitmap = new WriteableBitmap(width, height, _bitmapFrame.DpiX, _bitmapFrame.DpiY,
+ _bitmapFrame.Format, _bitmapFrame.Palette);
+ croppedBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixelData, rowStride, 0);
return croppedBitmap;
}
@@ -454,5 +475,10 @@ private WriteableBitmap CutBitmap()
return null;
}
}
+
+ public void Dispose()
+ {
+ CleanupResources(true);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/WPFDevelopers.Shared/Controls/Gauge/Gauge.cs b/src/WPFDevelopers.Shared/Controls/Gauge/Gauge.cs
index bd6b0d81..ac1543bb 100644
--- a/src/WPFDevelopers.Shared/Controls/Gauge/Gauge.cs
+++ b/src/WPFDevelopers.Shared/Controls/Gauge/Gauge.cs
@@ -8,20 +8,48 @@ namespace WPFDevelopers.Controls
public class Gauge : RangeBase
{
public static readonly DependencyProperty TitleProperty =
- DependencyProperty.Register("TitleProperty", typeof(string), typeof(Gauge), new PropertyMetadata("WD"));
+ DependencyProperty.Register("Title", typeof(string), typeof(Gauge),
+ new PropertyMetadata("WD", OnAppearanceChanged));
public static readonly DependencyProperty ValueFormatProperty =
DependencyProperty.Register("ValueFormat", typeof(string), typeof(Gauge),
- new PropertyMetadata("{0:0}%", OnValueFormatChanged));
+ new PropertyMetadata("{0:0}%", OnAppearanceChanged));
public static readonly DependencyProperty ThicknessProperty =
DependencyProperty.Register("Thickness", typeof(double), typeof(Gauge),
- new PropertyMetadata(10.0, OnValueFormatChanged));
+ new PropertyMetadata(10.0, OnAppearanceChanged));
+ public static readonly DependencyProperty OffsetXProperty =
+ DependencyProperty.Register("OffsetX", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
+
+ public static readonly DependencyProperty OffsetYProperty =
+ DependencyProperty.Register("OffsetY", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
+
+ public static readonly DependencyProperty TitleSpacingProperty =
+ DependencyProperty.Register("TitleSpacing", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
+
+ public static readonly DependencyProperty TickFontSizeProperty =
+ DependencyProperty.Register("TickFontSize", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
+
+ public static readonly DependencyProperty ValueFontSizeProperty =
+ DependencyProperty.Register("ValueFontSize", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
+
+ public static readonly DependencyProperty TitleFontSizeProperty =
+ DependencyProperty.Register("TitleFontSize", typeof(double), typeof(Gauge),
+ new PropertyMetadata(0.0, OnAppearanceChanged));
static Gauge()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Gauge), new FrameworkPropertyMetadata(typeof(Gauge)));
+ BackgroundProperty.OverrideMetadata(typeof(Gauge),
+ new FrameworkPropertyMetadata(null, OnAppearanceChanged));
+ ForegroundProperty.OverrideMetadata(typeof(Gauge),
+ new FrameworkPropertyMetadata(null, OnAppearanceChanged));
}
public Gauge()
@@ -49,7 +77,43 @@ public double Thickness
set => SetValue(ThicknessProperty, value);
}
- private static void OnValueFormatChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ public double OffsetX
+ {
+ get => (double) GetValue(OffsetXProperty);
+ set => SetValue(OffsetXProperty, value);
+ }
+
+ public double OffsetY
+ {
+ get => (double) GetValue(OffsetYProperty);
+ set => SetValue(OffsetYProperty, value);
+ }
+
+ public double TitleSpacing
+ {
+ get => (double) GetValue(TitleSpacingProperty);
+ set => SetValue(TitleSpacingProperty, value);
+ }
+
+ public double TickFontSize
+ {
+ get => (double) GetValue(TickFontSizeProperty);
+ set => SetValue(TickFontSizeProperty, value);
+ }
+
+ public double ValueFontSize
+ {
+ get => (double) GetValue(ValueFontSizeProperty);
+ set => SetValue(ValueFontSizeProperty, value);
+ }
+
+ public double TitleFontSize
+ {
+ get => (double) GetValue(TitleFontSizeProperty);
+ set => SetValue(TitleFontSizeProperty, value);
+ }
+
+ private static void OnAppearanceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var gauge = d as Gauge;
gauge?.InvalidateVisual();
@@ -146,12 +210,13 @@ protected override void OnRender(DrawingContext drawingContext)
var tickStartY = height / 2 + (radius - tickLength) * Math.Sin(angle * Math.PI / 180);
var tickEndX = width / 2 + (radius + Thickness / 2) * Math.Cos(angle * Math.PI / 180);
var tickEndY = height / 2 + (radius + Thickness / 2) * Math.Sin(angle * Math.PI / 180);
- drawingContext.DrawLine(new Pen(Brushes.White, 2), new Point(tickStartX, tickStartY),
+ drawingContext.DrawLine(new Pen(Foreground, 2), new Point(tickStartX, tickStartY),
new Point(tickEndX, tickEndY));
var labelValue = Minimum + step * i;
- var formattedText = DrawingContextHelper.GetFormattedText(labelValue.ToString(), Brushes.White,
- FlowDirection.LeftToRight, FontSize);
+ var tickFontSize = TickFontSize > 0 ? TickFontSize : FontSize;
+ var formattedText = DrawingContextHelper.GetFormattedText(labelValue.ToString(), Foreground,
+ FlowDirection.LeftToRight, tickFontSize);
var labelRadius = radius - tickLength * 2;
var labelX = width / 2 + labelRadius * Math.Cos(angle * Math.PI / 180) - formattedText.Width / 2;
@@ -169,16 +234,18 @@ protected override void OnRender(DrawingContext drawingContext)
throw new InvalidOperationException("Formatting failed ", ex);
}
- var currentValueText = DrawingContextHelper.GetFormattedText(formattedValue, Brushes.White,
- FlowDirection.LeftToRight, FontSize * 2);
- var valueX = width / 2 - currentValueText.Width / 2;
- var valueY = height / 2 + radius * 0.4;
+ var valueFontSize = ValueFontSize > 0 ? ValueFontSize : FontSize * 2;
+ var currentValueText = DrawingContextHelper.GetFormattedText(formattedValue, Foreground,
+ FlowDirection.LeftToRight, valueFontSize);
+ var valueX = width / 2 - currentValueText.Width / 2 + OffsetX;
+ var valueY = height / 2 + radius * 0.4 + OffsetY;
drawingContext.DrawText(currentValueText, new Point(valueX, valueY));
+ var titleFontSize = TitleFontSize > 0 ? TitleFontSize : FontSize;
var titleValue =
- DrawingContextHelper.GetFormattedText(Title, Brushes.White, FlowDirection.LeftToRight, FontSize);
- valueX = width / 2 - titleValue.Width / 2;
- valueY = height / 2 + radius * 0.8;
+ DrawingContextHelper.GetFormattedText(Title, Foreground, FlowDirection.LeftToRight, titleFontSize);
+ valueX = width / 2 - titleValue.Width / 2 + OffsetX;
+ valueY = height / 2 + radius * 0.8 + OffsetY + TitleSpacing;
drawingContext.DrawText(titleValue, new Point(valueX, valueY));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/WPFDevelopers.Shared/Controls/Magnifier/Magnifier.cs b/src/WPFDevelopers.Shared/Controls/Magnifier/Magnifier.cs
index 1d2302bf..34d0d56d 100644
--- a/src/WPFDevelopers.Shared/Controls/Magnifier/Magnifier.cs
+++ b/src/WPFDevelopers.Shared/Controls/Magnifier/Magnifier.cs
@@ -90,7 +90,20 @@ private void Element_MouseLeave(object sender, MouseEventArgs e)
private void Element_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
+ {
element.Unloaded -= Element_Unloaded;
+ element.MouseEnter -= Element_MouseEnter;
+ element.MouseLeave -= Element_MouseLeave;
+ element.MouseMove -= Element_MouseMove;
+ element.MouseWheel -= Element_MouseWheel;
+ }
+ if (_adornerContainer != null)
+ {
+ var layer = AdornerLayer.GetAdornerLayer(ParentTarget);
+ layer?.Remove(_adornerContainer);
+ _adornerContainer.Child = null;
+ _adornerContainer = null;
+ }
}
private void Element_MouseMove(object sender, MouseEventArgs e)
diff --git a/src/WPFDevelopers.Shared/Controls/Magnifier/MagnifierDesktop.cs b/src/WPFDevelopers.Shared/Controls/Magnifier/MagnifierDesktop.cs
index 23204909..4fe11744 100644
--- a/src/WPFDevelopers.Shared/Controls/Magnifier/MagnifierDesktop.cs
+++ b/src/WPFDevelopers.Shared/Controls/Magnifier/MagnifierDesktop.cs
@@ -26,6 +26,16 @@ protected override void OnPreviewMouseRightButtonDown(MouseButtonEventArgs e)
}
public void Dispose()
{
+ if (_border != null)
+ {
+ if (_border.Background is ImageBrush brush)
+ brush.ImageSource = null;
+ _border.Background = null;
+ _border.UpdateLayout();
+ }
+ _border = null;
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
GC.SuppressFinalize(this);
}
}
diff --git a/src/WPFDevelopers.Shared/Controls/Markdown/MarkdownViewer.cs b/src/WPFDevelopers.Shared/Controls/Markdown/MarkdownViewer.cs
new file mode 100644
index 00000000..8763af3a
--- /dev/null
+++ b/src/WPFDevelopers.Shared/Controls/Markdown/MarkdownViewer.cs
@@ -0,0 +1,1579 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+using WPFDevelopers.Helpers;
+
+namespace WPFDevelopers.Controls
+{
+ [TemplatePart(Name = DocumentHostTemplateName, Type = typeof(RichTextBox))]
+ public class MarkdownViewer : Control
+ {
+ private const string DocumentHostTemplateName = "PART_DocumentHost";
+
+ public static readonly DependencyProperty MarkdownProperty =
+ DependencyProperty.Register("Markdown", typeof(string), typeof(MarkdownViewer),
+ new PropertyMetadata(string.Empty, OnMarkdownChanged));
+
+ private RichTextBox _documentHost;
+ private readonly DispatcherTimer _themeProbeTimer;
+ private Color _lastPrimaryColor;
+ private ThemeType _lastThemeType;
+
+ static MarkdownViewer()
+ {
+ DefaultStyleKeyProperty.OverrideMetadata(typeof(MarkdownViewer),
+ new FrameworkPropertyMetadata(typeof(MarkdownViewer)));
+ }
+
+ public MarkdownViewer()
+ {
+ Loaded += OnLoaded;
+ Unloaded += OnUnloaded;
+ SizeChanged += OnSizeChanged;
+
+ _themeProbeTimer = new DispatcherTimer(DispatcherPriority.Background);
+ _themeProbeTimer.Interval = TimeSpan.FromMilliseconds(120);
+ _themeProbeTimer.Tick += ThemeProbeTimer_Tick;
+ }
+
+ public string Markdown
+ {
+ get { return (string)GetValue(MarkdownProperty); }
+ set { SetValue(MarkdownProperty, value); }
+ }
+
+ public override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ _documentHost = GetTemplateChild(DocumentHostTemplateName) as RichTextBox;
+ EnsureDocumentHostConfigured();
+ RenderMarkdown();
+ }
+
+ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
+ {
+ base.OnPropertyChanged(e);
+
+ if (e.Property == FontSizeProperty ||
+ e.Property == FontFamilyProperty ||
+ e.Property == ForegroundProperty ||
+ e.Property == PaddingProperty)
+ {
+ RenderMarkdown();
+ }
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ WPFDevelopers.Resources.ThemeChanged -= Resources_ThemeChanged;
+ WPFDevelopers.Resources.ThemeChanged += Resources_ThemeChanged;
+
+ SyncThemeSnapshot();
+ if (!_themeProbeTimer.IsEnabled)
+ _themeProbeTimer.Start();
+
+ RenderMarkdown();
+ }
+
+ private void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ WPFDevelopers.Resources.ThemeChanged -= Resources_ThemeChanged;
+
+ if (_themeProbeTimer.IsEnabled)
+ _themeProbeTimer.Stop();
+ }
+
+ private void OnSizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (Math.Abs(e.NewSize.Width - e.PreviousSize.Width) > 1)
+ RenderMarkdown();
+ }
+
+ private static void OnMarkdownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as MarkdownViewer;
+ if (control != null)
+ control.RenderMarkdown();
+ }
+
+ private void Resources_ThemeChanged(ThemeType currentTheme)
+ {
+ SyncThemeSnapshot();
+ RenderMarkdown();
+ }
+
+ private void ThemeProbeTimer_Tick(object sender, EventArgs e)
+ {
+ var resources = ThemeManager.Instance.Resources;
+ if (resources == null)
+ return;
+
+ var currentTheme = resources.Theme;
+ var currentColor = ThemeManager.Instance.PrimaryColor;
+
+ if (currentTheme != _lastThemeType || currentColor != _lastPrimaryColor)
+ {
+ _lastThemeType = currentTheme;
+ _lastPrimaryColor = currentColor;
+ RenderMarkdown();
+ }
+ }
+
+ private void SyncThemeSnapshot()
+ {
+ var resources = ThemeManager.Instance.Resources;
+ if (resources == null)
+ return;
+
+ _lastThemeType = resources.Theme;
+ _lastPrimaryColor = ThemeManager.Instance.PrimaryColor;
+ }
+
+ private void EnsureDocumentHostConfigured()
+ {
+ if (_documentHost == null)
+ return;
+
+ _documentHost.IsReadOnly = true;
+ _documentHost.IsDocumentEnabled = true;
+ _documentHost.BorderThickness = new Thickness(0);
+ _documentHost.Background = Brushes.Transparent;
+ _documentHost.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _documentHost.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ _documentHost.Focusable = false;
+ }
+
+ private void RenderMarkdown()
+ {
+ if (_documentHost == null)
+ return;
+
+ EnsureDocumentHostConfigured();
+
+ var document = new FlowDocument();
+ document.PagePadding = new Thickness(0);
+ document.TextAlignment = TextAlignment.Left;
+ document.FontFamily = FontFamily;
+ document.FontSize = FontSize > 0 ? FontSize : 13;
+ document.Foreground = ResolveBrush("WD.PrimaryTextBrush", Foreground);
+
+ var blocks = ParseMarkdown(Markdown ?? string.Empty);
+ foreach (var block in blocks)
+ document.Blocks.Add(CreateBlock(block));
+
+ if (document.Blocks.Count == 0)
+ document.Blocks.Add(new Paragraph());
+
+ _documentHost.Document = document;
+ }
+
+ private Block CreateBlock(MarkdownBlock block)
+ {
+ if (block is HeaderBlock)
+ return CreateHeader((HeaderBlock)block);
+ if (block is ParagraphBlock)
+ return CreateParagraph((ParagraphBlock)block);
+ if (block is HorizontalRuleBlock)
+ return CreateHorizontalRule();
+ if (block is UnorderedListBlock)
+ return CreateUnorderedList((UnorderedListBlock)block);
+ if (block is OrderedListBlock)
+ return CreateOrderedList((OrderedListBlock)block);
+ if (block is QuoteBlock)
+ return CreateQuote((QuoteBlock)block);
+ if (block is CodeBlock)
+ return CreateCodeBlock((CodeBlock)block);
+ if (block is ImageBlock)
+ return CreateImageBlock((ImageBlock)block);
+ if (block is TableBlock)
+ return CreateTableBlock((TableBlock)block);
+
+ return new Paragraph();
+ }
+
+ private Paragraph CreateHeader(HeaderBlock block)
+ {
+ var paragraph = new Paragraph();
+ paragraph.Margin = new Thickness(0, 8, 0, 4);
+ paragraph.FontWeight = FontWeights.SemiBold;
+
+ if (block.Level == 1) paragraph.FontSize = documentFontScale(2.0);
+ else if (block.Level == 2) paragraph.FontSize = documentFontScale(1.65);
+ else if (block.Level == 3) paragraph.FontSize = documentFontScale(1.4);
+ else if (block.Level == 4) paragraph.FontSize = documentFontScale(1.2);
+ else paragraph.FontSize = documentFontScale(1.05);
+
+ AddInlineTokens(paragraph.Inlines, ParseInlines(block.Text));
+ return paragraph;
+ }
+
+ private Paragraph CreateParagraph(ParagraphBlock block)
+ {
+ var paragraph = new Paragraph();
+ paragraph.Margin = new Thickness(0, 2, 0, 6);
+ paragraph.LineHeight = documentFontScale(1.6);
+ AddInlineTokens(paragraph.Inlines, ParseInlines(block.Text));
+ return paragraph;
+ }
+
+ private BlockUIContainer CreateHorizontalRule()
+ {
+ var line = new Border();
+ line.Height = 1;
+ line.Margin = new Thickness(0, 8, 0, 8);
+ line.Background = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ return new BlockUIContainer(line);
+ }
+
+ private List CreateUnorderedList(UnorderedListBlock block)
+ {
+ var list = new List();
+ list.MarkerStyle = TextMarkerStyle.Disc;
+ list.Margin = new Thickness(18, 2, 0, 6);
+ foreach (var text in block.Items)
+ {
+ var paragraph = new Paragraph();
+ paragraph.Margin = new Thickness(0, 0, 0, 2);
+ AddInlineTokens(paragraph.Inlines, ParseInlines(text));
+ list.ListItems.Add(new ListItem(paragraph));
+ }
+ return list;
+ }
+
+ private List CreateOrderedList(OrderedListBlock block)
+ {
+ var list = new List();
+ list.MarkerStyle = TextMarkerStyle.Decimal;
+ list.Margin = new Thickness(18, 2, 0, 6);
+ foreach (var text in block.Items)
+ {
+ var paragraph = new Paragraph();
+ paragraph.Margin = new Thickness(0, 0, 0, 2);
+ AddInlineTokens(paragraph.Inlines, ParseInlines(text));
+ list.ListItems.Add(new ListItem(paragraph));
+ }
+ return list;
+ }
+
+ private BlockUIContainer CreateQuote(QuoteBlock block)
+ {
+ var panel = new DockPanel();
+
+ var quoteLine = new Border();
+ quoteLine.Width = 4;
+ quoteLine.Background = ResolveBrush("WD.PrimaryBrush", Brushes.SteelBlue);
+ quoteLine.Margin = new Thickness(0, 0, 10, 0);
+ DockPanel.SetDock(quoteLine, Dock.Left);
+
+ var body = new TextBlock();
+ body.TextWrapping = TextWrapping.Wrap;
+ body.Foreground = ResolveBrush("WD.RegularTextBrush", Foreground);
+ body.Margin = new Thickness(0);
+ body.FontSize = documentFontScale(1);
+ AddInlineTokens(body.Inlines, ParseInlines(block.Text));
+
+ var container = new Border();
+ container.Background = ResolveQuoteBackground();
+ container.BorderBrush = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ container.BorderThickness = new Thickness(1);
+ container.Padding = new Thickness(10, 8, 10, 8);
+ container.Margin = new Thickness(0, 4, 0, 8);
+ container.CornerRadius = ElementHelper.GetCornerRadius(this);
+
+ panel.Children.Add(quoteLine);
+ panel.Children.Add(body);
+ container.Child = panel;
+
+ return new BlockUIContainer(container);
+ }
+
+ private BlockUIContainer CreateCodeBlock(CodeBlock block)
+ {
+ var card = new Border();
+ card.BorderBrush = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ card.BorderThickness = new Thickness(1);
+ card.Background = ResolveCodeBodyBackground();
+ card.CornerRadius = ElementHelper.GetCornerRadius(this);
+ card.Margin = new Thickness(0, 6, 0, 10);
+
+ var root = new Grid();
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var header = new Border();
+ header.Background = ResolveCodeHeaderBackground();
+ header.Padding = new Thickness(10, 4, 10, 4);
+ header.CornerRadius = new CornerRadius(card.CornerRadius.TopLeft, card.CornerRadius.TopRight, 0, 0);
+
+ var headerDock = new DockPanel();
+ var lang = string.IsNullOrWhiteSpace(block.Language) ? "text" : block.Language.Trim().ToLowerInvariant();
+ var codeText = block.Code ?? string.Empty;
+
+ var copyButton = new Button();
+ copyButton.Content = "Copy";
+ copyButton.Padding = new Thickness(8, 2, 8, 2);
+ copyButton.Margin = new Thickness(0, 0, 8, 0);
+ copyButton.FontSize = documentFontScale(0.75);
+ copyButton.ToolTip = "Copy code";
+ copyButton.Focusable = false;
+ copyButton.IsTabStop = false;
+
+ var copyButtonStyle = TryFindResource("WD.DefaultButton") as Style;
+ if (copyButtonStyle != null)
+ copyButton.Style = copyButtonStyle;
+ copyButton.Click += (sender, args) =>
+ {
+ try
+ {
+ Clipboard.SetText(codeText);
+ copyButton.Content = "Copied";
+ copyButton.IsEnabled = false;
+
+ var resetTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(1200)
+ };
+ resetTimer.Tick += (s, e) =>
+ {
+ resetTimer.Stop();
+ copyButton.Content = "Copy";
+ copyButton.IsEnabled = true;
+ };
+ resetTimer.Start();
+ }
+ catch
+ {
+ copyButton.Content = "Copy failed";
+ copyButton.IsEnabled = false;
+
+ var resetTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(1200)
+ };
+ resetTimer.Tick += (s, e) =>
+ {
+ resetTimer.Stop();
+ copyButton.Content = "Copy";
+ copyButton.IsEnabled = true;
+ };
+ resetTimer.Start();
+ }
+ };
+
+ var langBadge = new Border();
+ langBadge.CornerRadius = ElementHelper.GetCornerRadius(this);
+ langBadge.BorderBrush = ResolveBrush("WD.PrimaryBrush", Brushes.RoyalBlue);
+ langBadge.BorderThickness = new Thickness(1);
+ langBadge.Padding = new Thickness(8, 0, 8, 0);
+ langBadge.VerticalAlignment = VerticalAlignment.Center;
+
+ var langText = new TextBlock();
+ langText.Text = GetCodeLanguageLabel(lang);
+ langText.Foreground = ResolveBrush("WD.PrimaryBrush", Brushes.RoyalBlue);
+ langText.FontSize = documentFontScale(0.75);
+ langText.FontWeight = FontWeights.SemiBold;
+ langBadge.Child = langText;
+
+ var headerActions = new StackPanel();
+ headerActions.Orientation = Orientation.Horizontal;
+ headerActions.HorizontalAlignment = HorizontalAlignment.Right;
+ headerActions.Children.Add(copyButton);
+ headerActions.Children.Add(langBadge);
+ headerDock.Children.Add(headerActions);
+ header.Child = headerDock;
+ Grid.SetRow(header, 0);
+
+ var lines = (block.Code ?? string.Empty).Replace("\r", string.Empty).Split('\n');
+ if (lines.Length > 0 && lines[lines.Length - 1] == string.Empty)
+ lines = lines.Take(lines.Length - 1).ToArray();
+ if (lines.Length == 0)
+ lines = new[] { string.Empty };
+
+ var contentGrid = new Grid();
+ contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ contentGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ var numberBorder = new Border();
+ numberBorder.Background = ResolveCodeGutterBackground();
+ numberBorder.Padding = new Thickness(10, 8, 10, 8);
+ numberBorder.CornerRadius = new CornerRadius(0, 0, 0, card.CornerRadius.BottomLeft);
+
+ var lineNumbers = new TextBlock();
+ lineNumbers.FontFamily = new FontFamily("Consolas");
+ lineNumbers.Foreground = ResolveCodeLineNumberBrush();
+ lineNumbers.FontSize = documentFontScale(0.85);
+ lineNumbers.Text = string.Join("\n", Enumerable.Range(1, lines.Length).Select(i => i.ToString(CultureInfo.InvariantCulture)));
+ numberBorder.Child = lineNumbers;
+ Grid.SetColumn(numberBorder, 0);
+
+ var codeBorder = new Border();
+ codeBorder.Background = ResolveCodeBodyBackground();
+ codeBorder.Padding = new Thickness(10, 8, 10, 8);
+ codeBorder.CornerRadius = new CornerRadius(0, 0, card.CornerRadius.BottomRight, 0);
+ codeBorder.Child = BuildHighlightedCodeBody(lines, lang);
+ Grid.SetColumn(codeBorder, 1);
+
+ contentGrid.Children.Add(numberBorder);
+ contentGrid.Children.Add(codeBorder);
+
+ var contentScroll = new ScrollViewer();
+ contentScroll.Background = Brushes.Transparent;
+ contentScroll.BorderThickness = new Thickness(0);
+ contentScroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ contentScroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ contentScroll.Focusable = false;
+ contentScroll.IsTabStop = false;
+ contentScroll.PreviewMouseWheel += ForwardMouseWheelToParent;
+ contentScroll.Content = contentGrid;
+ Grid.SetRow(contentScroll, 1);
+
+ root.Children.Add(header);
+ root.Children.Add(contentScroll);
+ card.Child = root;
+
+ return new BlockUIContainer(card);
+ }
+
+ private Block CreateImageBlock(ImageBlock block)
+ {
+ try
+ {
+ var uri = BuildImageUri(block.Url);
+ if (uri == null)
+ return CreateParagraph(new ParagraphBlock { Text = string.IsNullOrWhiteSpace(block.AltText) ? block.Url : block.AltText });
+
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.UriSource = uri;
+ bitmap.CacheOption = BitmapCacheOption.OnLoad;
+ bitmap.EndInit();
+ bitmap.Freeze();
+
+ var image = new Image();
+ image.Source = bitmap;
+ image.Stretch = Stretch.Uniform;
+ image.MaxWidth = Math.Max(120, ActualWidth - 80);
+ image.Margin = new Thickness(0, 4, 0, 2);
+
+ var stack = new StackPanel();
+ stack.Children.Add(image);
+ if (!string.IsNullOrWhiteSpace(block.AltText))
+ {
+ var caption = new TextBlock();
+ caption.Text = block.AltText;
+ caption.Margin = new Thickness(0, 4, 0, 0);
+ caption.Foreground = ResolveBrush("WD.RegularTextBrush", Foreground);
+ caption.FontSize = documentFontScale(0.85);
+ stack.Children.Add(caption);
+ }
+
+ var container = new Border();
+ container.BorderBrush = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ container.BorderThickness = new Thickness(1);
+ container.CornerRadius = ElementHelper.GetCornerRadius(this);
+ container.Padding = new Thickness(6);
+ container.Margin = new Thickness(0, 4, 0, 8);
+ container.Child = stack;
+
+ return new BlockUIContainer(container);
+ }
+ catch
+ {
+ return CreateParagraph(new ParagraphBlock { Text = string.IsNullOrWhiteSpace(block.AltText) ? block.Url : block.AltText });
+ }
+ }
+
+ private BlockUIContainer CreateTableBlock(TableBlock block)
+ {
+ var grid = new Grid();
+ grid.SnapsToDevicePixels = true;
+ grid.ShowGridLines = false;
+
+ var rowCount = 1 + block.Rows.Count;
+ var colCount = block.Headers.Count;
+ if (colCount == 0)
+ return new BlockUIContainer(new Border());
+
+ for (var i = 0; i < colCount; i++)
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ for (var i = 0; i < rowCount; i++)
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ for (var col = 0; col < colCount; col++)
+ {
+ var header = CreateTableCell(block.Headers[col], true, TableAlignment.Left, 0, col, rowCount, colCount);
+ Grid.SetRow(header, 0);
+ Grid.SetColumn(header, col);
+ grid.Children.Add(header);
+ }
+
+ for (var row = 0; row < block.Rows.Count; row++)
+ {
+ var rowCells = block.Rows[row];
+ for (var col = 0; col < colCount; col++)
+ {
+ var text = col < rowCells.Count ? rowCells[col] : string.Empty;
+ var align = col < block.Alignments.Count ? block.Alignments[col] : TableAlignment.Left;
+ var cell = CreateTableCell(text, false, align, row + 1, col, rowCount, colCount);
+ Grid.SetRow(cell, row + 1);
+ Grid.SetColumn(cell, col);
+ grid.Children.Add(cell);
+ }
+ }
+
+ var border = new Border();
+ border.Margin = new Thickness(0, 6, 0, 10);
+ border.BorderBrush = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ border.BorderThickness = new Thickness(1);
+ border.CornerRadius = ElementHelper.GetCornerRadius(this);
+
+ var scroll = new ScrollViewer();
+ scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
+ scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
+ scroll.Focusable = false;
+ scroll.IsTabStop = false;
+ scroll.PreviewMouseWheel += ForwardMouseWheelToParent;
+ scroll.Content = grid;
+
+ border.Child = scroll;
+ return new BlockUIContainer(border);
+ }
+
+ private Border CreateTableCell(string text, bool isHeader, TableAlignment align, int rowIndex, int colIndex, int rowCount, int colCount)
+ {
+ var border = new Border();
+ border.BorderBrush = ResolveBrush("WD.BaseBrush", Brushes.LightGray);
+ border.BorderThickness = new Thickness(0.5);
+ border.Padding = new Thickness(8, 6, 8, 6);
+ border.Background = isHeader ? ResolveCodeHeaderBackground() : ResolveBrush("WD.BackgroundBrush", Brushes.White);
+
+ var corner = ElementHelper.GetCornerRadius(this);
+ border.CornerRadius = new CornerRadius(
+ rowIndex == 0 && colIndex == 0 ? corner.TopLeft : 0,
+ rowIndex == 0 && colIndex == colCount - 1 ? corner.TopRight : 0,
+ rowIndex == rowCount - 1 && colIndex == colCount - 1 ? corner.BottomRight : 0,
+ rowIndex == rowCount - 1 && colIndex == 0 ? corner.BottomLeft : 0);
+
+ var tb = new TextBlock();
+ tb.TextWrapping = TextWrapping.Wrap;
+ tb.FontSize = documentFontScale(0.9);
+ tb.Foreground = ResolveBrush("WD.PrimaryTextBrush", Foreground);
+ tb.FontWeight = isHeader ? FontWeights.SemiBold : FontWeights.Normal;
+ if (align == TableAlignment.Center) tb.TextAlignment = TextAlignment.Center;
+ else if (align == TableAlignment.Right) tb.TextAlignment = TextAlignment.Right;
+ else tb.TextAlignment = TextAlignment.Left;
+ AddInlineTokens(tb.Inlines, ParseInlines(text));
+ border.Child = tb;
+ return border;
+ }
+
+ private void ForwardMouseWheelToParent(object sender, MouseWheelEventArgs e)
+ {
+ if (e.Handled)
+ return;
+
+ var target = FindVerticalScrollAncestor(sender as DependencyObject);
+ if (target == null)
+ return;
+
+ e.Handled = true;
+ var forwardedArgs = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
+ {
+ RoutedEvent = UIElement.MouseWheelEvent,
+ Source = sender
+ };
+ target.RaiseEvent(forwardedArgs);
+ }
+
+ private static ScrollViewer FindVerticalScrollAncestor(DependencyObject start)
+ {
+ var current = GetParentObject(start);
+ while (current != null)
+ {
+ var scrollViewer = current as ScrollViewer;
+ if (scrollViewer != null && scrollViewer.VerticalScrollBarVisibility != ScrollBarVisibility.Disabled)
+ return scrollViewer;
+
+ current = GetParentObject(current);
+ }
+
+ return null;
+ }
+
+ private static DependencyObject GetParentObject(DependencyObject obj)
+ {
+ if (obj == null)
+ return null;
+
+ DependencyObject parent = null;
+ try
+ {
+ parent = VisualTreeHelper.GetParent(obj);
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ if (parent != null)
+ return parent;
+
+ return LogicalTreeHelper.GetParent(obj);
+ }
+
+ private Uri BuildImageUri(string url)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ return null;
+
+ Uri absolute;
+ if (Uri.TryCreate(url, UriKind.Absolute, out absolute))
+ return absolute;
+
+ if (File.Exists(url))
+ return new Uri(Path.GetFullPath(url), UriKind.Absolute);
+
+ var appBase = AppDomain.CurrentDomain.BaseDirectory;
+ var localPath = Path.Combine(appBase, url.Replace('/', Path.DirectorySeparatorChar));
+ if (File.Exists(localPath))
+ return new Uri(localPath, UriKind.Absolute);
+
+ var samplesLocalPath = Path.Combine(appBase, "Resources", url.Replace('/', Path.DirectorySeparatorChar));
+ if (File.Exists(samplesLocalPath))
+ return new Uri(samplesLocalPath, UriKind.Absolute);
+
+ if (Uri.TryCreate("pack://application:,,,/" + url.TrimStart('/'), UriKind.Absolute, out absolute))
+ return absolute;
+
+ if (Uri.TryCreate("pack://application:,,,/WPFDevelopers.Samples;component/" + url.TrimStart('/'), UriKind.Absolute, out absolute))
+ return absolute;
+
+ return null;
+ }
+
+ private static string ExtractUrlFromLinkTarget(string rawTarget)
+ {
+ if (string.IsNullOrWhiteSpace(rawTarget))
+ return string.Empty;
+
+ var target = rawTarget.Trim();
+ var quoteIndex = target.IndexOf('"');
+ if (quoteIndex > 0)
+ return target.Substring(0, quoteIndex).Trim();
+
+ quoteIndex = target.IndexOf('\'');
+ if (quoteIndex > 0)
+ return target.Substring(0, quoteIndex).Trim();
+
+ return target;
+ }
+
+ private void AddInlineTokens(InlineCollection inlines, List tokens)
+ {
+ foreach (var token in tokens)
+ {
+ if (token.Kind == InlineTokenKind.Text)
+ {
+ inlines.Add(new Run(token.Text));
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.Bold)
+ {
+ inlines.Add(new Bold(new Run(token.Text)));
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.Italic)
+ {
+ inlines.Add(new Italic(new Run(token.Text)));
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.BoldItalic)
+ {
+ inlines.Add(new Bold(new Italic(new Run(token.Text))));
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.StrikeThrough)
+ {
+ var run = new Run(token.Text);
+ run.TextDecorations = TextDecorations.Strikethrough;
+ run.Foreground = ResolveMutedTextBrush();
+ inlines.Add(run);
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.Code)
+ {
+ var run = new Run(token.Text);
+ run.FontFamily = new FontFamily("Consolas");
+ run.Background = ResolveCodeInlineBackground();
+ run.Foreground = ResolveBrush("WD.PrimaryBrush", Brushes.RoyalBlue);
+ inlines.Add(run);
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.Link)
+ {
+ var hyperlink = new Hyperlink(new Run(token.Text));
+ hyperlink.Foreground = ResolveBrush("WD.PrimaryBrush", Brushes.RoyalBlue);
+ hyperlink.TextDecorations = TextDecorations.Underline;
+
+ var linkTarget = ExtractUrlFromLinkTarget(token.Url);
+ Uri linkUri;
+ if (Uri.TryCreate(linkTarget, UriKind.Absolute, out linkUri))
+ hyperlink.NavigateUri = linkUri;
+
+ hyperlink.Click += Hyperlink_Click;
+ inlines.Add(hyperlink);
+ continue;
+ }
+
+ if (token.Kind == InlineTokenKind.Image)
+ {
+ var imageUri = BuildImageUri(token.Url);
+ if (imageUri != null)
+ {
+ try
+ {
+ var bitmap = new BitmapImage();
+ bitmap.BeginInit();
+ bitmap.UriSource = imageUri;
+ bitmap.CacheOption = BitmapCacheOption.OnLoad;
+ bitmap.EndInit();
+
+ var image = new Image();
+ image.Source = bitmap;
+ image.Stretch = Stretch.Uniform;
+ image.MaxHeight = 120;
+ image.MaxWidth = 240;
+
+ inlines.Add(new InlineUIContainer(image));
+ continue;
+ }
+ catch
+ {
+ }
+ }
+
+ inlines.Add(new Run(string.IsNullOrWhiteSpace(token.Text) ? "[image]" : token.Text));
+ }
+ }
+ }
+
+ private void Hyperlink_Click(object sender, RoutedEventArgs e)
+ {
+ var hyperlink = sender as Hyperlink;
+ if (hyperlink == null)
+ return;
+
+ var uri = hyperlink.NavigateUri;
+ if (uri == null)
+ return;
+
+ try
+ {
+ Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true });
+ }
+ catch
+ {
+ try { Process.Start(uri.AbsoluteUri); }
+ catch { }
+ }
+ }
+
+ private Brush ResolveBrush(string key, Brush fallback)
+ {
+ Brush brush = null;
+ if (ThemeManager.Instance.Resources != null)
+ brush = ThemeManager.Instance.Resources.TryFindResource(key);
+
+ if (brush != null)
+ return brush;
+
+ return fallback ?? Brushes.Black;
+ }
+
+ private bool IsDarkTheme()
+ {
+ if (ThemeManager.Instance.Resources == null)
+ return false;
+
+ return ThemeManager.Instance.Resources.Theme == ThemeType.Dark;
+ }
+
+ private Brush ResolveQuoteBackground()
+ {
+ var primary = ThemeManager.Instance.PrimaryColor;
+ return new SolidColorBrush(Color.FromArgb(30, primary.R, primary.G, primary.B));
+ }
+
+ private Brush ResolveCodeHeaderBackground()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(34, 36, 40))
+ : new SolidColorBrush(Color.FromRgb(246, 248, 252));
+ }
+
+ private Brush ResolveCodeBodyBackground()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(42, 42, 42))
+ : new SolidColorBrush(Color.FromRgb(249, 250, 252));
+ }
+
+ private Brush ResolveCodeGutterBackground()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(36, 38, 42))
+ : new SolidColorBrush(Color.FromRgb(250, 251, 254));
+ }
+
+ private Brush ResolveCodeLineNumberBrush()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(128, 136, 150))
+ : new SolidColorBrush(Color.FromRgb(150, 156, 166));
+ }
+
+ private Brush ResolveCodeTextBrush()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(232, 236, 242))
+ : new SolidColorBrush(Color.FromRgb(36, 40, 48));
+ }
+
+ private Brush ResolveMutedTextBrush()
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(170, 176, 186))
+ : new SolidColorBrush(Color.FromRgb(118, 124, 136));
+ }
+
+ private Brush ResolveCodeInlineBackground()
+ {
+ return ResolveQuoteBackground();
+ }
+
+ private FrameworkElement BuildHighlightedCodeBody(string[] lines, string lang)
+ {
+ var normalizedLang = NormalizeCodeLanguage(lang);
+ if (normalizedLang == "text")
+ normalizedLang = DetectCodeLanguage(lines);
+
+ var panel = new StackPanel();
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var lineText = new TextBlock();
+ lineText.TextWrapping = TextWrapping.NoWrap;
+ lineText.FontFamily = new FontFamily("Consolas");
+ lineText.FontSize = documentFontScale(0.95);
+ lineText.Foreground = ResolveCodeTextBrush();
+ AppendHighlightedCodeRuns(lineText.Inlines, lines[i], normalizedLang);
+ panel.Children.Add(lineText);
+ }
+
+ return panel;
+ }
+
+ private void AppendHighlightedCodeRuns(InlineCollection inlines, string line, string lang)
+ {
+ var normalizedLang = NormalizeCodeLanguage(lang);
+ var tokens = TokenizeCodeLine(line, normalizedLang);
+ for (var i = 0; i < tokens.Count; i++)
+ {
+ var token = tokens[i];
+ var run = new Run(token);
+ run.Foreground = ClassifyCodeToken(token, normalizedLang, tokens, i);
+ inlines.Add(run);
+ }
+ }
+
+ private static List TokenizeCodeLine(string line, string lang)
+ {
+ if (IsMarkupLanguage(lang))
+ {
+ var result = new List();
+ var matches = Regex.Matches(line ?? string.Empty, "(\"[^\"]*\"|'[^']*'||/>|[<>=/]|[A-Za-z_][A-Za-z0-9_:\\.-]*|\\s+|.)");
+ foreach (Match match in matches)
+ result.Add(match.Value);
+ return result;
+ }
+
+ return Regex.Split(line, "(\\s+|[;.,(){}\\[\\]=+\\-*/<>!&|:%?])")
+ .Where(s => !string.IsNullOrEmpty(s))
+ .ToList();
+ }
+
+ private Brush ClassifyCodeToken(string token, string lang, List tokens, int index)
+ {
+ var langLower = NormalizeCodeLanguage(lang);
+
+ if (IsMarkupLanguage(langLower))
+ return ClassifyMarkupToken(token, tokens, index);
+
+ var keywords = new HashSet(GetLanguageKeywords(langLower), StringComparer.OrdinalIgnoreCase);
+
+ if (string.IsNullOrWhiteSpace(token))
+ return ResolveCodeTextBrush();
+
+ if (Regex.IsMatch(token, "^//.*") || Regex.IsMatch(token, "^#.*") || token.StartsWith("/*", StringComparison.Ordinal) || token.StartsWith("*", StringComparison.Ordinal) || token.EndsWith("*/", StringComparison.Ordinal))
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(122, 132, 146))
+ : new SolidColorBrush(Color.FromRgb(121, 132, 145));
+ }
+
+ if (keywords.Contains(token) || keywords.Contains(token.TrimEnd('(')))
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(127, 194, 255))
+ : new SolidColorBrush(Color.FromRgb(0, 92, 197));
+ }
+
+ if ((token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal)) ||
+ (token.StartsWith("'", StringComparison.Ordinal) && token.EndsWith("'", StringComparison.Ordinal)))
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(165, 214, 125))
+ : new SolidColorBrush(Color.FromRgb(46, 125, 50));
+ }
+
+ if (Regex.IsMatch(token, "^\\d+(\\.\\d+)?$"))
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(245, 171, 116))
+ : new SolidColorBrush(Color.FromRgb(198, 91, 23));
+ }
+
+ if (token == "{" || token == "}" || token == "(" || token == ")" || token == "[" || token == "]")
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(255, 202, 124))
+ : new SolidColorBrush(Color.FromRgb(193, 124, 0));
+ }
+
+ return ResolveCodeTextBrush();
+ }
+
+ private Brush ClassifyMarkupToken(string token, List tokens, int index)
+ {
+ if (string.IsNullOrWhiteSpace(token))
+ return ResolveCodeTextBrush();
+
+ if ((token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal)) ||
+ (token.StartsWith("'", StringComparison.Ordinal) && token.EndsWith("'", StringComparison.Ordinal)))
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(165, 214, 125))
+ : new SolidColorBrush(Color.FromRgb(46, 125, 50));
+ }
+
+ if (token == "<" || token == ">" || token == "" || token == "/>" || token == "=" || token == "/")
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(255, 202, 124))
+ : new SolidColorBrush(Color.FromRgb(193, 124, 0));
+ }
+
+ var prev = GetPreviousSignificantToken(tokens, index);
+ var next = GetNextSignificantToken(tokens, index);
+ if (prev == "<" || prev == "")
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(127, 194, 255))
+ : new SolidColorBrush(Color.FromRgb(0, 92, 197));
+ }
+
+ if (next == "=")
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(245, 171, 116))
+ : new SolidColorBrush(Color.FromRgb(198, 91, 23));
+ }
+
+ if (token.IndexOf(':') >= 0)
+ {
+ return IsDarkTheme()
+ ? new SolidColorBrush(Color.FromRgb(166, 214, 255))
+ : new SolidColorBrush(Color.FromRgb(21, 101, 192));
+ }
+
+ return ResolveCodeTextBrush();
+ }
+
+ private static string GetPreviousSignificantToken(List tokens, int index)
+ {
+ for (var i = index - 1; i >= 0; i--)
+ {
+ if (!string.IsNullOrWhiteSpace(tokens[i]))
+ return tokens[i];
+ }
+
+ return string.Empty;
+ }
+
+ private static string GetNextSignificantToken(List tokens, int index)
+ {
+ for (var i = index + 1; i < tokens.Count; i++)
+ {
+ if (!string.IsNullOrWhiteSpace(tokens[i]))
+ return tokens[i];
+ }
+
+ return string.Empty;
+ }
+
+ private static bool IsMarkupLanguage(string lang)
+ {
+ return lang == "xaml" || lang == "xml" || lang == "html";
+ }
+
+ private static string[] GetLanguageKeywords(string langLower)
+ {
+ if (langLower == "csharp")
+ {
+ return new[]
+ {
+ "var", "int", "string", "double", "float", "bool", "void", "class", "public", "private", "protected", "internal", "static", "return",
+ "if", "else", "for", "foreach", "while", "do", "switch", "case", "break", "continue", "new", "null", "true", "false", "this", "base",
+ "override", "virtual", "abstract", "sealed", "using", "namespace", "try", "catch", "finally", "throw", "async", "await", "typeof", "is", "as",
+ "in", "from", "where", "select", "let", "group", "by", "into", "orderby", "join", "on", "equals", "Console", "List", "Dictionary", "Task",
+ "IEnumerable", "IAsyncEnumerable"
+ };
+ }
+
+ if (langLower == "javascript" || langLower == "typescript")
+ {
+ return new[]
+ {
+ "const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "extends", "import", "export", "default", "from",
+ "new", "this", "null", "undefined", "true", "false", "async", "await", "try", "catch", "throw", "typeof", "instanceof", "console", "Promise",
+ "Map", "Set", "Array", "type", "interface", "enum", "implements", "readonly", "any", "unknown", "never", "keyof", "infer"
+ };
+ }
+
+ if (langLower == "python")
+ {
+ return new[]
+ {
+ "def", "class", "import", "from", "as", "return", "if", "elif", "else", "for", "while", "try", "except", "finally", "raise", "with", "yield",
+ "lambda", "pass", "break", "continue", "and", "or", "not", "is", "in", "None", "True", "False", "self", "print", "range", "len", "str", "int",
+ "float", "list", "dict", "set", "tuple"
+ };
+ }
+
+ if (langLower == "xml" || langLower == "html")
+ {
+ return new[] { "xml", "html", "head", "body", "div", "span", "p", "a", "img", "script", "style", "link", "meta", "title" };
+ }
+
+ if (langLower == "css")
+ {
+ return new[] { "important" };
+ }
+
+ if (langLower == "sql")
+ {
+ return new[]
+ {
+ "SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "TABLE", "INTO", "VALUES", "SET", "JOIN", "LEFT", "RIGHT",
+ "INNER", "OUTER", "ON", "GROUP", "BY", "ORDER", "HAVING", "LIMIT", "AS", "AND", "OR", "NOT", "NULL", "IS", "IN", "EXISTS", "BETWEEN", "LIKE",
+ "DISTINCT", "COUNT", "SUM", "AVG", "MAX", "MIN"
+ };
+ }
+
+ if (langLower == "bash")
+ {
+ return new[]
+ {
+ "if", "then", "else", "fi", "for", "while", "do", "done", "case", "esac", "function", "return", "exit", "echo", "export", "source", "cd", "pwd",
+ "ls", "grep", "sed", "awk", "cat", "mkdir", "rm", "cp", "mv", "chmod", "chown", "sudo", "apt", "yum", "brew"
+ };
+ }
+
+ return new string[0];
+ }
+
+ private static string NormalizeCodeLanguage(string lang)
+ {
+ var value = (lang ?? string.Empty).Trim().ToLowerInvariant();
+ if (value == "c#" || value == "cs") return "csharp";
+ if (value == "js") return "javascript";
+ if (value == "ts") return "typescript";
+ if (value == "py") return "python";
+ if (value == "xaml") return "xaml";
+ if (value == "sh" || value == "shell" || value == "zsh") return "bash";
+ return string.IsNullOrWhiteSpace(value) ? "text" : value;
+ }
+
+ private static string GetCodeLanguageLabel(string lang)
+ {
+ switch (NormalizeCodeLanguage(lang))
+ {
+ case "csharp":
+ return "C#";
+ case "javascript":
+ return "JS";
+ case "typescript":
+ return "TS";
+ case "python":
+ return "Py";
+ case "xaml":
+ return "XAML";
+ case "xml":
+ return "XML";
+ case "html":
+ return "HTML";
+ case "bash":
+ return "Bash";
+ case "sql":
+ return "SQL";
+ default:
+ return "Text";
+ }
+ }
+
+ private static string DetectCodeLanguage(string[] lines)
+ {
+ var text = string.Join("\n", lines ?? new string[0]);
+
+ if (Regex.IsMatch(text, "\\b(namespace|using|public|private|protected|internal|class|static|Console)\\b"))
+ return "csharp";
+ if (Regex.IsMatch(text, "<\\s*[A-Za-z_][A-Za-z0-9_:\\.-]*") || Regex.IsMatch(text, "\\bxmlns(:[A-Za-z_][A-Za-z0-9_]*)?=", RegexOptions.IgnoreCase))
+ return "xaml";
+ if (Regex.IsMatch(text, "\\b(def|import|from|lambda|yield|None|True|False)\\b"))
+ return "python";
+ if (Regex.IsMatch(text, "\\b(const|let|function|=>|console\\.log|import|export)\\b"))
+ return "javascript";
+ if (Regex.IsMatch(text, "\\b(type|interface|implements|readonly|keyof|infer)\\b"))
+ return "typescript";
+
+ return "text";
+ }
+
+ private double documentFontScale(double factor)
+ {
+ var baseSize = FontSize > 0 ? FontSize : 13;
+ return Math.Round(baseSize * factor, 2, MidpointRounding.AwayFromZero);
+ }
+
+ private static List ParseMarkdown(string text)
+ {
+ var result = new List();
+ if (string.IsNullOrWhiteSpace(text))
+ return result;
+
+ var lines = text.Replace("\r", string.Empty).Split('\n');
+ var paragraphBuilder = new StringBuilder();
+ var inCodeBlock = false;
+ var codeBuilder = new StringBuilder();
+ var codeLang = string.Empty;
+ var listBuffer = new List();
+ var orderedListBuffer = new List();
+
+ Action flushParagraph = delegate
+ {
+ if (paragraphBuilder.Length == 0)
+ return;
+
+ var content = paragraphBuilder.ToString().Trim();
+ if (content.Length > 0)
+ result.Add(new ParagraphBlock { Text = content });
+
+ paragraphBuilder.Clear();
+ };
+
+ Action flushLists = delegate
+ {
+ if (listBuffer.Count > 0)
+ {
+ var list = new UnorderedListBlock();
+ list.Items.AddRange(listBuffer);
+ result.Add(list);
+ listBuffer.Clear();
+ }
+
+ if (orderedListBuffer.Count > 0)
+ {
+ var list = new OrderedListBlock();
+ list.Items.AddRange(orderedListBuffer);
+ result.Add(list);
+ orderedListBuffer.Clear();
+ }
+ };
+
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var line = lines[i];
+ var trimmed = line.Trim();
+
+ if (inCodeBlock)
+ {
+ if (trimmed.StartsWith("```", StringComparison.Ordinal))
+ {
+ result.Add(new CodeBlock { Code = codeBuilder.ToString().TrimEnd('\n'), Language = codeLang });
+ codeBuilder.Clear();
+ codeLang = string.Empty;
+ inCodeBlock = false;
+ }
+ else
+ {
+ codeBuilder.AppendLine(line);
+ }
+ continue;
+ }
+
+ if (trimmed.StartsWith("```", StringComparison.Ordinal))
+ {
+ flushParagraph();
+ flushLists();
+ inCodeBlock = true;
+ codeLang = trimmed.Length > 3 ? trimmed.Substring(3).Trim() : string.Empty;
+ continue;
+ }
+
+ if (trimmed.StartsWith("|") && IsTableSeparator(lines, i + 1))
+ {
+ flushParagraph();
+ flushLists();
+ result.Add(ParseTable(lines, ref i));
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(trimmed))
+ {
+ flushParagraph();
+ flushLists();
+ continue;
+ }
+
+ if (Regex.IsMatch(trimmed, "^(-{3,}|\\*{3,}|_{3,})$"))
+ {
+ flushParagraph();
+ flushLists();
+ result.Add(new HorizontalRuleBlock());
+ continue;
+ }
+
+ var headerMatch = Regex.Match(line, "^(#{1,6})\\s+(.*)");
+ if (headerMatch.Success)
+ {
+ flushParagraph();
+ flushLists();
+ result.Add(new HeaderBlock
+ {
+ Level = headerMatch.Groups[1].Value.Length,
+ Text = headerMatch.Groups[2].Value.Trim()
+ });
+ continue;
+ }
+
+ if (trimmed.StartsWith(">", StringComparison.Ordinal))
+ {
+ flushParagraph();
+ flushLists();
+
+ var quoteBuilder = new StringBuilder();
+ while (i < lines.Length)
+ {
+ var currentLine = lines[i];
+ var currentTrimmed = currentLine.TrimStart();
+ if (!currentTrimmed.StartsWith(">", StringComparison.Ordinal))
+ break;
+
+ var content = currentTrimmed.Substring(1);
+ if (content.StartsWith(" ", StringComparison.Ordinal))
+ content = content.Substring(1);
+
+ if (quoteBuilder.Length > 0)
+ quoteBuilder.AppendLine();
+ quoteBuilder.Append(content.TrimEnd());
+ i++;
+ }
+
+ i--;
+ result.Add(new QuoteBlock { Text = quoteBuilder.ToString() });
+ continue;
+ }
+
+ if (trimmed.StartsWith("- ", StringComparison.Ordinal) ||
+ trimmed.StartsWith("* ", StringComparison.Ordinal) ||
+ trimmed.StartsWith("+ ", StringComparison.Ordinal))
+ {
+ flushParagraph();
+ if (orderedListBuffer.Count > 0)
+ {
+ var ordered = new OrderedListBlock();
+ ordered.Items.AddRange(orderedListBuffer);
+ result.Add(ordered);
+ orderedListBuffer.Clear();
+ }
+ listBuffer.Add(trimmed.Substring(2).Trim());
+ continue;
+ }
+
+ var orderedMatch = Regex.Match(trimmed, "^(\\d+)\\.\\s+(.*)");
+ if (orderedMatch.Success)
+ {
+ flushParagraph();
+ if (listBuffer.Count > 0)
+ {
+ var unordered = new UnorderedListBlock();
+ unordered.Items.AddRange(listBuffer);
+ result.Add(unordered);
+ listBuffer.Clear();
+ }
+ orderedListBuffer.Add(orderedMatch.Groups[2].Value.Trim());
+ continue;
+ }
+
+ var imageMatch = Regex.Match(trimmed, "^!\\[([^\\]]*)\\]\\(([^)]+)\\)$");
+ if (imageMatch.Success)
+ {
+ flushParagraph();
+ flushLists();
+ result.Add(new ImageBlock
+ {
+ AltText = imageMatch.Groups[1].Value,
+ Url = imageMatch.Groups[2].Value
+ });
+ continue;
+ }
+
+ flushLists();
+ if (paragraphBuilder.Length > 0)
+ paragraphBuilder.AppendLine();
+ paragraphBuilder.Append(line);
+ }
+
+ if (inCodeBlock)
+ result.Add(new CodeBlock { Code = codeBuilder.ToString().TrimEnd('\n'), Language = codeLang });
+
+ flushParagraph();
+ flushLists();
+ return result;
+ }
+
+ private static bool IsTableSeparator(string[] lines, int idx)
+ {
+ if (idx < 0 || idx >= lines.Length)
+ return false;
+
+ var line = lines[idx].Trim();
+ if (!line.StartsWith("|", StringComparison.Ordinal))
+ return false;
+
+ var cells = line.Split('|');
+ if (cells.Length < 3)
+ return false;
+
+ for (var i = 1; i < cells.Length - 1; i++)
+ {
+ var cell = cells[i].Trim().Trim(':');
+ if (string.IsNullOrEmpty(cell) || cell.Any(c => c != '-'))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static TableBlock ParseTable(string[] lines, ref int idx)
+ {
+ var table = new TableBlock();
+ table.Headers = SplitTableCells(lines[idx].Trim());
+ table.Alignments = ParseTableAlignments(lines[idx + 1].Trim());
+
+ idx += 2;
+ while (idx < lines.Length)
+ {
+ var line = lines[idx].Trim();
+ if (string.IsNullOrEmpty(line) || !line.StartsWith("|", StringComparison.Ordinal))
+ break;
+
+ if (IsTableSeparator(lines, idx))
+ break;
+
+ table.Rows.Add(SplitTableCells(line));
+ idx++;
+ }
+
+ idx--;
+ return table;
+ }
+
+ private static List ParseTableAlignments(string line)
+ {
+ var result = new List();
+ var cells = line.Split('|');
+ for (var i = 1; i < cells.Length - 1; i++)
+ {
+ var cell = cells[i].Trim();
+ var left = cell.StartsWith(":", StringComparison.Ordinal);
+ var right = cell.EndsWith(":", StringComparison.Ordinal);
+ if (left && right) result.Add(TableAlignment.Center);
+ else if (right) result.Add(TableAlignment.Right);
+ else result.Add(TableAlignment.Left);
+ }
+ return result;
+ }
+
+ private static List SplitTableCells(string line)
+ {
+ var cells = line.Split('|').Select(c => c.Trim()).ToList();
+ if (cells.Count > 0 && cells[0] == string.Empty) cells.RemoveAt(0);
+ if (cells.Count > 0 && cells[cells.Count - 1] == string.Empty) cells.RemoveAt(cells.Count - 1);
+ return cells;
+ }
+
+ private static List ParseInlines(string text)
+ {
+ var result = new List();
+ if (string.IsNullOrEmpty(text))
+ return result;
+
+ const string pattern =
+ "(`[^`]+`)|(!\\[([^\\]]*)\\]\\(([^)]+)\\))|(\\[([^\\]]+)\\]\\(([^)]+)\\))|(\\*\\*\\*(.+?)\\*\\*\\*)|(___(.+?)___)|(\\*\\*(.+?)\\*\\*)|(__(.+?)__)|(\\*(.+?)\\*)|(_(.+?)_)|(~~(.+?)~~)";
+
+ var matches = Regex.Matches(text, pattern);
+ var start = 0;
+
+ foreach (Match match in matches)
+ {
+ if (match.Index > start)
+ {
+ var plain = text.Substring(start, match.Index - start);
+ if (!string.IsNullOrEmpty(plain))
+ result.Add(new InlineToken(InlineTokenKind.Text, plain, null));
+ }
+
+ if (match.Groups[1].Success)
+ result.Add(new InlineToken(InlineTokenKind.Code, match.Groups[1].Value.Trim('`'), null));
+ else if (match.Groups[3].Success)
+ result.Add(new InlineToken(InlineTokenKind.Image, match.Groups[3].Value, match.Groups[4].Value));
+ else if (match.Groups[6].Success)
+ result.Add(new InlineToken(InlineTokenKind.Link, match.Groups[6].Value, match.Groups[7].Value));
+ else if (match.Groups[9].Success)
+ result.Add(new InlineToken(InlineTokenKind.BoldItalic, match.Groups[9].Value, null));
+ else if (match.Groups[11].Success)
+ result.Add(new InlineToken(InlineTokenKind.BoldItalic, match.Groups[11].Value, null));
+ else if (match.Groups[13].Success)
+ result.Add(new InlineToken(InlineTokenKind.Bold, match.Groups[13].Value, null));
+ else if (match.Groups[15].Success)
+ result.Add(new InlineToken(InlineTokenKind.Bold, match.Groups[15].Value, null));
+ else if (match.Groups[17].Success)
+ result.Add(new InlineToken(InlineTokenKind.Italic, match.Groups[17].Value, null));
+ else if (match.Groups[19].Success)
+ result.Add(new InlineToken(InlineTokenKind.Italic, match.Groups[19].Value, null));
+ else if (match.Groups[21].Success)
+ result.Add(new InlineToken(InlineTokenKind.StrikeThrough, match.Groups[21].Value, null));
+
+ start = match.Index + match.Length;
+ }
+
+ if (start < text.Length)
+ {
+ var plain = text.Substring(start);
+ if (!string.IsNullOrEmpty(plain))
+ result.Add(new InlineToken(InlineTokenKind.Text, plain, null));
+ }
+
+ return result;
+ }
+
+ private abstract class MarkdownBlock { }
+
+ private sealed class HeaderBlock : MarkdownBlock
+ {
+ public int Level;
+ public string Text;
+ }
+
+ private sealed class ParagraphBlock : MarkdownBlock
+ {
+ public string Text;
+ }
+
+ private sealed class HorizontalRuleBlock : MarkdownBlock { }
+
+ private sealed class UnorderedListBlock : MarkdownBlock
+ {
+ public readonly List Items = new List();
+ }
+
+ private sealed class OrderedListBlock : MarkdownBlock
+ {
+ public readonly List Items = new List();
+ }
+
+ private sealed class QuoteBlock : MarkdownBlock
+ {
+ public string Text;
+ }
+
+ private sealed class CodeBlock : MarkdownBlock
+ {
+ public string Code;
+ public string Language;
+ }
+
+ private sealed class ImageBlock : MarkdownBlock
+ {
+ public string AltText;
+ public string Url;
+ }
+
+ private sealed class TableBlock : MarkdownBlock
+ {
+ public List Headers = new List();
+ public List> Rows = new List>();
+ public List Alignments = new List();
+ }
+
+ private enum TableAlignment
+ {
+ Left,
+ Center,
+ Right
+ }
+
+ private enum InlineTokenKind
+ {
+ Text,
+ Bold,
+ Italic,
+ BoldItalic,
+ Code,
+ StrikeThrough,
+ Link,
+ Image
+ }
+
+ private sealed class InlineToken
+ {
+ public InlineToken(InlineTokenKind kind, string text, string url)
+ {
+ Kind = kind;
+ Text = text;
+ Url = url;
+ }
+
+ public readonly InlineTokenKind Kind;
+ public readonly string Text;
+ public readonly string Url;
+ }
+ }
+}
diff --git a/src/WPFDevelopers.Shared/Controls/MultiSelectComboBox/MultiSelectComboBox.cs b/src/WPFDevelopers.Shared/Controls/MultiSelectComboBox/MultiSelectComboBox.cs
index ad6f2955..a1f2d8d1 100644
--- a/src/WPFDevelopers.Shared/Controls/MultiSelectComboBox/MultiSelectComboBox.cs
+++ b/src/WPFDevelopers.Shared/Controls/MultiSelectComboBox/MultiSelectComboBox.cs
@@ -102,6 +102,7 @@ public static readonly DependencyProperty MaxDropDownHeightProperty
private TextBox _editableTextBox;
private string _theLastText;
private bool _isUpdating;
+ private bool _needApplySelectedItems;
private List
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
MSBuild:Compile
Designer