/// <summary> /// Verifies if the link is valid, before processing into a link, or plain text. /// </summary> /// <param name="element"> The parsed inline element to render. </param> /// <param name="context"> Persistent state. </param> protected void CheckRenderMarkdownLink(MarkdownLinkInline element, IRenderContext context) { // Avoid processing when link text is empty. if (element.Inlines.Count == 0) { return; } // Attempt to resolve references. element.ResolveReference(Document); if (element.Url == null) { // The element couldn't be resolved, just render it as text. RenderInlineChildren(element.Inlines, context); return; } foreach (MarkdownInline inline in element.Inlines) { if (inline is ImageInline imageInline) { // this is an image, create Image. if (!string.IsNullOrEmpty(imageInline.ReferenceId)) { imageInline.ResolveReference(Document); } imageInline.Url = element.Url; RenderImage(imageInline, context); return; } } RenderMarkdownLink(element, context); }
static Common() { BoldItalicTextInline.AddTripChars(_triggerList); BoldTextInline.AddTripChars(_triggerList); ItalicTextInline.AddTripChars(_triggerList); MarkdownLinkInline.AddTripChars(_triggerList); HyperlinkInline.AddTripChars(_triggerList); CommentInline.AddTripChars(_triggerList); StrikethroughTextInline.AddTripChars(_triggerList); SuperscriptTextInline.AddTripChars(_triggerList); SubscriptTextInline.AddTripChars(_triggerList); CodeInline.AddTripChars(_triggerList); ImageInline.AddTripChars(_triggerList); EmojiInline.AddTripChars(_triggerList); LinkAnchorInline.AddTripChars(_triggerList); // Create an array of characters to search against using IndexOfAny. _tripCharacters = _triggerList.Select(trigger => trigger.FirstChar).Distinct().ToArray(); }
/// <summary> /// Attempts to parse a markdown link e.g. "[](http://www.reddit.com)". /// </summary> /// <param name="markdown"> The markdown text. </param> /// <param name="start"> The location to start parsing. </param> /// <param name="maxEnd"> The location to stop parsing. </param> /// <returns> A parsed markdown link, or <c>null</c> if this is not a markdown link. </returns> internal static InlineParseResult Parse(string markdown, int start, int maxEnd) { // Expect a '[' character. if (start == maxEnd || markdown[start] != '[') { return(null); } // Find the ']' character, keeping in mind that [test [0-9]](http://www.test.com) is allowed. int linkTextOpen = start + 1; int pos = linkTextOpen; int linkTextClose; int openSquareBracketCount = 0; while (true) { linkTextClose = markdown.IndexOfAny(new char[] { '[', ']' }, pos, maxEnd - pos); if (linkTextClose == -1) { return(null); } if (markdown[linkTextClose] == '[') { openSquareBracketCount++; } else if (openSquareBracketCount > 0) { openSquareBracketCount--; } else { break; } pos = linkTextClose + 1; } // Skip whitespace. pos = linkTextClose + 1; while (pos < maxEnd && ParseHelpers.IsMarkdownWhiteSpace(markdown[pos])) { pos++; } if (pos == maxEnd) { return(null); } // Expect the '(' character or the '[' character. int linkOpen = pos; if (markdown[pos] == '(') { // Skip whitespace. linkOpen++; while (linkOpen < maxEnd && ParseHelpers.IsMarkdownWhiteSpace(markdown[linkOpen])) { linkOpen++; } // Find the ')' character. pos = linkOpen; int linkClose = -1; var openParenthesis = 0; while (pos < maxEnd) { if (markdown[pos] == ')') { if (openParenthesis == 0) { linkClose = pos; break; } else { openParenthesis--; } } if (markdown[pos] == '(') { openParenthesis++; } pos++; } if (pos >= maxEnd) { return(null); } int end = linkClose + 1; // Skip whitespace backwards. while (linkClose > linkOpen && ParseHelpers.IsMarkdownWhiteSpace(markdown[linkClose - 1])) { linkClose--; } // If there is no text whatsoever, then this is not a valid link. if (linkOpen == linkClose) { return(null); } // Check if there is tooltip text. string url; string tooltip = null; bool lastUrlCharIsDoubleQuote = markdown[linkClose - 1] == '"'; int tooltipStart = Common.IndexOf(markdown, " \"", linkOpen, linkClose - 1); if (tooltipStart == linkOpen) { return(null); } if (lastUrlCharIsDoubleQuote && tooltipStart != -1) { // Extract the URL (resolving any escape sequences). url = TextRunInline.ResolveEscapeSequences(markdown, linkOpen, tooltipStart).TrimEnd(' ', '\t', '\r', '\n'); tooltip = markdown.Substring(tooltipStart + 2, (linkClose - 1) - (tooltipStart + 2)); } else { // Extract the URL (resolving any escape sequences). url = TextRunInline.ResolveEscapeSequences(markdown, linkOpen, linkClose); } // Check the URL is okay. if (!url.IsEmail()) { if (!Common.IsUrlValid(url)) { return(null); } } else { tooltip = url = $"mailto:{url}"; } // We found a regular stand-alone link. var result = new MarkdownLinkInline { Inlines = Common.ParseInlineChildren(markdown, linkTextOpen, linkTextClose, ignoreLinks: true), Url = url.Replace(" ", "%20", StringComparison.Ordinal), Tooltip = tooltip }; return(new InlineParseResult(result, start, end)); } else if (markdown[pos] == '[') { // Find the ']' character. int linkClose = Common.IndexOf(markdown, ']', pos + 1, maxEnd); if (linkClose == -1) { return(null); } // We found a reference-style link. var result = new MarkdownLinkInline { Inlines = Common.ParseInlineChildren(markdown, linkTextOpen, linkTextClose, ignoreLinks: true), ReferenceId = markdown.Substring(linkOpen + 1, linkClose - (linkOpen + 1)) }; if (string.IsNullOrEmpty(result.ReferenceId)) { result.ReferenceId = markdown.Substring(linkTextOpen, linkTextClose - linkTextOpen); } return(new InlineParseResult(result, start, linkClose + 1)); } return(null); }
/// <summary> /// Renders a link element /// </summary> /// <param name="element"> The parsed inline element to render. </param> /// <param name="context"> Persistent state. </param> protected override void RenderMarkdownLink(MarkdownLinkInline element, IRenderContext context) { var localContext = context as InlineRenderContext; if (localContext == null) { throw new RenderContextIncorrectException(); } // HACK: Superscript is not allowed within a hyperlink. But if we switch it around, so // that the superscript is outside the hyperlink, then it will render correctly. // This assumes that the entire hyperlink is to be rendered as superscript. if (AllTextIsSuperscript(element) == false) { // Regular ol' hyperlink. var link = new Hyperlink(); // Register the link LinkRegister.RegisterNewHyperLink(link, element.Url); // Remove superscripts. RemoveSuperscriptRuns(element, insertCaret: true); // Render the children into the link inline. var childContext = new InlineRenderContext(link.Inlines, context) { Parent = link, WithinHyperlink = true }; if (localContext.OverrideForeground) { link.Foreground = localContext.Foreground; } else if (LinkForeground != null) { link.Foreground = LinkForeground; } RenderInlineChildren(element.Inlines, childContext); context.TrimLeadingWhitespace = childContext.TrimLeadingWhitespace; ToolTipService.SetToolTip(link, element.Tooltip ?? element.Url); // Add it to the current inlines localContext.InlineCollection.Add(link); } else { // THE HACK IS ON! // Create a fake superscript element. var fakeSuperscript = new SuperscriptTextInline { Inlines = new List <MarkdownInline> { element } }; // Remove superscripts. RemoveSuperscriptRuns(element, insertCaret: false); // Now render it. RenderSuperscriptRun(fakeSuperscript, context); } }
/// <summary> /// Finds the next inline element by matching trip chars and verifying the match. /// </summary> /// <param name="markdown"> The markdown text to parse. </param> /// <param name="start"> The position to start parsing. </param> /// <param name="end"> The position to stop parsing. </param> /// <param name="ignoreLinks"> Indicates whether to parse links. </param> /// <returns>Returns the next element</returns> private static InlineParseResult FindNextInlineElement(string markdown, int start, int end, bool ignoreLinks) { // Search for the next inline sequence. for (int pos = start; pos < end; pos++) { // IndexOfAny should be the fastest way to skip characters we don't care about. pos = markdown.IndexOfAny(_tripCharacters, pos, end - pos); if (pos < 0) { break; } // Find the trigger(s) that matched. char currentChar = markdown[pos]; foreach (InlineTripCharHelper currentTripChar in _triggerList) { // Check if our current char matches the suffix char. if (currentChar == currentTripChar.FirstChar) { // Don't match if the previous character was a backslash. if (pos > start && markdown[pos - 1] == '\\') { continue; } // If we are here we have a possible match. Call into the inline class to verify. InlineParseResult parseResult = null; switch (currentTripChar.Method) { case InlineParseMethod.BoldItalic: parseResult = BoldItalicTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.Comment: parseResult = CommentInline.Parse(markdown, pos, end); break; case InlineParseMethod.LinkReference: parseResult = LinkAnchorInline.Parse(markdown, pos, end); break; case InlineParseMethod.Bold: parseResult = BoldTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.Italic: parseResult = ItalicTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.MarkdownLink: if (!ignoreLinks) { parseResult = MarkdownLinkInline.Parse(markdown, pos, end); } break; case InlineParseMethod.AngleBracketLink: if (!ignoreLinks) { parseResult = HyperlinkInline.ParseAngleBracketLink(markdown, pos, end); } break; case InlineParseMethod.Url: if (!ignoreLinks) { parseResult = HyperlinkInline.ParseUrl(markdown, pos, end); } break; case InlineParseMethod.RedditLink: if (!ignoreLinks) { parseResult = HyperlinkInline.ParseRedditLink(markdown, pos, end); } break; case InlineParseMethod.PartialLink: if (!ignoreLinks) { parseResult = HyperlinkInline.ParsePartialLink(markdown, pos, end); } break; case InlineParseMethod.Email: if (!ignoreLinks) { parseResult = HyperlinkInline.ParseEmailAddress(markdown, start, pos, end); } break; case InlineParseMethod.Strikethrough: parseResult = StrikethroughTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.Superscript: parseResult = SuperscriptTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.Subscript: parseResult = SubscriptTextInline.Parse(markdown, pos, end); break; case InlineParseMethod.Code: parseResult = CodeInline.Parse(markdown, pos, end); break; case InlineParseMethod.Image: parseResult = ImageInline.Parse(markdown, pos, end); break; case InlineParseMethod.Emoji: parseResult = EmojiInline.Parse(markdown, pos, end); break; } if (parseResult != null) { return(parseResult); } } } } // If we didn't find any elements we have a normal text block. // Let us consume the entire range. return(new InlineParseResult(TextRunInline.Parse(markdown, start, end), start, end)); }