Markdown Display in WPF
I wanted to display a markdown document in WPF sensibly without relying on an HTML rendering system. I wrote a control based on the RichTextBox
, and used Microsoft's markdown parser. What do you think?
XAML:
<RichTextBox x:Class="MarkdownViewer.MarkdownBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MarkdownViewer"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="Content">
</RichTextBox>
C# backend for the XAML file:
public partial class MarkdownBox : RichTextBox
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(MarkdownBox), new UIPropertyMetadata(default(string), PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
if (source is MarkdownBox control)
{
var newValue = (string)args.NewValue;
switch (args.Property.Name)
{
case nameof(Text):
control.Text = newValue;
break;
}
}
}
public string Text
{
get => (string)GetValue(TextProperty);
set
{
var old = GetValue(TextProperty);
SetValue(TextProperty, value);
OnPropertyChanged(new DependencyPropertyChangedEventArgs(TextProperty, old, value));
SetTextboxContent();
}
}
public MarkdownBox()
{
InitializeComponent();
}
private void Hlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
private void SetTextboxContent()
{
Content.Document.Blocks.Clear();
var doc = new MarkdownDocument();
doc.Parse(Text ?? string.Empty);
Content.Document.Blocks.AddRange(GetBlocks(doc.Blocks));
}
private IEnumerable<Block> GetBlocks(IList<MarkdownBlock> blocks)
{
foreach (var block in blocks)
{
switch (block)
{
case HeaderBlock header:
yield return GetHeaderBlock(header);
break;
case ParagraphBlock paragraph:
yield return GetParagraphBlock(paragraph);
break;
case ListBlock list:
yield return GetListBlock(list);
break;
case CodeBlock code:
yield return GetCodeBlock(code);
break;
case QuoteBlock quote:
yield return GetQuoteBlock(quote);
break;
case HorizontalRuleBlock rule:
yield return GetRuleBlock(rule);
break;
case TableBlock table:
yield return GetTableBlock(table);
break;
default:
throw new NotImplementedException();
}
}
}
private Block GetHeaderBlock(HeaderBlock header)
{
var headerLevels = new Dictionary<int, double>
{
[1] = 28,
[2] = 21,
[3] = 16.3833,
[4] = 14,
[5] = 11.6167,
[6] = 9.38333,
};
var content = header.Inlines.Select(GetInline);
var span = new Span();
span.Inlines.AddRange(content);
var labelElement = new Label
{
Content = span,
FontSize = headerLevels[header.HeaderLevel]
};
var blockElement = new BlockUIContainer(labelElement);
return blockElement;
}
private Block GetParagraphBlock(ParagraphBlock paragraph)
{
var paragraphElement = new Paragraph();
paragraphElement.Inlines.AddRange(paragraph.Inlines.Select(GetInline));
return paragraphElement;
}
private Block GetListBlock(ListBlock list)
{
var listElement = new List
{
MarkerStyle = list.Style == ListStyle.Bulleted ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal
};
foreach (var item in list.Items)
{
var listItemElement = new ListItem();
listItemElement.Blocks.AddRange(GetBlocks(item.Blocks));
listElement.ListItems.Add(listItemElement);
}
return listElement;
}
private Block GetCodeBlock(CodeBlock code)
{
var typeConverter = new HighlightingDefinitionTypeConverter();
var avalon = new TextEditor
{
Text = code.Text,
SyntaxHighlighting = (IHighlightingDefinition)typeConverter.ConvertFrom("C#"),
FontFamily = new FontFamily("Consolas"),
FontSize = 12,
Padding = new Thickness(10),
BorderBrush = Brushes.LightGray,
BorderThickness = new Thickness(1),
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
IsReadOnly = true,
ShowLineNumbers = true,
MaxHeight = 250
};
return new BlockUIContainer(avalon);
}
private Block GetQuoteBlock(QuoteBlock quote)
{
var sectionElement = new Section
{
Background = new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0xDC)),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xff, 0xeb, 0x8e)),
BorderThickness = new Thickness(2, 0, 0, 0),
Padding = new Thickness(5)
};
var quoteBlocks = GetBlocks(quote.Blocks).ToList();
for (var i = 0; i < quoteBlocks.Count; i++)
{
var item = quoteBlocks[i];
item.Padding = new Thickness(5, 0, 5, 0);
item.Margin = new Thickness(0);
sectionElement.Blocks.Add(item);
}
return sectionElement;
}
private Block GetRuleBlock(HorizontalRuleBlock rule)
{
var line = new Line
{
Stretch = Stretch.Fill,
Stroke = Brushes.DarkGray,
X2 = 1
};
return new Paragraph(new InlineUIContainer(line));
}
private Block GetTableBlock(TableBlock table)
{
var alignments = new Dictionary<ColumnAlignment, TextAlignment>
{
[ColumnAlignment.Center] = TextAlignment.Center,
[ColumnAlignment.Left] = TextAlignment.Left,
[ColumnAlignment.Right] = TextAlignment.Right,
[ColumnAlignment.Unspecified] = TextAlignment.Justify
};
var tableElement = new Table
{
BorderThickness = new Thickness(0, 0, 1, 1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
CellSpacing = 0
};
var tableRowGroup = new TableRowGroup();
for (int rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
{
var row = table.Rows[rowIndex];
var tableRow = new TableRow();
if (rowIndex % 2 == 0 && rowIndex != 0)
{
tableRow.Background = new SolidColorBrush(Color.FromRgb(0xf6, 0xf8, 0xfa));
}
for (int cellIndex = 0; cellIndex < row.Cells.Count; cellIndex++)
{
var cell = row.Cells[cellIndex];
var cellContent = new Paragraph();
cellContent.Inlines.AddRange(cell.Inlines.Select(GetInline));
var tableCell = new TableCell
{
TextAlignment = alignments[table.ColumnDefinitions[cellIndex].Alignment],
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
BorderThickness = new Thickness(1, 1, 0, 0),
Padding = new Thickness(13, 6, 13, 6)
};
tableCell.Blocks.Add(cellContent);
if (rowIndex == 0)
{
tableCell.FontWeight = FontWeights.Bold;
}
tableRow.Cells.Add(tableCell);
}
tableRowGroup.Rows.Add(tableRow);
}
tableElement.RowGroups.Add(tableRowGroup);
return tableElement;
}
private Inline GetInline(MarkdownInline element)
{
switch (element)
{
case BoldTextInline bold:
return GetBoldInline(bold);
case TextRunInline text:
return GetTextRunInline(text);
case ItalicTextInline italic:
return GetItalicInline(italic);
case StrikethroughTextInline strikethrough:
return GetStrikethroughInline(strikethrough);
case CodeInline code:
return GetCodeInline(code);
case MarkdownLinkInline markdownLink:
return GetMarkdownLinkInline(markdownLink);
case HyperlinkInline hyperlink:
return GetHyperlinkInline(hyperlink);
case ImageInline image:
return GetImageInline(image);
case SubscriptTextInline subscript:
return GetSubscriptInline(subscript);
case SuperscriptTextInline superscript:
return GetSuperscriptInline(superscript);
default:
throw new NotImplementedException();
}
}
private Inline GetBoldInline(BoldTextInline bold)
{
var boldElement = new Bold();
foreach (var inline in bold.Inlines)
{
boldElement.Inlines.Add(GetInline(inline));
}
return boldElement;
}
private static Inline GetTextRunInline(TextRunInline text)
{
return new Run(text.ToString());
}
private Inline GetItalicInline(ItalicTextInline italic)
{
var italicElement = new Italic();
foreach (var inline in italic.Inlines)
{
italicElement.Inlines.Add(GetInline(inline));
}
return italicElement;
}
private Inline GetStrikethroughInline(StrikethroughTextInline strikethrough)
{
var strikethroughElement = new Span();
strikethroughElement.TextDecorations.Add(TextDecorations.Strikethrough);
foreach (var inline in strikethrough.Inlines)
{
strikethroughElement.Inlines.Add(GetInline(inline));
}
return strikethroughElement;
}
private static Inline GetCodeInline(CodeInline code)
{
return new Run(code.Text)
{
Background = new SolidColorBrush(Color.FromRgb(0xef, 0xf0, 0xf1))
};
}
private Inline GetMarkdownLinkInline(MarkdownLinkInline markdownLink)
{
var markdownLinkElement = new Hyperlink();
markdownLinkElement.Inlines.AddRange(markdownLink.Inlines.Select(GetInline));
markdownLinkElement.NavigateUri = new Uri(markdownLink.Url);
markdownLinkElement.ToolTip = markdownLink.Tooltip;
markdownLinkElement.RequestNavigate += Hlink_RequestNavigate;
return markdownLinkElement;
}
private Inline GetHyperlinkInline(HyperlinkInline hyperlink)
{
var hyperlinkElement = new Hyperlink();
hyperlinkElement.Inlines.Add(hyperlink.Text);
hyperlinkElement.NavigateUri = new Uri(hyperlink.Url);
hyperlinkElement.RequestNavigate += Hlink_RequestNavigate;
return hyperlinkElement;
}
private static Inline GetImageInline(ImageInline image)
{
var uri = new Uri(image.RenderUrl);
var bitmap = new BitmapImage(uri);
var imageElement = new Image
{
Source = bitmap,
Height = image.ImageHeight == 0 ? double.NaN : image.ImageHeight,
Width = image.ImageWidth == 0 ? double.NaN : image.ImageWidth,
ToolTip = image.Tooltip
};
return new InlineUIContainer(imageElement);
}
private Inline GetSubscriptInline(SubscriptTextInline subscript)
{
var subscriptElement = new Span();
subscriptElement.Typography.Variants = FontVariants.Subscript;
foreach (var inline in subscript.Inlines)
{
subscriptElement.Inlines.Add(GetInline(inline));
}
return subscriptElement;
}
private Inline GetSuperscriptInline(SuperscriptTextInline superscript)
{
var superscriptElement = new Span();
superscriptElement.Typography.Variants = FontVariants.Superscript;
foreach (var inline in superscript.Inlines)
{
superscriptElement.Inlines.Add(GetInline(inline));
}
return superscriptElement;
}
}
c# wpf markdown
add a comment |
I wanted to display a markdown document in WPF sensibly without relying on an HTML rendering system. I wrote a control based on the RichTextBox
, and used Microsoft's markdown parser. What do you think?
XAML:
<RichTextBox x:Class="MarkdownViewer.MarkdownBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MarkdownViewer"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="Content">
</RichTextBox>
C# backend for the XAML file:
public partial class MarkdownBox : RichTextBox
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(MarkdownBox), new UIPropertyMetadata(default(string), PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
if (source is MarkdownBox control)
{
var newValue = (string)args.NewValue;
switch (args.Property.Name)
{
case nameof(Text):
control.Text = newValue;
break;
}
}
}
public string Text
{
get => (string)GetValue(TextProperty);
set
{
var old = GetValue(TextProperty);
SetValue(TextProperty, value);
OnPropertyChanged(new DependencyPropertyChangedEventArgs(TextProperty, old, value));
SetTextboxContent();
}
}
public MarkdownBox()
{
InitializeComponent();
}
private void Hlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
private void SetTextboxContent()
{
Content.Document.Blocks.Clear();
var doc = new MarkdownDocument();
doc.Parse(Text ?? string.Empty);
Content.Document.Blocks.AddRange(GetBlocks(doc.Blocks));
}
private IEnumerable<Block> GetBlocks(IList<MarkdownBlock> blocks)
{
foreach (var block in blocks)
{
switch (block)
{
case HeaderBlock header:
yield return GetHeaderBlock(header);
break;
case ParagraphBlock paragraph:
yield return GetParagraphBlock(paragraph);
break;
case ListBlock list:
yield return GetListBlock(list);
break;
case CodeBlock code:
yield return GetCodeBlock(code);
break;
case QuoteBlock quote:
yield return GetQuoteBlock(quote);
break;
case HorizontalRuleBlock rule:
yield return GetRuleBlock(rule);
break;
case TableBlock table:
yield return GetTableBlock(table);
break;
default:
throw new NotImplementedException();
}
}
}
private Block GetHeaderBlock(HeaderBlock header)
{
var headerLevels = new Dictionary<int, double>
{
[1] = 28,
[2] = 21,
[3] = 16.3833,
[4] = 14,
[5] = 11.6167,
[6] = 9.38333,
};
var content = header.Inlines.Select(GetInline);
var span = new Span();
span.Inlines.AddRange(content);
var labelElement = new Label
{
Content = span,
FontSize = headerLevels[header.HeaderLevel]
};
var blockElement = new BlockUIContainer(labelElement);
return blockElement;
}
private Block GetParagraphBlock(ParagraphBlock paragraph)
{
var paragraphElement = new Paragraph();
paragraphElement.Inlines.AddRange(paragraph.Inlines.Select(GetInline));
return paragraphElement;
}
private Block GetListBlock(ListBlock list)
{
var listElement = new List
{
MarkerStyle = list.Style == ListStyle.Bulleted ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal
};
foreach (var item in list.Items)
{
var listItemElement = new ListItem();
listItemElement.Blocks.AddRange(GetBlocks(item.Blocks));
listElement.ListItems.Add(listItemElement);
}
return listElement;
}
private Block GetCodeBlock(CodeBlock code)
{
var typeConverter = new HighlightingDefinitionTypeConverter();
var avalon = new TextEditor
{
Text = code.Text,
SyntaxHighlighting = (IHighlightingDefinition)typeConverter.ConvertFrom("C#"),
FontFamily = new FontFamily("Consolas"),
FontSize = 12,
Padding = new Thickness(10),
BorderBrush = Brushes.LightGray,
BorderThickness = new Thickness(1),
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
IsReadOnly = true,
ShowLineNumbers = true,
MaxHeight = 250
};
return new BlockUIContainer(avalon);
}
private Block GetQuoteBlock(QuoteBlock quote)
{
var sectionElement = new Section
{
Background = new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0xDC)),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xff, 0xeb, 0x8e)),
BorderThickness = new Thickness(2, 0, 0, 0),
Padding = new Thickness(5)
};
var quoteBlocks = GetBlocks(quote.Blocks).ToList();
for (var i = 0; i < quoteBlocks.Count; i++)
{
var item = quoteBlocks[i];
item.Padding = new Thickness(5, 0, 5, 0);
item.Margin = new Thickness(0);
sectionElement.Blocks.Add(item);
}
return sectionElement;
}
private Block GetRuleBlock(HorizontalRuleBlock rule)
{
var line = new Line
{
Stretch = Stretch.Fill,
Stroke = Brushes.DarkGray,
X2 = 1
};
return new Paragraph(new InlineUIContainer(line));
}
private Block GetTableBlock(TableBlock table)
{
var alignments = new Dictionary<ColumnAlignment, TextAlignment>
{
[ColumnAlignment.Center] = TextAlignment.Center,
[ColumnAlignment.Left] = TextAlignment.Left,
[ColumnAlignment.Right] = TextAlignment.Right,
[ColumnAlignment.Unspecified] = TextAlignment.Justify
};
var tableElement = new Table
{
BorderThickness = new Thickness(0, 0, 1, 1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
CellSpacing = 0
};
var tableRowGroup = new TableRowGroup();
for (int rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
{
var row = table.Rows[rowIndex];
var tableRow = new TableRow();
if (rowIndex % 2 == 0 && rowIndex != 0)
{
tableRow.Background = new SolidColorBrush(Color.FromRgb(0xf6, 0xf8, 0xfa));
}
for (int cellIndex = 0; cellIndex < row.Cells.Count; cellIndex++)
{
var cell = row.Cells[cellIndex];
var cellContent = new Paragraph();
cellContent.Inlines.AddRange(cell.Inlines.Select(GetInline));
var tableCell = new TableCell
{
TextAlignment = alignments[table.ColumnDefinitions[cellIndex].Alignment],
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
BorderThickness = new Thickness(1, 1, 0, 0),
Padding = new Thickness(13, 6, 13, 6)
};
tableCell.Blocks.Add(cellContent);
if (rowIndex == 0)
{
tableCell.FontWeight = FontWeights.Bold;
}
tableRow.Cells.Add(tableCell);
}
tableRowGroup.Rows.Add(tableRow);
}
tableElement.RowGroups.Add(tableRowGroup);
return tableElement;
}
private Inline GetInline(MarkdownInline element)
{
switch (element)
{
case BoldTextInline bold:
return GetBoldInline(bold);
case TextRunInline text:
return GetTextRunInline(text);
case ItalicTextInline italic:
return GetItalicInline(italic);
case StrikethroughTextInline strikethrough:
return GetStrikethroughInline(strikethrough);
case CodeInline code:
return GetCodeInline(code);
case MarkdownLinkInline markdownLink:
return GetMarkdownLinkInline(markdownLink);
case HyperlinkInline hyperlink:
return GetHyperlinkInline(hyperlink);
case ImageInline image:
return GetImageInline(image);
case SubscriptTextInline subscript:
return GetSubscriptInline(subscript);
case SuperscriptTextInline superscript:
return GetSuperscriptInline(superscript);
default:
throw new NotImplementedException();
}
}
private Inline GetBoldInline(BoldTextInline bold)
{
var boldElement = new Bold();
foreach (var inline in bold.Inlines)
{
boldElement.Inlines.Add(GetInline(inline));
}
return boldElement;
}
private static Inline GetTextRunInline(TextRunInline text)
{
return new Run(text.ToString());
}
private Inline GetItalicInline(ItalicTextInline italic)
{
var italicElement = new Italic();
foreach (var inline in italic.Inlines)
{
italicElement.Inlines.Add(GetInline(inline));
}
return italicElement;
}
private Inline GetStrikethroughInline(StrikethroughTextInline strikethrough)
{
var strikethroughElement = new Span();
strikethroughElement.TextDecorations.Add(TextDecorations.Strikethrough);
foreach (var inline in strikethrough.Inlines)
{
strikethroughElement.Inlines.Add(GetInline(inline));
}
return strikethroughElement;
}
private static Inline GetCodeInline(CodeInline code)
{
return new Run(code.Text)
{
Background = new SolidColorBrush(Color.FromRgb(0xef, 0xf0, 0xf1))
};
}
private Inline GetMarkdownLinkInline(MarkdownLinkInline markdownLink)
{
var markdownLinkElement = new Hyperlink();
markdownLinkElement.Inlines.AddRange(markdownLink.Inlines.Select(GetInline));
markdownLinkElement.NavigateUri = new Uri(markdownLink.Url);
markdownLinkElement.ToolTip = markdownLink.Tooltip;
markdownLinkElement.RequestNavigate += Hlink_RequestNavigate;
return markdownLinkElement;
}
private Inline GetHyperlinkInline(HyperlinkInline hyperlink)
{
var hyperlinkElement = new Hyperlink();
hyperlinkElement.Inlines.Add(hyperlink.Text);
hyperlinkElement.NavigateUri = new Uri(hyperlink.Url);
hyperlinkElement.RequestNavigate += Hlink_RequestNavigate;
return hyperlinkElement;
}
private static Inline GetImageInline(ImageInline image)
{
var uri = new Uri(image.RenderUrl);
var bitmap = new BitmapImage(uri);
var imageElement = new Image
{
Source = bitmap,
Height = image.ImageHeight == 0 ? double.NaN : image.ImageHeight,
Width = image.ImageWidth == 0 ? double.NaN : image.ImageWidth,
ToolTip = image.Tooltip
};
return new InlineUIContainer(imageElement);
}
private Inline GetSubscriptInline(SubscriptTextInline subscript)
{
var subscriptElement = new Span();
subscriptElement.Typography.Variants = FontVariants.Subscript;
foreach (var inline in subscript.Inlines)
{
subscriptElement.Inlines.Add(GetInline(inline));
}
return subscriptElement;
}
private Inline GetSuperscriptInline(SuperscriptTextInline superscript)
{
var superscriptElement = new Span();
superscriptElement.Typography.Variants = FontVariants.Superscript;
foreach (var inline in superscript.Inlines)
{
superscriptElement.Inlines.Add(GetInline(inline));
}
return superscriptElement;
}
}
c# wpf markdown
add a comment |
I wanted to display a markdown document in WPF sensibly without relying on an HTML rendering system. I wrote a control based on the RichTextBox
, and used Microsoft's markdown parser. What do you think?
XAML:
<RichTextBox x:Class="MarkdownViewer.MarkdownBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MarkdownViewer"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="Content">
</RichTextBox>
C# backend for the XAML file:
public partial class MarkdownBox : RichTextBox
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(MarkdownBox), new UIPropertyMetadata(default(string), PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
if (source is MarkdownBox control)
{
var newValue = (string)args.NewValue;
switch (args.Property.Name)
{
case nameof(Text):
control.Text = newValue;
break;
}
}
}
public string Text
{
get => (string)GetValue(TextProperty);
set
{
var old = GetValue(TextProperty);
SetValue(TextProperty, value);
OnPropertyChanged(new DependencyPropertyChangedEventArgs(TextProperty, old, value));
SetTextboxContent();
}
}
public MarkdownBox()
{
InitializeComponent();
}
private void Hlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
private void SetTextboxContent()
{
Content.Document.Blocks.Clear();
var doc = new MarkdownDocument();
doc.Parse(Text ?? string.Empty);
Content.Document.Blocks.AddRange(GetBlocks(doc.Blocks));
}
private IEnumerable<Block> GetBlocks(IList<MarkdownBlock> blocks)
{
foreach (var block in blocks)
{
switch (block)
{
case HeaderBlock header:
yield return GetHeaderBlock(header);
break;
case ParagraphBlock paragraph:
yield return GetParagraphBlock(paragraph);
break;
case ListBlock list:
yield return GetListBlock(list);
break;
case CodeBlock code:
yield return GetCodeBlock(code);
break;
case QuoteBlock quote:
yield return GetQuoteBlock(quote);
break;
case HorizontalRuleBlock rule:
yield return GetRuleBlock(rule);
break;
case TableBlock table:
yield return GetTableBlock(table);
break;
default:
throw new NotImplementedException();
}
}
}
private Block GetHeaderBlock(HeaderBlock header)
{
var headerLevels = new Dictionary<int, double>
{
[1] = 28,
[2] = 21,
[3] = 16.3833,
[4] = 14,
[5] = 11.6167,
[6] = 9.38333,
};
var content = header.Inlines.Select(GetInline);
var span = new Span();
span.Inlines.AddRange(content);
var labelElement = new Label
{
Content = span,
FontSize = headerLevels[header.HeaderLevel]
};
var blockElement = new BlockUIContainer(labelElement);
return blockElement;
}
private Block GetParagraphBlock(ParagraphBlock paragraph)
{
var paragraphElement = new Paragraph();
paragraphElement.Inlines.AddRange(paragraph.Inlines.Select(GetInline));
return paragraphElement;
}
private Block GetListBlock(ListBlock list)
{
var listElement = new List
{
MarkerStyle = list.Style == ListStyle.Bulleted ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal
};
foreach (var item in list.Items)
{
var listItemElement = new ListItem();
listItemElement.Blocks.AddRange(GetBlocks(item.Blocks));
listElement.ListItems.Add(listItemElement);
}
return listElement;
}
private Block GetCodeBlock(CodeBlock code)
{
var typeConverter = new HighlightingDefinitionTypeConverter();
var avalon = new TextEditor
{
Text = code.Text,
SyntaxHighlighting = (IHighlightingDefinition)typeConverter.ConvertFrom("C#"),
FontFamily = new FontFamily("Consolas"),
FontSize = 12,
Padding = new Thickness(10),
BorderBrush = Brushes.LightGray,
BorderThickness = new Thickness(1),
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
IsReadOnly = true,
ShowLineNumbers = true,
MaxHeight = 250
};
return new BlockUIContainer(avalon);
}
private Block GetQuoteBlock(QuoteBlock quote)
{
var sectionElement = new Section
{
Background = new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0xDC)),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xff, 0xeb, 0x8e)),
BorderThickness = new Thickness(2, 0, 0, 0),
Padding = new Thickness(5)
};
var quoteBlocks = GetBlocks(quote.Blocks).ToList();
for (var i = 0; i < quoteBlocks.Count; i++)
{
var item = quoteBlocks[i];
item.Padding = new Thickness(5, 0, 5, 0);
item.Margin = new Thickness(0);
sectionElement.Blocks.Add(item);
}
return sectionElement;
}
private Block GetRuleBlock(HorizontalRuleBlock rule)
{
var line = new Line
{
Stretch = Stretch.Fill,
Stroke = Brushes.DarkGray,
X2 = 1
};
return new Paragraph(new InlineUIContainer(line));
}
private Block GetTableBlock(TableBlock table)
{
var alignments = new Dictionary<ColumnAlignment, TextAlignment>
{
[ColumnAlignment.Center] = TextAlignment.Center,
[ColumnAlignment.Left] = TextAlignment.Left,
[ColumnAlignment.Right] = TextAlignment.Right,
[ColumnAlignment.Unspecified] = TextAlignment.Justify
};
var tableElement = new Table
{
BorderThickness = new Thickness(0, 0, 1, 1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
CellSpacing = 0
};
var tableRowGroup = new TableRowGroup();
for (int rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
{
var row = table.Rows[rowIndex];
var tableRow = new TableRow();
if (rowIndex % 2 == 0 && rowIndex != 0)
{
tableRow.Background = new SolidColorBrush(Color.FromRgb(0xf6, 0xf8, 0xfa));
}
for (int cellIndex = 0; cellIndex < row.Cells.Count; cellIndex++)
{
var cell = row.Cells[cellIndex];
var cellContent = new Paragraph();
cellContent.Inlines.AddRange(cell.Inlines.Select(GetInline));
var tableCell = new TableCell
{
TextAlignment = alignments[table.ColumnDefinitions[cellIndex].Alignment],
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
BorderThickness = new Thickness(1, 1, 0, 0),
Padding = new Thickness(13, 6, 13, 6)
};
tableCell.Blocks.Add(cellContent);
if (rowIndex == 0)
{
tableCell.FontWeight = FontWeights.Bold;
}
tableRow.Cells.Add(tableCell);
}
tableRowGroup.Rows.Add(tableRow);
}
tableElement.RowGroups.Add(tableRowGroup);
return tableElement;
}
private Inline GetInline(MarkdownInline element)
{
switch (element)
{
case BoldTextInline bold:
return GetBoldInline(bold);
case TextRunInline text:
return GetTextRunInline(text);
case ItalicTextInline italic:
return GetItalicInline(italic);
case StrikethroughTextInline strikethrough:
return GetStrikethroughInline(strikethrough);
case CodeInline code:
return GetCodeInline(code);
case MarkdownLinkInline markdownLink:
return GetMarkdownLinkInline(markdownLink);
case HyperlinkInline hyperlink:
return GetHyperlinkInline(hyperlink);
case ImageInline image:
return GetImageInline(image);
case SubscriptTextInline subscript:
return GetSubscriptInline(subscript);
case SuperscriptTextInline superscript:
return GetSuperscriptInline(superscript);
default:
throw new NotImplementedException();
}
}
private Inline GetBoldInline(BoldTextInline bold)
{
var boldElement = new Bold();
foreach (var inline in bold.Inlines)
{
boldElement.Inlines.Add(GetInline(inline));
}
return boldElement;
}
private static Inline GetTextRunInline(TextRunInline text)
{
return new Run(text.ToString());
}
private Inline GetItalicInline(ItalicTextInline italic)
{
var italicElement = new Italic();
foreach (var inline in italic.Inlines)
{
italicElement.Inlines.Add(GetInline(inline));
}
return italicElement;
}
private Inline GetStrikethroughInline(StrikethroughTextInline strikethrough)
{
var strikethroughElement = new Span();
strikethroughElement.TextDecorations.Add(TextDecorations.Strikethrough);
foreach (var inline in strikethrough.Inlines)
{
strikethroughElement.Inlines.Add(GetInline(inline));
}
return strikethroughElement;
}
private static Inline GetCodeInline(CodeInline code)
{
return new Run(code.Text)
{
Background = new SolidColorBrush(Color.FromRgb(0xef, 0xf0, 0xf1))
};
}
private Inline GetMarkdownLinkInline(MarkdownLinkInline markdownLink)
{
var markdownLinkElement = new Hyperlink();
markdownLinkElement.Inlines.AddRange(markdownLink.Inlines.Select(GetInline));
markdownLinkElement.NavigateUri = new Uri(markdownLink.Url);
markdownLinkElement.ToolTip = markdownLink.Tooltip;
markdownLinkElement.RequestNavigate += Hlink_RequestNavigate;
return markdownLinkElement;
}
private Inline GetHyperlinkInline(HyperlinkInline hyperlink)
{
var hyperlinkElement = new Hyperlink();
hyperlinkElement.Inlines.Add(hyperlink.Text);
hyperlinkElement.NavigateUri = new Uri(hyperlink.Url);
hyperlinkElement.RequestNavigate += Hlink_RequestNavigate;
return hyperlinkElement;
}
private static Inline GetImageInline(ImageInline image)
{
var uri = new Uri(image.RenderUrl);
var bitmap = new BitmapImage(uri);
var imageElement = new Image
{
Source = bitmap,
Height = image.ImageHeight == 0 ? double.NaN : image.ImageHeight,
Width = image.ImageWidth == 0 ? double.NaN : image.ImageWidth,
ToolTip = image.Tooltip
};
return new InlineUIContainer(imageElement);
}
private Inline GetSubscriptInline(SubscriptTextInline subscript)
{
var subscriptElement = new Span();
subscriptElement.Typography.Variants = FontVariants.Subscript;
foreach (var inline in subscript.Inlines)
{
subscriptElement.Inlines.Add(GetInline(inline));
}
return subscriptElement;
}
private Inline GetSuperscriptInline(SuperscriptTextInline superscript)
{
var superscriptElement = new Span();
superscriptElement.Typography.Variants = FontVariants.Superscript;
foreach (var inline in superscript.Inlines)
{
superscriptElement.Inlines.Add(GetInline(inline));
}
return superscriptElement;
}
}
c# wpf markdown
I wanted to display a markdown document in WPF sensibly without relying on an HTML rendering system. I wrote a control based on the RichTextBox
, and used Microsoft's markdown parser. What do you think?
XAML:
<RichTextBox x:Class="MarkdownViewer.MarkdownBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MarkdownViewer"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="Content">
</RichTextBox>
C# backend for the XAML file:
public partial class MarkdownBox : RichTextBox
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(MarkdownBox), new UIPropertyMetadata(default(string), PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject source, DependencyPropertyChangedEventArgs args)
{
if (source is MarkdownBox control)
{
var newValue = (string)args.NewValue;
switch (args.Property.Name)
{
case nameof(Text):
control.Text = newValue;
break;
}
}
}
public string Text
{
get => (string)GetValue(TextProperty);
set
{
var old = GetValue(TextProperty);
SetValue(TextProperty, value);
OnPropertyChanged(new DependencyPropertyChangedEventArgs(TextProperty, old, value));
SetTextboxContent();
}
}
public MarkdownBox()
{
InitializeComponent();
}
private void Hlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
e.Handled = true;
}
private void SetTextboxContent()
{
Content.Document.Blocks.Clear();
var doc = new MarkdownDocument();
doc.Parse(Text ?? string.Empty);
Content.Document.Blocks.AddRange(GetBlocks(doc.Blocks));
}
private IEnumerable<Block> GetBlocks(IList<MarkdownBlock> blocks)
{
foreach (var block in blocks)
{
switch (block)
{
case HeaderBlock header:
yield return GetHeaderBlock(header);
break;
case ParagraphBlock paragraph:
yield return GetParagraphBlock(paragraph);
break;
case ListBlock list:
yield return GetListBlock(list);
break;
case CodeBlock code:
yield return GetCodeBlock(code);
break;
case QuoteBlock quote:
yield return GetQuoteBlock(quote);
break;
case HorizontalRuleBlock rule:
yield return GetRuleBlock(rule);
break;
case TableBlock table:
yield return GetTableBlock(table);
break;
default:
throw new NotImplementedException();
}
}
}
private Block GetHeaderBlock(HeaderBlock header)
{
var headerLevels = new Dictionary<int, double>
{
[1] = 28,
[2] = 21,
[3] = 16.3833,
[4] = 14,
[5] = 11.6167,
[6] = 9.38333,
};
var content = header.Inlines.Select(GetInline);
var span = new Span();
span.Inlines.AddRange(content);
var labelElement = new Label
{
Content = span,
FontSize = headerLevels[header.HeaderLevel]
};
var blockElement = new BlockUIContainer(labelElement);
return blockElement;
}
private Block GetParagraphBlock(ParagraphBlock paragraph)
{
var paragraphElement = new Paragraph();
paragraphElement.Inlines.AddRange(paragraph.Inlines.Select(GetInline));
return paragraphElement;
}
private Block GetListBlock(ListBlock list)
{
var listElement = new List
{
MarkerStyle = list.Style == ListStyle.Bulleted ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal
};
foreach (var item in list.Items)
{
var listItemElement = new ListItem();
listItemElement.Blocks.AddRange(GetBlocks(item.Blocks));
listElement.ListItems.Add(listItemElement);
}
return listElement;
}
private Block GetCodeBlock(CodeBlock code)
{
var typeConverter = new HighlightingDefinitionTypeConverter();
var avalon = new TextEditor
{
Text = code.Text,
SyntaxHighlighting = (IHighlightingDefinition)typeConverter.ConvertFrom("C#"),
FontFamily = new FontFamily("Consolas"),
FontSize = 12,
Padding = new Thickness(10),
BorderBrush = Brushes.LightGray,
BorderThickness = new Thickness(1),
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
IsReadOnly = true,
ShowLineNumbers = true,
MaxHeight = 250
};
return new BlockUIContainer(avalon);
}
private Block GetQuoteBlock(QuoteBlock quote)
{
var sectionElement = new Section
{
Background = new SolidColorBrush(Color.FromRgb(0xFF, 0xF8, 0xDC)),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xff, 0xeb, 0x8e)),
BorderThickness = new Thickness(2, 0, 0, 0),
Padding = new Thickness(5)
};
var quoteBlocks = GetBlocks(quote.Blocks).ToList();
for (var i = 0; i < quoteBlocks.Count; i++)
{
var item = quoteBlocks[i];
item.Padding = new Thickness(5, 0, 5, 0);
item.Margin = new Thickness(0);
sectionElement.Blocks.Add(item);
}
return sectionElement;
}
private Block GetRuleBlock(HorizontalRuleBlock rule)
{
var line = new Line
{
Stretch = Stretch.Fill,
Stroke = Brushes.DarkGray,
X2 = 1
};
return new Paragraph(new InlineUIContainer(line));
}
private Block GetTableBlock(TableBlock table)
{
var alignments = new Dictionary<ColumnAlignment, TextAlignment>
{
[ColumnAlignment.Center] = TextAlignment.Center,
[ColumnAlignment.Left] = TextAlignment.Left,
[ColumnAlignment.Right] = TextAlignment.Right,
[ColumnAlignment.Unspecified] = TextAlignment.Justify
};
var tableElement = new Table
{
BorderThickness = new Thickness(0, 0, 1, 1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
CellSpacing = 0
};
var tableRowGroup = new TableRowGroup();
for (int rowIndex = 0; rowIndex < table.Rows.Count; rowIndex++)
{
var row = table.Rows[rowIndex];
var tableRow = new TableRow();
if (rowIndex % 2 == 0 && rowIndex != 0)
{
tableRow.Background = new SolidColorBrush(Color.FromRgb(0xf6, 0xf8, 0xfa));
}
for (int cellIndex = 0; cellIndex < row.Cells.Count; cellIndex++)
{
var cell = row.Cells[cellIndex];
var cellContent = new Paragraph();
cellContent.Inlines.AddRange(cell.Inlines.Select(GetInline));
var tableCell = new TableCell
{
TextAlignment = alignments[table.ColumnDefinitions[cellIndex].Alignment],
BorderBrush = new SolidColorBrush(Color.FromRgb(0xdf, 0xe2, 0xe5)),
BorderThickness = new Thickness(1, 1, 0, 0),
Padding = new Thickness(13, 6, 13, 6)
};
tableCell.Blocks.Add(cellContent);
if (rowIndex == 0)
{
tableCell.FontWeight = FontWeights.Bold;
}
tableRow.Cells.Add(tableCell);
}
tableRowGroup.Rows.Add(tableRow);
}
tableElement.RowGroups.Add(tableRowGroup);
return tableElement;
}
private Inline GetInline(MarkdownInline element)
{
switch (element)
{
case BoldTextInline bold:
return GetBoldInline(bold);
case TextRunInline text:
return GetTextRunInline(text);
case ItalicTextInline italic:
return GetItalicInline(italic);
case StrikethroughTextInline strikethrough:
return GetStrikethroughInline(strikethrough);
case CodeInline code:
return GetCodeInline(code);
case MarkdownLinkInline markdownLink:
return GetMarkdownLinkInline(markdownLink);
case HyperlinkInline hyperlink:
return GetHyperlinkInline(hyperlink);
case ImageInline image:
return GetImageInline(image);
case SubscriptTextInline subscript:
return GetSubscriptInline(subscript);
case SuperscriptTextInline superscript:
return GetSuperscriptInline(superscript);
default:
throw new NotImplementedException();
}
}
private Inline GetBoldInline(BoldTextInline bold)
{
var boldElement = new Bold();
foreach (var inline in bold.Inlines)
{
boldElement.Inlines.Add(GetInline(inline));
}
return boldElement;
}
private static Inline GetTextRunInline(TextRunInline text)
{
return new Run(text.ToString());
}
private Inline GetItalicInline(ItalicTextInline italic)
{
var italicElement = new Italic();
foreach (var inline in italic.Inlines)
{
italicElement.Inlines.Add(GetInline(inline));
}
return italicElement;
}
private Inline GetStrikethroughInline(StrikethroughTextInline strikethrough)
{
var strikethroughElement = new Span();
strikethroughElement.TextDecorations.Add(TextDecorations.Strikethrough);
foreach (var inline in strikethrough.Inlines)
{
strikethroughElement.Inlines.Add(GetInline(inline));
}
return strikethroughElement;
}
private static Inline GetCodeInline(CodeInline code)
{
return new Run(code.Text)
{
Background = new SolidColorBrush(Color.FromRgb(0xef, 0xf0, 0xf1))
};
}
private Inline GetMarkdownLinkInline(MarkdownLinkInline markdownLink)
{
var markdownLinkElement = new Hyperlink();
markdownLinkElement.Inlines.AddRange(markdownLink.Inlines.Select(GetInline));
markdownLinkElement.NavigateUri = new Uri(markdownLink.Url);
markdownLinkElement.ToolTip = markdownLink.Tooltip;
markdownLinkElement.RequestNavigate += Hlink_RequestNavigate;
return markdownLinkElement;
}
private Inline GetHyperlinkInline(HyperlinkInline hyperlink)
{
var hyperlinkElement = new Hyperlink();
hyperlinkElement.Inlines.Add(hyperlink.Text);
hyperlinkElement.NavigateUri = new Uri(hyperlink.Url);
hyperlinkElement.RequestNavigate += Hlink_RequestNavigate;
return hyperlinkElement;
}
private static Inline GetImageInline(ImageInline image)
{
var uri = new Uri(image.RenderUrl);
var bitmap = new BitmapImage(uri);
var imageElement = new Image
{
Source = bitmap,
Height = image.ImageHeight == 0 ? double.NaN : image.ImageHeight,
Width = image.ImageWidth == 0 ? double.NaN : image.ImageWidth,
ToolTip = image.Tooltip
};
return new InlineUIContainer(imageElement);
}
private Inline GetSubscriptInline(SubscriptTextInline subscript)
{
var subscriptElement = new Span();
subscriptElement.Typography.Variants = FontVariants.Subscript;
foreach (var inline in subscript.Inlines)
{
subscriptElement.Inlines.Add(GetInline(inline));
}
return subscriptElement;
}
private Inline GetSuperscriptInline(SuperscriptTextInline superscript)
{
var superscriptElement = new Span();
superscriptElement.Typography.Variants = FontVariants.Superscript;
foreach (var inline in superscript.Inlines)
{
superscriptElement.Inlines.Add(GetInline(inline));
}
return superscriptElement;
}
}
c# wpf markdown
c# wpf markdown
asked 57 mins ago
Hosch250
17.2k565157
17.2k565157
add a comment |
add a comment |
active
oldest
votes
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210520%2fmarkdown-display-in-wpf%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f210520%2fmarkdown-display-in-wpf%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown