RunBlockGamut(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("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
+ //// tags around block-level tags.
+ //text = HashHTMLBlocks(text);
+
+ //text = FormParagraphs(text);
+
+ //return text;
+ }
+
+ ///
+ /// Perform transformations that occur *within* block-level tags like paragraphs, headers, and list items.
+ ///
+ private IEnumerable RunSpanGamut(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("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]().
+ //text = DoAutoLinks(text);
+
+ //text = EncodeAmpsAndAngles(text);
+ //text = DoItalicsAndBold(text);
+ //text = DoHardBreaks(text);
+
+ //return text;
+ }
+
+ private static Regex _newlinesLeadingTrailing = new Regex(@"^\n+|\n+\z", RegexOptions.Compiled);
+ private static Regex _newlinesMultiple = new Regex(@"\n{2,}", RegexOptions.Compiled);
+ private static Regex _leadingWhitespace = new Regex(@"^[ ]*", RegexOptions.Compiled);
+
+ ///
+ /// splits on two or more newlines, to form "paragraphs";
+ ///
+ private IEnumerable FormParagraphs(string text)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ // split on two or more newlines
+ string[] grafs = _newlinesMultiple.Split(_newlinesLeadingTrailing.Replace(text, ""));
+
+ foreach (var g in grafs)
+ {
+ yield return Create(RunSpanGamut(g));
+ }
+ }
+
+ private static string _nestedBracketsPattern;
+
+ ///
+ /// Reusable pattern to match balanced [brackets]. See Friedl's
+ /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+ ///
+ private static string GetNestedBracketsPattern()
+ {
+ // in other words [this] and [this[also]] and [this[also[too]]]
+ // up to _nestDepth
+ if (_nestedBracketsPattern == null)
+ _nestedBracketsPattern =
+ RepeatString(@"
+ (?> # Atomic matching
+ [^\[\]]+ # Anything other than brackets
+ |
+ \[
+ ", _nestDepth) + RepeatString(
+ @" \]
+ )*"
+ , _nestDepth);
+ return _nestedBracketsPattern;
+ }
+
+ private static string _nestedParensPattern;
+
+ ///
+ /// Reusable pattern to match balanced (parens). See Friedl's
+ /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+ ///
+ private static string GetNestedParensPattern()
+ {
+ // in other words (this) and (this(also)) and (this(also(too)))
+ // up to _nestDepth
+ if (_nestedParensPattern == null)
+ _nestedParensPattern =
+ RepeatString(@"
+ (?> # Atomic matching
+ [^()\s]+ # Anything other than parens or whitespace
+ |
+ \(
+ ", _nestDepth) + RepeatString(
+ @" \)
+ )*"
+ , _nestDepth);
+ return _nestedParensPattern;
+ }
+
+ private static string _nestedParensPatternWithWhiteSpace;
+
+ ///
+ /// Reusable pattern to match balanced (parens), including whitespace. See Friedl's
+ /// "Mastering Regular Expressions", 2nd Ed., pp. 328-331.
+ ///
+ private 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;
+ }
+
+ private 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);
+
+ private 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);
+
+ ///
+ /// Turn Markdown images into images
+ ///
+ ///
+ /// 
+ ///
+ private IEnumerable DoImages(string text, Func> defaultHandler)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ return Evaluate(text, _imageInline, ImageInlineEvaluator, defaultHandler);
+ }
+
+ private Inline ImageInlineEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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);
+ }
+
+ ///
+ /// Turn Markdown link shortcuts into hyperlinks
+ ///
+ ///
+ /// [link text](url "title")
+ ///
+ private IEnumerable DoAnchors(string text, Func> defaultHandler)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ // Next, inline-style links: [link text](url "optional title") or [link text](url "optional title")
+ return Evaluate(text, _anchorInline, AnchorInlineEvaluator, defaultHandler);
+ }
+
+ private Inline AnchorInlineEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("match");
+ }
+
+ string linkText = match.Groups[2].Value;
+ string url = match.Groups[3].Value;
+ string title = match.Groups[6].Value;
+
+ var result = Create(RunSpanGamut(linkText));
+ result.Command = HyperlinkCommand;
+ result.CommandParameter = url;
+ if (LinkStyle != null)
+ {
+ result.Style = LinkStyle;
+ }
+
+ return result;
+ }
+
+ private static Regex _headerSetext = new Regex(@"
+ ^(.+?)
+ [ ]*
+ \n
+ (=+|-+) # $1 = string of ='s or -'s
+ [ ]*
+ \n+",
+ RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+ private 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);
+
+ ///
+ /// Turn Markdown headers into HTML header tags
+ ///
+ ///
+ /// Header 1
+ /// ========
+ ///
+ /// Header 2
+ /// --------
+ ///
+ /// # Header 1
+ /// ## Header 2
+ /// ## Header 2 with closing hashes ##
+ /// ...
+ /// ###### Header 6
+ ///
+ private IEnumerable DoHeaders(string text, Func> defaultHandler)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ return Evaluate(text, _headerSetext, m => SetextHeaderEvaluator(m),
+ s => Evaluate(s, _headerAtx, m => AtxHeaderEvaluator(m), defaultHandler));
+ }
+
+ private Block SetextHeaderEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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()));
+ }
+
+ private Block AtxHeaderEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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 content)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException("content");
+ }
+
+ var block = Create(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;
+ }
+
+ private 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);
+
+ ///
+ /// Turn Markdown horizontal rules into HTML hr tags
+ ///
+ ///
+ /// ***
+ /// * * *
+ /// ---
+ /// - - -
+ ///
+ private IEnumerable DoHorizontalRules(string text, Func> defaultHandler)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("text");
+ }
+
+ return Evaluate(text, _horizontalRules, RuleEvaluator, defaultHandler);
+ }
+
+ private Block RuleEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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;
+ }
+
+ private 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);
+
+ private static Regex _listNested = new Regex(@"^" + _wholeList,
+ RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+ private static Regex _listTopLevel = new Regex(@"(?:(?<=\n\n)|\A\n?)" + _wholeList,
+ RegexOptions.Multiline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
+
+ ///
+ /// Turn Markdown lists into HTML ul and ol and li tags
+ ///
+ private IEnumerable DoLists(string text, Func> defaultHandler)
+ {
+ if (text == null)
+ {
+ throw new ArgumentNullException("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);
+ }
+
+ private Block ListEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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(ProcessListItems(list, listType == "ul" ? _markerUL : _markerOL));
+
+ resultList.MarkerStyle = listType == "ul" ? TextMarkerStyle.Disc : TextMarkerStyle.Decimal;
+
+ return resultList;
+ }
+
+ ///
+ /// Process the contents of a single ordered or unordered list, splitting it
+ /// into individual list items.
+ ///
+ private IEnumerable 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--;
+ }
+ }
+
+ private ListItem ListItemEvaluator(Match match)
+ {
+ if (match == null)
+ {
+ throw new ArgumentNullException("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(RunBlockGamut(item));
+ else
+ {
+ // recursion for sub-lists
+ return Create(RunBlockGamut(item));
+ }
+ }
+
+ private static Regex _codeSpan = new Regex(@"
+ (?
+ /// Turn Markdown `code spans` into HTML code tags
+ ///