Last active
March 17, 2025 00:31
-
-
Save JamesNK/4d4fbef86cdde2ab54df8bae8421bed6 to your computer and use it in GitHub Desktop.
CompletePartialElements.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void CompletePartialElements(MarkdownDocument document) | |
{ | |
var lastChild = document.LastChild; | |
while (lastChild is ContainerBlock containerBlock) | |
{ | |
lastChild = containerBlock.LastChild; | |
} | |
if (lastChild is not LeafBlock leafBlock) | |
{ | |
return; | |
} | |
// "Level: 2" means '-' was used. | |
if (leafBlock is HeadingBlock { IsSetext: true, Level: 2, HeaderCharCount: 1 } setext) | |
{ | |
var paragraph = new ParagraphBlock(); | |
paragraph.Inline = new ContainerInline(); | |
setext.Inline?.EmbraceChildrenBy(paragraph.Inline); | |
var parent = setext.Parent!; | |
parent[parent.IndexOf(setext)] = paragraph; | |
leafBlock = paragraph; | |
} | |
if (leafBlock.Inline?.LastChild is LiteralInline literal) | |
{ | |
// A LiteralInline with a backtick character is a potential CodeInline that wasn't closed. | |
var indexOfBacktick = literal.Content.IndexOf('`'); | |
if (indexOfBacktick >= 0) | |
{ | |
// But it could also happen if the backticks were escaped. | |
if (literal.Content.AsSpan().Count('`') == 1) | |
{ | |
// "Text with `a code inline" => "Text with `a code inline`" | |
//var originalLength = literal.Content.Length; | |
// Shorten the existing text. -1 to exclude the backtick. | |
literal.Content.End = indexOfBacktick - 1; | |
// Insert a CodeInline with the remainder. +1 and -1 to account for the backtick. | |
var code = literal.Content.Text.Substring(indexOfBacktick + 1, originalLength - literal.Content.Length - 1); | |
literal.InsertAfter(new CodeInline(code)); | |
return; | |
} | |
} | |
var previousSibling = literal.PreviousSibling; | |
// Handle unclosed bold/italic that don't yet have any following content. | |
if (previousSibling is null && IsEmphasisStart(literal.Content.AsSpan())) | |
{ | |
literal.Remove(); | |
return; | |
} | |
if (previousSibling is EmphasisInline) | |
{ | |
// Handle cases like "**_foo_ and bar" by skipping the _foo_ emphasis. | |
previousSibling = previousSibling.PreviousSibling; | |
} | |
if (previousSibling is LiteralInline previousInline) | |
{ | |
var content = previousInline.Content.AsSpan(); | |
// Unclosed bold/italic (EmphasisInline)? | |
// Note that this doesn't catch cases with mixed opening chars, e.g. "**_text" | |
if (IsEmphasisStart(content)) | |
{ | |
literal.Remove(); | |
var emphasis = new EmphasisInline(); | |
emphasis.DelimiterChar = '*'; | |
emphasis.DelimiterCount = previousInline.Content.Length; | |
previousInline.ReplaceBy(emphasis); | |
if (emphasis.DelimiterCount <= 2) | |
{ | |
// Just * or ** | |
emphasis.AppendChild(literal); | |
} | |
else | |
{ | |
// E.g. "***text", which we need to turn into nested <em><strong>text</strong></em> | |
emphasis.DelimiterCount = 1; | |
var nestedStrong = new EmphasisInline(); | |
nestedStrong.DelimiterChar = emphasis.DelimiterChar; | |
nestedStrong.DelimiterCount = 2; | |
nestedStrong.AppendChild(literal); | |
emphasis.AppendChild(nestedStrong); | |
} | |
if (emphasis.NextSibling is EmphasisInline nextSibling) | |
{ | |
// This is the EmphasisInline we've skipped before. Fix the ordering. | |
// "**_foo_ and bar" is currently "** and bar**_foo_". | |
// Move the skipped emphasis to be the first child of the node we've generated. | |
nextSibling.Remove(); | |
emphasis.FirstChild!.InsertBefore(nextSibling); | |
} | |
return; | |
} | |
else if (content is "[") | |
{ | |
// In-progress link, e.g. [text](http:// | |
literal.Remove(); | |
previousInline.Remove(); | |
} | |
} | |
} | |
else if (leafBlock.Inline?.LastChild is LinkDelimiterInline linkDelimiterInline) | |
{ | |
// In-progress link, e.g. [text, or [text] | |
linkDelimiterInline.Remove(); | |
} | |
else if (leafBlock.Inline?.LastChild is LinkInline linkInline) | |
{ | |
// In-progress link, e.g. [text](http:// | |
if (!linkInline.IsClosed) | |
{ | |
linkInline.Remove(); | |
} | |
} | |
static bool IsEmphasisStart(ReadOnlySpan<char> text) | |
{ | |
return text is "*" or "**" or "***" or "_" or "__" or "___"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment