Use MarkdownViewer from Markdig.Wpf.

Not yet working as markdig package dependency isn't strong name signed.
This commit is contained in:
Steven Kirk 2017-06-23 10:04:17 +02:00
Родитель 06b4c7d26b
Коммит a08ab1ee55
11 изменённых файлов: 50 добавлений и 1102 удалений

3
.gitmodules поставляемый
Просмотреть файл

@ -16,3 +16,6 @@
[submodule "script"]
path = script
url = git@github.com:github/VisualStudioBuildScripts
[submodule "submodules/markdig-wpf"]
path = submodules/markdig-wpf
url = https://github.com/grokys/markdig-wpf.git

Просмотреть файл

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

Просмотреть файл

@ -106,6 +106,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.StartPage", "src\Git
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI.UnitTests", "test\GitHub.UI.UnitTests\GitHub.UI.UnitTests.csproj", "{110B206F-8554-4B51-BF86-94DAA32F5E26}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Markdig", "Markdig", "{C5964392-2BEF-41DD-8D36-9E306FD67137}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Markdig.Wpf", "submodules\markdig-wpf\src\Markdig.Wpf\Markdig.Wpf.csproj", "{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -444,6 +448,18 @@ Global
{110B206F-8554-4B51-BF86-94DAA32F5E26}.Release|Any CPU.Build.0 = Release|Any CPU
{110B206F-8554-4B51-BF86-94DAA32F5E26}.Release|x86.ActiveCfg = Release|Any CPU
{110B206F-8554-4B51-BF86-94DAA32F5E26}.Release|x86.Build.0 = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Debug|x86.ActiveCfg = Debug|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Debug|x86.Build.0 = Debug|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Publish|Any CPU.ActiveCfg = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Publish|Any CPU.Build.0 = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Publish|x86.ActiveCfg = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Publish|x86.Build.0 = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|Any CPU.Build.0 = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|x86.ActiveCfg = Release|Any CPU
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -468,5 +484,7 @@ Global
{DD99FD0F-82F6-4C30-930E-4A1D0DF01D65} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB9}
{7B835A7D-CF94-45E8-B191-96F5A4FE26A8} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{110B206F-8554-4B51-BF86-94DAA32F5E26} = {8A7DA2E7-262B-4581-807A-1C45CE79CDFD}
{C5964392-2BEF-41DD-8D36-9E306FD67137} = {1E7F7253-A6AF-43C4-A955-37BEDDA01AB8}
{01E40FB5-B4E9-489D-98D5-4771DDC2D1D7} = {C5964392-2BEF-41DD-8D36-9E306FD67137}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -1,984 +0,0 @@
/*
* Based on https://github.com/theunrepentantgeek/Markdown.XAML (e4435c0291)
*/
using GitHub.Extensions;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace GitHub.UI
{
public class Markdown : DependencyObject
{
/// <summary>
/// maximum nested depth of [] and () supported by the transform; implementation detail
/// </summary>
const int nestDepth = 6;
/// <summary>
/// Tabs are automatically converted to spaces as part of the transform
/// this constant determines how "wide" those tabs become in spaces
/// </summary>
const int tabWidth = 4;
const string markerUL = @"[*+-]";
const string markerOL = @"\d+[.]";
int listLevel;
/// <summary>
/// when true, bold and italic require non-word characters on either side
/// WARNING: this is a significant deviation from the markdown spec
/// </summary>
///
public bool StrictBoldItalic { get; set; }
public ICommand HyperlinkCommand
{
get { return (ICommand)GetValue(HyperlinkCommandProperty); }
set { SetValue(HyperlinkCommandProperty, value); }
}
public static readonly DependencyProperty HyperlinkCommandProperty =
DependencyProperty.Register("HyperlinkCommand", typeof(ICommand), typeof(Markdown), new PropertyMetadata(null));
public Style DocumentStyle
{
get { return (Style)GetValue(DocumentStyleProperty); }
set { SetValue(DocumentStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for DocumentStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DocumentStyleProperty =
DependencyProperty.Register("DocumentStyle", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style Heading1Style
{
get { return (Style)GetValue(Heading1StyleProperty); }
set { SetValue(Heading1StyleProperty, value); }
}
// Using a DependencyProperty as the backing store for Heading1Style. This enables animation, styling, binding, etc...
public static readonly DependencyProperty Heading1StyleProperty =
DependencyProperty.Register("Heading1Style", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style Heading2Style
{
get { return (Style)GetValue(Heading2StyleProperty); }
set { SetValue(Heading2StyleProperty, value); }
}
// Using a DependencyProperty as the backing store for Heading2Style. This enables animation, styling, binding, etc...
public static readonly DependencyProperty Heading2StyleProperty =
DependencyProperty.Register("Heading2Style", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style Heading3Style
{
get { return (Style)GetValue(Heading3StyleProperty); }
set { SetValue(Heading3StyleProperty, value); }
}
// Using a DependencyProperty as the backing store for Heading3Style. This enables animation, styling, binding, etc...
public static readonly DependencyProperty Heading3StyleProperty =
DependencyProperty.Register("Heading3Style", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style Heading4Style
{
get { return (Style)GetValue(Heading4StyleProperty); }
set { SetValue(Heading4StyleProperty, value); }
}
// Using a DependencyProperty as the backing store for Heading4Style. This enables animation, styling, binding, etc...
public static readonly DependencyProperty Heading4StyleProperty =
DependencyProperty.Register("Heading4Style", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style CodeStyle
{
get { return (Style)GetValue(CodeStyleProperty); }
set { SetValue(CodeStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for CodeStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CodeStyleProperty =
DependencyProperty.Register("CodeStyle", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style LinkStyle
{
get { return (Style)GetValue(LinkStyleProperty); }
set { SetValue(LinkStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for LinkStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LinkStyleProperty =
DependencyProperty.Register("LinkStyle", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style ImageStyle
{
get { return (Style)GetValue(ImageStyleProperty); }
set { SetValue(ImageStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for ImageStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ImageStyleProperty =
DependencyProperty.Register("ImageStyle", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public Style SeparatorStyle
{
get { return (Style)GetValue(SeparatorStyleProperty); }
set { SetValue(SeparatorStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for SeparatorStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SeparatorStyleProperty =
DependencyProperty.Register("SeparatorStyle", typeof(Style), typeof(Markdown), new PropertyMetadata(null));
public string AssetPathRoot
{
get { return (string)GetValue(AssetPathRootProperty); }
set { SetValue(AssetPathRootProperty, value); }
}
public static readonly DependencyProperty AssetPathRootProperty =
DependencyProperty.Register("AssetPathRootRoot", typeof(string), typeof(Markdown), new PropertyMetadata(null));
public Markdown()
{
//HyperlinkCommand = NavigationCommands.GoToPage;
}
public FlowDocument Transform(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
text = Normalize(text);
var document = Create<FlowDocument, Block>(RunBlockGamut(text));
if (DocumentStyle != null)
{
document.Style = DocumentStyle;
}
else
{
document.PagePadding = new Thickness(0);
}
return document;
}
/// <summary>
/// Perform transformations that form block-level tags like paragraphs, headers, and list items.
/// </summary>
IEnumerable<Block> RunBlockGamut(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
return DoHeaders(text,
s1 => DoHorizontalRules(s1,
s2 => DoLists(s2,
sn => FormParagraphs(sn))));
//text = DoCodeBlocks(text);
//text = DoBlockQuotes(text);
//// We already ran HashHTMLBlocks() before, in Markdown(), but that
//// was to escape raw HTML in the original Markdown source. This time,
//// we're escaping the markup we've just created, so that we don't wrap
//// <p> tags around block-level tags.
//text = HashHTMLBlocks(text);
//text = FormParagraphs(text);
//return text;
}
/// <summary>
/// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items.
/// </summary>
IEnumerable<Inline> RunSpanGamut(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
return DoCodeSpans(text,
s0 => DoImages(s0,
s1 => DoAnchors(s1,
s2 => DoItalicsAndBold(s2,
s3 => DoText(s3)))));
//text = EscapeSpecialCharsWithinTagAttributes(text);
//text = EscapeBackslashes(text);
//// Images must come first, because ![foo][f] looks like an anchor.
//text = DoImages(text);
//text = DoAnchors(text);
//// Must come after DoAnchors(), because you can use < and >
//// delimiters in inline links like [this](<url>).
//text = DoAutoLinks(text);
//text = EncodeAmpsAndAngles(text);
//text = DoItalicsAndBold(text);
//text = DoHardBreaks(text);
//return text;
}
static Regex newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled);
static Regex newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled);
static Regex leadingWhitespace = new Regex(@"^[ ]*", RegexOptions.Compiled);
/// <summary>
/// splits on two or more newlines, to form "paragraphs";
/// </summary>
IEnumerable<Block> FormParagraphs(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
// split on two or more newlines
string[] grafs = newlinesMultiple.Split(newlinesLeadingTrailing.Replace(text, ""));
foreach (var g in grafs)
{
yield return Create<Paragraph, Inline>(RunSpanGamut(g));
}
}
static string nestedBracketsPattern;
/// <summary>
/// Reusable pattern to match balanced [brackets]. See Friedl's
/// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
/// </summary>
static string GetNestedBracketsPattern()
{
// in other words [this] and [this[also]] and [this[also[too]]]
// up to nestDepth
if (nestedBracketsPattern == null)
nestedBracketsPattern =
RepeatString(@"
(?> # Atomic matching
[^\[\]]+ # Anything other than brackets
|
\[
", nestDepth) + RepeatString(
@" \]
)*"
, nestDepth);
return nestedBracketsPattern;
}
static string nestedParensPattern;
/// <summary>
/// Reusable pattern to match balanced (parens). See Friedl's
/// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
/// </summary>
static string GetNestedParensPattern()
{
// in other words (this) and (this(also)) and (this(also(too)))
// up to nestDepth
if (nestedParensPattern == null)
nestedParensPattern =
RepeatString(@"
(?> # Atomic matching
[^()\s]+ # Anything other than parens or whitespace
|
\(
", nestDepth) + RepeatString(
@" \)
)*"
, nestDepth);
return nestedParensPattern;
}
static string nestedParensPatternWithWhiteSpace;
/// <summary>
/// Reusable pattern to match balanced (parens), including whitespace. See Friedl's
/// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
/// </summary>
static string GetNestedParensPatternWithWhiteSpace()
{
// in other words (this) and (this(also)) and (this(also(too)))
// up to nestDepth
if (nestedParensPatternWithWhiteSpace == null)
nestedParensPatternWithWhiteSpace =
RepeatString(@"
(?> # Atomic matching
[^()]+ # Anything other than parens
|
\(
", nestDepth) + RepeatString(
@" \)
)*"
, nestDepth);
return nestedParensPatternWithWhiteSpace;
}
static Regex imageInline = new Regex(string.Format(@"
( # wrap whole match in $1
!\[
({0}) # link text = $2
\]
\( # literal paren
[ ]*
({1}) # href = $3
[ ]*
( # $4
(['""]) # quote char = $5
(.*?) # title = $6
\5 # matching quote
#[ ]* # ignore any spaces between closing quote and )
)? # title is optional
\)
)", GetNestedBracketsPattern(), GetNestedParensPatternWithWhiteSpace()),
RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
static Regex anchorInline = new Regex(string.Format(@"
( # wrap whole match in $1
\[
({0}) # link text = $2
\]
\( # literal paren
[ ]*
({1}) # href = $3
[ ]*
( # $4
(['""]) # quote char = $5
(.*?) # title = $6
\5 # matching quote
[ ]* # ignore any spaces between closing quote and )
)? # title is optional
\)
)", GetNestedBracketsPattern(), GetNestedParensPattern()),
RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown images into images
/// </summary>
/// <remarks>
/// ![image alt](url)
/// </remarks>
IEnumerable<Inline> DoImages(string text, Func<string, IEnumerable<Inline>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
return Evaluate(text, imageInline, ImageInlineEvaluator, defaultHandler);
}
Inline ImageInlineEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string linkText = match.Groups[2].Value;
string url = match.Groups[3].Value;
BitmapImage imgSource = null;
try
{
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute) && !System.IO.Path.IsPathRooted(url))
{
url = System.IO.Path.Combine(AssetPathRoot ?? string.Empty, url);
}
imgSource = new BitmapImage(new Uri(url, UriKind.RelativeOrAbsolute));
}
catch (Exception)
{
return new Run("!" + url) { Foreground = Brushes.Red };
}
Image image = new Image { Source = imgSource, Tag = linkText };
if (ImageStyle == null)
{
image.Margin = new Thickness(0);
}
else
{
image.Style = ImageStyle;
}
// Bind size so document is updated when image is downloaded
if (imgSource.IsDownloading)
{
Binding binding = new Binding(nameof(BitmapImage.Width));
binding.Source = imgSource;
binding.Mode = BindingMode.OneWay;
BindingExpressionBase bindingExpression = BindingOperations.SetBinding(image, Image.WidthProperty, binding);
EventHandler downloadCompletedHandler = null;
downloadCompletedHandler = (sender, e) =>
{
imgSource.DownloadCompleted -= downloadCompletedHandler;
bindingExpression.UpdateTarget();
};
imgSource.DownloadCompleted += downloadCompletedHandler;
}
else
{
image.Width = imgSource.Width;
}
return new InlineUIContainer(image);
}
/// <summary>
/// Turn Markdown link shortcuts into hyperlinks
/// </summary>
/// <remarks>
/// [link text](url "title")
/// </remarks>
IEnumerable<Inline> DoAnchors(string text, Func<string, IEnumerable<Inline>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
// Next, inline-style links: [link text](url "optional title") or [link text](url "optional title")
return Evaluate(text, anchorInline, AnchorInlineEvaluator, defaultHandler);
}
Inline AnchorInlineEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string linkText = match.Groups[2].Value;
string url = match.Groups[3].Value;
string title = match.Groups[6].Value;
var result = Create<Hyperlink, Inline>(RunSpanGamut(linkText));
result.Command = HyperlinkCommand;
result.CommandParameter = url;
if (LinkStyle != null)
{
result.Style = LinkStyle;
}
return result;
}
static Regex headerSetext = new Regex(@"
^(.+?)
[ ]*
\n
(=+|-+) # $1 = string of ='s or -'s
[ ]*
\n+",
RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
static Regex headerAtx = new Regex(@"
^(\#{1,6}) # $1 = string of #'s
[ ]*
(.+?) # $2 = Header text
[ ]*
\#* # optional closing #'s (not counted)
\n+",
RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown headers into HTML header tags
/// </summary>
/// <remarks>
/// Header 1
/// ========
///
/// Header 2
/// --------
///
/// # Header 1
/// ## Header 2
/// ## Header 2 with closing hashes ##
/// ...
/// ###### Header 6
/// </remarks>
IEnumerable<Block> DoHeaders(string text, Func<string, IEnumerable<Block>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
return Evaluate<Block>(text, headerSetext, m => SetextHeaderEvaluator(m),
s => Evaluate<Block>(s, headerAtx, m => AtxHeaderEvaluator(m), defaultHandler));
}
Block SetextHeaderEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string header = match.Groups[1].Value;
int level = match.Groups[2].Value.StartsWith("=") ? 1 : 2;
//TODO: Style the paragraph based on the header level
return CreateHeader(level, RunSpanGamut(header.Trim()));
}
Block AtxHeaderEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string header = match.Groups[2].Value;
int level = match.Groups[1].Value.Length;
return CreateHeader(level, RunSpanGamut(header));
}
public Block CreateHeader(int level, IEnumerable<Inline> content)
{
Guard.ArgumentNotNull(content, nameof(content));
var block = Create<Paragraph, Inline>(content);
switch (level)
{
case 1:
if (Heading1Style != null)
{
block.Style = Heading1Style;
}
break;
case 2:
if (Heading2Style != null)
{
block.Style = Heading2Style;
}
break;
case 3:
if (Heading3Style != null)
{
block.Style = Heading3Style;
}
break;
case 4:
if (Heading4Style != null)
{
block.Style = Heading4Style;
}
break;
}
return block;
}
static Regex horizontalRules = new Regex(@"
^[ ]{0,3} # Leading space
([-*_]) # $1: First marker
(?> # Repeated marker group
[ ]{0,2} # Zero, one, or two spaces.
\1 # Marker character
){2,} # Group repeated at least twice
[ ]* # Trailing spaces
$ # End of line.
", RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown horizontal rules into HTML hr tags
/// </summary>
/// <remarks>
/// ***
/// * * *
/// ---
/// - - -
/// </remarks>
IEnumerable<Block> DoHorizontalRules(string text, Func<string, IEnumerable<Block>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
return Evaluate(text, horizontalRules, RuleEvaluator, defaultHandler);
}
Block RuleEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
Line line = new Line();
if (SeparatorStyle == null)
{
line.X2 = 1;
line.StrokeThickness = 1.0;
}
else
{
line.Style = SeparatorStyle;
}
var container = new BlockUIContainer(line);
return container;
}
static string wholeList = string.Format(@"
( # $1 = whole list
( # $2
[ ]{{0,{1}}}
({0}) # $3 = first list item marker
[ ]+
)
(?s:.+?)
( # $4
\z
|
\n{{2,}}
(?=\S)
(?! # Negative lookahead for another list item marker
[ ]*
{0}[ ]+
)
)
)", string.Format("(?:{0}|{1})", markerUL, markerOL), tabWidth - 1);
static Regex listNested = new Regex(@"^" + wholeList,
RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
static Regex listTopLevel = new Regex(@"(?:(?<=\n\n)|\A\n?)" + wholeList,
RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown lists into HTML ul and ol and li tags
/// </summary>
IEnumerable<Block> DoLists(string text, Func<string, IEnumerable<Block>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
// We use a different prefix before nested lists than top-level lists.
// See extended comment in ProcessListItems().
if (listLevel > 0)
return Evaluate(text, listNested, ListEvaluator, defaultHandler);
else
return Evaluate(text, listTopLevel, ListEvaluator, defaultHandler);
}
Block ListEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string list = match.Groups[1].Value;
string listType = Regex.IsMatch(match.Groups[3].Value, markerUL) ? "ul" : "ol";
// Turn double returns into triple returns, so that we can make a
// paragraph for the last item in a list, if necessary:
list = Regex.Replace(list, @"\n{2,}", "\n\n\n");
var resultList = Create<List, ListItem>(ProcessListItems(list, listType == "ul" ? markerUL : markerOL));
resultList.MarkerStyle = listType == "ul" ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal;
return resultList;
}
/// <summary>
/// Process the contents of a single ordered or unordered list, splitting it
/// into individual list items.
/// </summary>
IEnumerable<ListItem> ProcessListItems(string list, string marker)
{
// The listLevel global keeps track of when we're inside a list.
// Each time we enter a list, we increment it; when we leave a list,
// we decrement. If it's zero, we're not in a list anymore.
// We do this because when we're not inside a list, we want to treat
// something like this:
// I recommend upgrading to version
// 8. Oops, now this line is treated
// as a sub-list.
// As a single paragraph, despite the fact that the second line starts
// with a digit-period-space sequence.
// Whereas when we're inside a list (or sub-list), that line will be
// treated as the start of a sub-list. What a kludge, huh? This is
// an aspect of Markdown's syntax that's hard to parse perfectly
// without resorting to mind-reading. Perhaps the solution is to
// change the syntax rules such that sub-lists must start with a
// starting cardinal number; e.g. "1." or "a.".
listLevel++;
try
{
// Trim trailing blank lines:
list = Regex.Replace(list, @"\n{2,}\z", "\n");
string pattern = string.Format(
@"(\n)? # leading line = $1
(^[ ]*) # leading whitespace = $2
({0}) [ ]+ # list marker = $3
((?s:.+?) # list item text = $4
(\n{{1,2}}))
(?= \n* (\z | \2 ({0}) [ ]+))", marker);
var regex = new Regex(pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
var matches = regex.Matches(list);
foreach (Match m in matches)
{
yield return ListItemEvaluator(m);
}
}
finally
{
listLevel--;
}
}
ListItem ListItemEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string item = match.Groups[4].Value;
string leadingLine = match.Groups[1].Value;
if (!String.IsNullOrEmpty(leadingLine) || Regex.IsMatch(item, @"\n{2,}"))
// we could correct any bad indentation here..
return Create<ListItem, Block>(RunBlockGamut(item));
else
{
// recursion for sub-lists
return Create<ListItem, Block>(RunBlockGamut(item));
}
}
static Regex codeSpan = new Regex(@"
(?<!\\) # Character before opening ` can't be a backslash
(`+) # $1 = Opening run of `
(.+?) # $2 = The code block
(?<!`)
\1
(?!`)", RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown `code spans` into HTML code tags
/// </summary>
IEnumerable<Inline> DoCodeSpans(string text, Func<string, IEnumerable<Inline>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
// * You can use multiple backticks as the delimiters if you want to
// include literal backticks in the code span. So, this input:
//
// Just type ``foo `bar` baz`` at the prompt.
//
// Will translate to:
//
// <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
//
// There's no arbitrary limit to the number of backticks you
// can use as delimters. If you need three consecutive backticks
// in your code, use four for delimiters, etc.
//
// * You can use spaces to get literal backticks at the edges:
//
// ... type `` `bar` `` ...
//
// Turns to:
//
// ... type <code>`bar`</code> ...
//
return Evaluate(text, codeSpan, CodeSpanEvaluator, defaultHandler);
}
Inline CodeSpanEvaluator(Match match)
{
Guard.ArgumentNotNull(match, nameof(match));
string span = match.Groups[2].Value;
span = Regex.Replace(span, @"^[ ]*", ""); // leading whitespace
span = Regex.Replace(span, @"[ ]*$", ""); // trailing whitespace
var result = new Run(span);
if (CodeStyle != null)
{
result.Style = CodeStyle;
}
return result;
}
static Regex bold = new Regex(@"(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
static Regex strictBold = new Regex(@"([\W_]|^) (\*\*|__) (?=\S) ([^\r]*?\S[\*_]*) \2 ([\W_]|$)",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
static Regex italic = new Regex(@"(\*|_) (?=\S) (.+?) (?<=\S) \1",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
static Regex strictItalic = new Regex(@"([\W_]|^) (\*|_) (?=\S) ([^\r\*_]*?\S) \2 ([\W_]|$)",
RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled);
/// <summary>
/// Turn Markdown *italics* and **bold** into HTML strong and em tags
/// </summary>
IEnumerable<Inline> DoItalicsAndBold(string text, Func<string, IEnumerable<Inline>> defaultHandler)
{
Guard.ArgumentNotNull(text, nameof(text));
// <strong> must go first, then <em>
if (StrictBoldItalic)
{
return Evaluate<Inline>(text, strictBold, m => BoldEvaluator(m, 3),
s1 => Evaluate<Inline>(s1, strictItalic, m => ItalicEvaluator(m, 3),
s2 => defaultHandler(s2)));
}
else
{
return Evaluate<Inline>(text, bold, m => BoldEvaluator(m, 2),
s1 => Evaluate<Inline>(s1, italic, m => ItalicEvaluator(m, 2),
s2 => defaultHandler(s2)));
}
}
Inline ItalicEvaluator(Match match, int contentGroup)
{
Guard.ArgumentNotNull(match, nameof(match));
var content = match.Groups[contentGroup].Value;
return Create<Italic, Inline>(RunSpanGamut(content));
}
Inline BoldEvaluator(Match match, int contentGroup)
{
Guard.ArgumentNotNull(match, nameof(match));
var content = match.Groups[contentGroup].Value;
return Create<Bold, Inline>(RunSpanGamut(content));
}
static Regex outDent = new Regex(@"^[ ]{1," + tabWidth + @"}", RegexOptions.Multiline | RegexOptions.Compiled);
/// <summary>
/// Remove one level of line-leading spaces
/// </summary>
string Outdent(string block)
{
return outDent.Replace(block, "");
}
/// <summary>
/// convert all tabs to tabWidth spaces;
/// standardizes line endings from DOS (CR LF) or Mac (CR) to UNIX (LF);
/// makes sure text ends with a couple of newlines;
/// removes any blank lines (only spaces) in the text
/// </summary>
string Normalize(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
var output = new StringBuilder(text.Length);
var line = new StringBuilder();
bool valid = false;
for (int i = 0; i < text.Length; i++)
{
switch (text[i])
{
case '\n':
if (valid)
output.Append(line);
output.Append('\n');
line.Length = 0;
valid = false;
break;
case '\r':
if ((i < text.Length - 1) && (text[i + 1] != '\n'))
{
if (valid)
output.Append(line);
output.Append('\n');
line.Length = 0;
valid = false;
}
break;
case '\t':
int width = (tabWidth - line.Length % tabWidth);
for (int k = 0; k < width; k++)
line.Append(' ');
break;
case '\x1A':
break;
default:
if (!valid && text[i] != ' ')
valid = true;
line.Append(text[i]);
break;
}
}
if (valid)
output.Append(line);
output.Append('\n');
// add two newlines to the end before return
return output.Append("\n\n").ToString();
}
/// <summary>
/// this is to emulate what's evailable in PHP
/// </summary>
static string RepeatString(string text, int count)
{
Guard.ArgumentNotNull(text, nameof(text));
var sb = new StringBuilder(text.Length * count);
for (int i = 0; i < count; i++)
sb.Append(text);
return sb.ToString();
}
TResult Create<TResult, TContent>(IEnumerable<TContent> content)
where TResult : IAddChild, new()
{
var result = new TResult();
foreach (var c in content)
{
result.AddChild(c);
}
return result;
}
IEnumerable<T> Evaluate<T>(string text, Regex expression, Func<Match, T> build, Func<string, IEnumerable<T>> rest)
{
Guard.ArgumentNotNull(text, nameof(text));
var matches = expression.Matches(text);
var index = 0;
foreach (Match m in matches)
{
if (m.Index > index)
{
var prefix = text.Substring(index, m.Index - index);
foreach (var t in rest(prefix))
{
yield return t;
}
}
yield return build(m);
index = m.Index + m.Length;
}
if (index < text.Length)
{
var suffix = text.Substring(index, text.Length - index);
foreach (var t in rest(suffix))
{
yield return t;
}
}
}
static Regex eoln = new Regex("\\s+");
public IEnumerable<Inline> DoText(string text)
{
Guard.ArgumentNotNull(text, nameof(text));
var t = eoln.Replace(text, " ");
yield return new Run(t);
}
}
}

Просмотреть файл

@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
namespace GitHub.UI
{
public class MarkdownViewer : FlowDocumentScrollViewer
{
public static readonly DependencyProperty MarkdownProperty =
DependencyProperty.Register("Markdown", typeof(Markdown), typeof(MarkdownViewer), new PropertyMetadata(null));
public static readonly DependencyProperty RawContentProperty =
DependencyProperty.Register("RawContent", typeof(string), typeof(MarkdownViewer), new PropertyMetadata(null, UpdateDocument));
public Markdown Markdown
{
get { return (Markdown)GetValue(MarkdownProperty); }
set { SetValue(MarkdownProperty, value); }
}
public string RawContent
{
get { return (string)GetValue(RawContentProperty); }
set { SetValue(RawContentProperty, value); }
}
public ICommand HyperlinkCommand
{
get { return (ICommand)GetValue(HyperlinkCommandProperty); }
set { SetValue(HyperlinkCommandProperty, value); }
}
public static readonly DependencyProperty HyperlinkCommandProperty =
Markdown.HyperlinkCommandProperty.AddOwner(typeof(MarkdownViewer),
new FrameworkPropertyMetadata(UpdateLinkedProperties));
public Style DocumentStyle
{
get { return (Style)GetValue(DocumentStyleProperty); }
set { SetValue(DocumentStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for DocumentStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DocumentStyleProperty =
Markdown.DocumentStyleProperty.AddOwner(typeof(MarkdownViewer),
new FrameworkPropertyMetadata(UpdateLinkedProperties));
public Style LinkStyle
{
get { return (Style)GetValue(LinkStyleProperty); }
set { SetValue(LinkStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for LinkStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LinkStyleProperty =
Markdown.LinkStyleProperty.AddOwner(typeof(MarkdownViewer),
new FrameworkPropertyMetadata(UpdateLinkedProperties));
public MarkdownViewer()
{
Markdown = new Markdown();
}
static void UpdateLinkedProperties(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var c = (MarkdownViewer)d;
c.Markdown?.SetCurrentValue(e.Property, e.NewValue);
}
static void UpdateDocument(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MarkdownViewer)d;
var raw = e.NewValue as string;
control.SetCurrentValue(DocumentProperty, raw != null ? control.Markdown?.Transform(raw) : null);
}
}
}

Просмотреть файл

@ -79,8 +79,6 @@
<Compile Include="Behaviours\ClosePopupAction.cs" />
<Compile Include="Behaviours\OpenPopupAction.cs" />
<Compile Include="Controls\ComboBoxes\LinkDropDown.cs" />
<Compile Include="Controls\Markdown.cs" />
<Compile Include="Controls\MarkdownViewer.cs" />
<Compile Include="Controls\Octicons\OcticonPaths.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>

Просмотреть файл

@ -190,6 +190,10 @@
</Page>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\submodules\markdig-wpf\src\Markdig.Wpf\Markdig.Wpf.csproj">
<Project>{01e40fb5-b4e9-489d-98d5-4771ddc2d1d7}</Project>
<Name>Markdig.Wpf</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\octokit.net\Octokit\Octokit.csproj">
<Project>{08dd4305-7787-4823-a53f-4d0f725a07f3}</Project>
<Name>Octokit</Name>

Просмотреть файл

@ -8,6 +8,7 @@
xmlns:sampleData="clr-namespace:GitHub.SampleData;assembly=GitHub.App"
xmlns:ui="clr-namespace:GitHub.UI;assembly=GitHub.UI"
xmlns:automation="clr-namespace:GitHub.UI.TestAutomation;assembly=GitHub.UI"
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
Background="Beige"
d:DataContext="{d:DesignInstance Type=sampleData:InfoPanelDesigner, IsDesignTimeCreatable=True}"
d:DesignHeight="300"
@ -51,17 +52,13 @@
Foreground="Black"
Icon="x"/>
<ui:MarkdownViewer x:Name="document"
Margin="8,0"
VerticalAlignment="Top"
DockPanel.Dock="Top"
DocumentStyle="{StaticResource DocumentStyle}"
Foreground="Black"
LinkStyle="{StaticResource LinkStyle}"
RawContent="{Binding Message}"
HyperlinkCommand="{Binding LinkCommand}"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
<markdig:MarkdownViewer x:Name="document"
Margin="8,0"
VerticalAlignment="Top"
DockPanel.Dock="Top"
Foreground="Black"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
</DockPanel>
</UserControl>

Просмотреть файл

@ -573,6 +573,10 @@
<IncludeOutputGroupsInVSIX>BuiltProjectOutputGroup;GetCopyToOutputDirectoryItems;DebugSymbolsProjectOutputGroup;</IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly>DebugSymbolsProjectOutputGroup;</IncludeOutputGroupsInVSIXLocalOnly>
</ProjectReference>
<ProjectReference Include="..\..\submodules\markdig-wpf\src\Markdig.Wpf\Markdig.Wpf.csproj">
<Project>{01e40fb5-b4e9-489d-98d5-4771ddc2d1d7}</Project>
<Name>Markdig.Wpf</Name>
</ProjectReference>
<ProjectReference Include="..\..\submodules\octokit.net\Octokit.Reactive\Octokit.Reactive.csproj">
<Project>{674b69b8-0780-4d54-ae2b-c15821fa51cb}</Project>
<Name>Octokit.Reactive</Name>
@ -738,4 +742,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

Просмотреть файл

@ -11,6 +11,7 @@
xmlns:uir="clr-namespace:GitHub.UI;assembly=GitHub.UI.Reactive"
xmlns:vm="clr-namespace:GitHub.ViewModels;assembly=GitHub.App"
xmlns:prop="clr-namespace:GitHub.VisualStudio.UI;assembly=GitHub.VisualStudio.UI"
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
Background="{DynamicResource GitHubVsToolWindowBackground}"
Foreground="{DynamicResource GitHubVsWindowText}"
DataContext="{Binding ViewModel}"
@ -52,21 +53,6 @@
<Setter Property="Fill" Value="{DynamicResource GitHubHeaderSeparatorBrush}"/>
</Style>
<Style x:Key="DocumentStyle" TargetType="FlowDocument">
<Setter Property="FontFamily" Value="Segoe UI"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextAlignment" Value="Left"/>
<Setter Property="PagePadding" Value="0"/>
<Style.Resources>
<Style TargetType="Image">
<Setter Property="Stretch" Value="UniformToFill" />
<Setter Property="StretchDirection" Value="DownOnly" />
<Setter Property="MaxWidth" Value="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type ui:MarkdownViewer}}}"/>
</Style>
</Style.Resources>
</Style>
<!-- TODO Fix this: here we change the color of TextBlock depending on the label.
It's a hack, it will break with localization -->
<Style x:Key="StateIndicator" TargetType="TextBlock">
@ -261,12 +247,10 @@
</StackPanel>
<!-- PR Body -->
<ui:MarkdownViewer Name="bodyMarkdown"
Grid.Row="1"
Margin="2 10 10 6"
DocumentStyle="{StaticResource DocumentStyle}"
RawContent="{Binding Body}"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
<markdig:MarkdownViewer Name="bodyMarkdown"
Grid.Row="1"
Margin="2 10 10 6"
Markdown="{Binding Body}"/>
<!-- View on github link -->
<ui:GitHubActionLink Grid.Column="2" Grid.Row="2" Margin="0 6" Command="{Binding OpenOnGitHub}">

@ -0,0 +1 @@
Subproject commit 8087b251d7e7429bd4e6769de8babf4b26c86c83