/// <summary> /// Parsing helper. /// </summary> /// <param name="list"></param> /// <returns> <c>true</c> if any of the list items were parsed using the block parser. </returns> private static bool ReplaceStringBuilders(ListBlock list) { bool usedBlockParser = false; foreach (var listItem in list.Items) { // Use the inline parser if there is one paragraph, use the block parser otherwise. var useBlockParser = listItem.Blocks.Count(block => block.Type == MarkdownBlockType.ListItemBuilder) > 1; // Recursively replace any child lists. foreach (var block in listItem.Blocks) { if (block is ListBlock && ReplaceStringBuilders((ListBlock)block)) { useBlockParser = true; } } // Parse the text content of the list items. var newBlockList = new List <MarkdownBlock>(); foreach (var block in listItem.Blocks) { if (block is ListItemBuilder) { var blockText = ((ListItemBuilder)block).Builder.ToString(); if (blockText.TrimStart().StartsWith("```")) { useBlockParser = true; } if (useBlockParser) { // Parse the list item as a series of blocks. int actualEnd; newBlockList.AddRange(MarkdownDocument.Parse(blockText, 0, blockText.Length, quoteDepth: 0, actualEnd: out actualEnd)); usedBlockParser = true; } else { // Don't allow blocks. var paragraph = new ParagraphBlock(); paragraph.Inlines = Common.ParseInlineChildren(blockText, 0, blockText.Length); newBlockList.Add(paragraph); } } else { newBlockList.Add(block); } } listItem.Blocks = newBlockList; } return(usedBlockParser); }
/// <summary> /// Parsing helper. /// </summary> /// <param name="list"></param> /// <returns> <c>true</c> if any of the list items were parsed using the block parser. </returns> private static bool ReplaceStringBuilders(ListBlock list) { bool usedBlockParser = false; foreach (var listItem in list.Items) { // Use the inline parser if there is one paragraph, use the block parser otherwise. var useBlockParser = listItem.Blocks.Count(block => block.Type == MarkdownBlockType.ListItemBuilder) > 1; // Recursively replace any child lists. foreach (var block in listItem.Blocks) if (block is ListBlock && ReplaceStringBuilders((ListBlock)block)) useBlockParser = true; // Parse the text content of the list items. var newBlockList = new List<MarkdownBlock>(); foreach (var block in listItem.Blocks) { if (block is ListItemBuilder) { var blockText = ((ListItemBuilder)block).Builder.ToString(); if (useBlockParser) { // Parse the list item as a series of blocks. int actualEnd; newBlockList.AddRange(MarkdownDocument.Parse(blockText, 0, blockText.Length, quoteDepth: 0, actualEnd: out actualEnd)); usedBlockParser = true; } else { // Don't allow blocks. var paragraph = new ParagraphBlock(); paragraph.Inlines = Common.ParseInlineChildren(blockText, 0, blockText.Length); newBlockList.Add(paragraph); } } else newBlockList.Add(block); } listItem.Blocks = newBlockList; } return usedBlockParser; }
/// <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>(); int russianDollIndex = -1; bool previousLineWasBlank = 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; int 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; } // 0 spaces = end of the list. // 1-4 spaces = first level. // 5-8 spaces = second level, etc. if (previousLineWasBlank) { // This is the start of a new paragraph. int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; if (spaceCount == 0) break; russianDollIndex = Math.Min(russianDollIndex, (spaceCount - 1) / 4); ListBlock listToAddTo = russianDolls[russianDollIndex].List; currentListItem = listToAddTo.Items[listToAddTo.Items.Count - 1]; currentListItem.Blocks.Add(new ListItemBuilder()); AppendTextToListItem(currentListItem, markdown, Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + (russianDollIndex + 1) * 4), lineInfo.EndOfLine); } else { // Inline text. AppendTextToListItem(currentListItem, markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine); } } // The line was not blank. previousLineWasBlank = false; } // Go to the next line. actualEnd = lineInfo.EndOfLine; } var result = russianDolls[0].List; ReplaceStringBuilders(result); return result; }
/// <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) { actualEnd = start; ListItemBlock currentListItem = null; ListBlock listToAddTo = null; var listStack = new Stack <ListBlock>(); var level = 0; var lastLevel = -1; var previousLineWasBlank = false; var previousLineWasListHeader = false; var isCodeBlock = false; var lastStype = ListStyle.Bulleted; 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 < (level + 2) * 4) { listItemPreamble = ParseItemPreamble(markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine); } if (listItemPreamble != null) { previousLineWasListHeader = true; // 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. var info = markdown.Substring(lineInfo.StartOfLine, lineInfo.EndOfLine - lineInfo.StartOfLine); //Console.WriteLine(info); // Stack if (markdown.Substring(lineInfo.StartOfLine).TrimStart().StartsWith("- [gdb with lua")) { int jj = 0; } int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; level = GetLevel(markdown.Substring(lineInfo.StartOfLine, spaceCount)); if (level > lastLevel) { // Create a new list. var newlistToAddTo = new ListBlock { Style = listItemPreamble.Style, Items = new List <ListItemBlock>() }; lastStype = newlistToAddTo.Style; listStack.Push(newlistToAddTo); if (currentListItem != null) { currentListItem.Blocks.Add(newlistToAddTo); } listToAddTo = newlistToAddTo; } else if (level < lastLevel) { int popCount = lastLevel - level; while (popCount > 0) { popCount--; listStack.Pop(); } listToAddTo = listStack.Peek(); } // 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); lastLevel = level; } else { // No, this line contains text. // Is there even a list in progress? if (currentListItem == null) { actualEnd = start; return(null); } // -------------------------------- // // 0 spaces = end of the list. // // 1-4 spaces = first level. // // 5-8 spaces = second level, etc. // // -------------------------------- // bool codeTag = false; if ((previousLineWasBlank) && (!isCodeBlock)) { // This is the start of a new paragraph. int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; if (spaceCount == 0) { break; } level = GetLevel(markdown.Substring(lineInfo.StartOfLine, spaceCount)); int popCount = lastLevel - level; while (popCount > 0) { popCount--; listStack.Pop(); } listToAddTo = listStack.Peek(); currentListItem = listToAddTo.Items[listToAddTo.Items.Count - 1]; currentListItem.Blocks.Add(new ListItemBuilder()); codeTag = AppendTextToListItem(currentListItem, markdown, Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + (level + 1) * 4), lineInfo.EndOfLine); if (codeTag) { isCodeBlock = !isCodeBlock; } } else { // Inline text. if (previousLineWasListHeader) { //currentListItem = new ListItemBlock { // Blocks = new List<MarkdownBlock>() //}; //listToAddTo.Items.Add(currentListItem); var newlistToAddTo = new ListBlock { Style = lastStype, Items = new List <ListItemBlock>() }; listStack.Push(newlistToAddTo); if (currentListItem != null) { currentListItem.Blocks.Add(newlistToAddTo); } listToAddTo = newlistToAddTo; // Add a new list item. currentListItem = new ListItemBlock { Blocks = new List <MarkdownBlock>() }; listToAddTo.Items.Add(currentListItem); lastLevel = lastLevel + 1; } codeTag = AppendTextToListItem(currentListItem, markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine); } previousLineWasListHeader = false; if (codeTag) { isCodeBlock = !isCodeBlock; } } // The line was not blank. previousLineWasBlank = false; } // Go to the next line. actualEnd = lineInfo.EndOfLine; } var result = listStack.Last(); ReplaceStringBuilders(result); return(result); }
/// <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>(); int russianDollIndex = -1; bool previousLineWasBlank = 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; int 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; } 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; } 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); } // 0 spaces = end of the list. // 1-4 spaces = first level. // 5-8 spaces = second level, etc. if (previousLineWasBlank) { // This is the start of a new paragraph. int spaceCount = lineInfo.FirstNonWhitespaceChar - lineInfo.StartOfLine; if (spaceCount == 0) { break; } russianDollIndex = Math.Min(russianDollIndex, (spaceCount - 1) / 4); ListBlock listToAddTo = russianDolls[russianDollIndex].List; currentListItem = listToAddTo.Items[listToAddTo.Items.Count - 1]; currentListItem.Blocks.Add(new ListItemBuilder()); AppendTextToListItem(currentListItem, markdown, Math.Min(lineInfo.FirstNonWhitespaceChar, lineInfo.StartOfLine + (russianDollIndex + 1) * 4), lineInfo.EndOfLine); } else { // Inline text. AppendTextToListItem(currentListItem, markdown, lineInfo.FirstNonWhitespaceChar, lineInfo.EndOfLine); } } // The line was not blank. previousLineWasBlank = false; } // Go to the next line. actualEnd = lineInfo.EndOfLine; } var result = russianDolls[0].List; ReplaceStringBuilders(result); return(result); }
/// <summary> /// Renders a list element. /// </summary> /// <param name="element"></param> /// <param name="blockUIElementCollection"></param> /// <param name="context"></param> private void RenderListElement(ListBlock element, UIElementCollection blockUIElementCollection, RenderContext context) { // Create a grid with two columns. Grid grid = new Grid(); grid.Margin = ListMargin; // The first column for the bullet (or number) and the second for the text. grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(ListGutterWidth) }); grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) }); for (int rowIndex = 0; rowIndex < element.Items.Count; rowIndex ++) { var listItem = element.Items[rowIndex]; // Add a row definition. grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // Add the bullet or number. var bullet = CreateTextBlock(context); bullet.Margin = ParagraphMargin; switch (element.Style) { case ListStyle.Bulleted: bullet.Text = "•"; break; case ListStyle.Numbered: bullet.Text = $"{rowIndex + 1}."; break; } bullet.HorizontalAlignment = HorizontalAlignment.Right; bullet.Margin = new Thickness(0, 0, ListBulletSpacing, 0); Grid.SetRow(bullet, rowIndex); grid.Children.Add(bullet); // Add the list item content. var content = new StackPanel(); RenderBlocks(listItem.Blocks, content.Children, context); Grid.SetColumn(content, 1); Grid.SetRow(content, rowIndex); grid.Children.Add(content); } blockUIElementCollection.Add(grid); }