/// <summary> /// Parsing helper method. /// </summary> private static void AppendTextToListItem(ListItemBlock listItem, string markdown, int start, int end, bool newLine = false) { ListItemBuilder listItemBuilder = null; if (listItem.Blocks.Count > 0) { listItemBuilder = listItem.Blocks[listItem.Blocks.Count - 1] as ListItemBuilder; } if (listItemBuilder == null) { // Add a new block. listItemBuilder = new ListItemBuilder(); listItem.Blocks.Add(listItemBuilder); } var builder = listItemBuilder.Builder; if (builder.Length >= 2 && ParseHelpers.IsMarkdownWhiteSpace(builder[builder.Length - 2]) && ParseHelpers.IsMarkdownWhiteSpace(builder[builder.Length - 1])) { builder.Length -= 2; builder.AppendLine(); } else if (builder.Length > 0) { builder.Append(' '); } if (newLine) { builder.Append(Environment.NewLine); } builder.Append(markdown.Substring(start, end - start)); }
/// <summary> /// Parses a list block. /// </summary> /// <param name="markdown"> The markdown text. </param> /// <param name="start"> The location of the first character in the block. </param> /// <param name="maxEnd"> The location to stop parsing. </param> /// <param name="quoteDepth"> The current nesting level for block quoting. </param> /// <param name="actualEnd"> Set to the end of the block when the return value is non-null. </param> /// <returns> A parsed list block, or <c>null</c> if this is not a list block. </returns> internal static ListBlock Parse(string markdown, int start, int maxEnd, int quoteDepth, out int actualEnd) { var russianDolls = new List <NestedListInfo>(); var russianDollIndex = -1; var previousLineWasBlank = false; var inCodeBlock = false; ListItemBlock currentListItem = null; actualEnd = start; foreach (var lineInfo in Common.ParseLines(markdown, start, maxEnd, quoteDepth)) { // Is this line blank? if (lineInfo.IsLineBlank) { // The line is blank, which means the next line which contains text may end the list (or it may not...). previousLineWasBlank = true; } else { // Does the line contain a list item? ListItemPreamble listItemPreamble = null; if (lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine < (russianDollIndex + 2) * 4) { listItemPreamble = ParseItemPreamble(markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine); } if (listItemPreamble != null) { // Yes, this line contains a list item. // Determining the nesting level is done as follows: // 1. If this is the first line, then the list is not nested. // 2. If the number of spaces at the start of the line is equal to that of // an existing list, then the nesting level is the same as that list. // 3. Otherwise, if the number of spaces is 0-4, then the nesting level // is one level deep. // 4. Otherwise, if the number of spaces is 5-8, then the nesting level // is two levels deep (but no deeper than one level more than the // previous list item). // 5. Etcetera. ListBlock listToAddTo = null; var spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; russianDollIndex = russianDolls.FindIndex(rd => rd.SpaceCount == spaceCount); if (russianDollIndex >= 0) { // Add the new list item to an existing list. listToAddTo = russianDolls[russianDollIndex].List; // Don't add new list items to items higher up in the list. russianDolls.RemoveRange(russianDollIndex + 1, russianDolls.Count - (russianDollIndex + 1)); } else { russianDollIndex = Math.Max(1, 1 + ((spaceCount - 1) / 4)); if (russianDollIndex < russianDolls.Count) { // Add the new list item to an existing list. listToAddTo = russianDolls[russianDollIndex].List; // Don't add new list items to items higher up in the list. russianDolls.RemoveRange(russianDollIndex + 1, russianDolls.Count - (russianDollIndex + 1)); } else { // Create a new list. listToAddTo = new ListBlock { Style = listItemPreamble.Style, Items = new List <ListItemBlock>() }; if (russianDolls.Count > 0) { currentListItem.Blocks.Add(listToAddTo); } russianDollIndex = russianDolls.Count; russianDolls.Add(new NestedListInfo { List = listToAddTo, SpaceCount = spaceCount }); } } // Add a new list item. currentListItem = new ListItemBlock() { Blocks = new List <MarkdownBlock>() }; listToAddTo.Items.Add(currentListItem); // Add the rest of the line to the builder. AppendTextToListItem(currentListItem, markdown, listItemPreamble.ContentStartPos, lineInfo.EndOfLine); } else { // No, this line contains text. // Is there even a list in progress? if (currentListItem == null) { actualEnd = start; return(null); } // This is the start of a new paragraph. var spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; if (spaceCount == 0) { break; } russianDollIndex = Math.Min(russianDollIndex, (spaceCount - 1) / 4); var linestart = Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + ((russianDollIndex + 1) * 4)); // 0 spaces = end of the list. // 1-4 spaces = first level. // 5-8 spaces = second level, etc. if (previousLineWasBlank) { var listToAddTo = russianDolls[russianDollIndex].List; currentListItem = listToAddTo.Items[listToAddTo.Items.Count - 1]; ListItemBuilder builder; // Prevents new Block creation if still in a Code Block. if (!inCodeBlock) { builder = new ListItemBuilder(); currentListItem.Blocks.Add(builder); } else { // This can only ever be a ListItemBuilder, so it is not a null reference. builder = currentListItem.Blocks.Last() as ListItemBuilder; // Make up for the escaped NewLines. builder.Builder.AppendLine(); builder.Builder.AppendLine(); } AppendTextToListItem(currentListItem, markdown, linestart, lineInfo.EndOfLine); } else { // Inline text. Ignores the 4 spaces that are used to continue the list. AppendTextToListItem(currentListItem, markdown, linestart, lineInfo.EndOfLine, true); } } // Check for Closing Code Blocks. if (currentListItem.Blocks.Last() is ListItemBuilder currentBlock) { var blockmatchcount = Regex.Matches(currentBlock.Builder.ToString(), "```").Count; if (blockmatchcount > 0 && blockmatchcount % 2 != 0) { inCodeBlock = true; } else { inCodeBlock = false; } } // The line was not blank. previousLineWasBlank = false; } // Go to the next line. actualEnd = lineInfo.EndOfLine; } var result = russianDolls[0].List; ReplaceStringBuilders(result); return(result); }