Files
markdig/src/Textamina.Markdig/Syntax/ListBlock.cs

382 lines
15 KiB
C#
Raw Normal View History

using Textamina.Markdig.Helpers;
using Textamina.Markdig.Parsing;
namespace Textamina.Markdig.Syntax
{
public class ListBlock : ContainerBlock
{
public new static readonly BlockParser Parser = new ParserInternal();
public ListBlock(BlockParser parser) : base(parser)
{
}
2016-02-23 00:01:43 +09:00
public bool IsOrdered { get; set; }
public char BulletChar { get; set; }
2016-02-23 00:01:43 +09:00
public int OrderedStart { get; set; }
public char OrderedDelimiter { get; set; }
public bool IsLoose { get; set; }
2016-02-24 01:01:40 +09:00
private int CountAllBlankLines { get; set; }
2016-02-24 01:01:40 +09:00
private int CountBlankLinesReset { get; set; }
private class ParserInternal : BlockParser
{
public override MatchLineResult Match(BlockParserState state)
{
if (state.Pending is ListBlock && state.NextPending is ListItemBlock)
{
// We try to match only on item block if the ListBlock
return MatchLineResult.Skip;
}
2016-02-24 01:01:40 +09:00
// When both a thematic break and a list item are possible
// interpretations of a line, the thematic break takes precedence
2016-02-25 20:30:37 +09:00
var save = state.Line;
if (ThematicBreakBlock.Parser.Match(state) == MatchLineResult.Last)
2016-02-24 01:01:40 +09:00
{
2016-02-25 20:30:37 +09:00
// Remove the ThematicBreakBlock as we will let the ThematicBreakBlock to catch it later
2016-02-24 01:01:40 +09:00
state.NewBlocks.Pop();
return MatchLineResult.None;
}
2016-02-25 20:30:37 +09:00
state.SetCurrentLine(ref save);
2016-02-24 01:01:40 +09:00
// 5.2 List items
// TODO: Check with specs, it is not clear that list marker or bullet marker must be followed by at least 1 space
2016-02-23 00:01:43 +09:00
int preIndent = 0;
2016-02-25 20:30:37 +09:00
for (int i = state.Line.Start - 1; i >= 0; i--)
2016-02-23 00:01:43 +09:00
{
2016-02-25 20:30:37 +09:00
if (state.Line[i].IsSpaceOrTab())
2016-02-23 00:01:43 +09:00
{
preIndent++;
}
else
{
break;
}
}
2016-02-25 20:30:37 +09:00
var saveLiner = state.Line;
// If we have already a ListItemBlock, we are going to try to append to it
var listItem = state.Pending as ListItemBlock;
2016-02-23 00:01:43 +09:00
if (listItem != null)
{
var list = (ListBlock) listItem.Parent;
// Allow all blanks lines if the last block is a fenced code block
// Allow 1 blank line inside a list
// If > 1 blank line, terminate this list
2016-02-25 20:30:37 +09:00
var isBlankLine = state.IsBlankLine;
2016-02-23 00:01:43 +09:00
//if (isBlankLine && !(state.LastBlock is FencedCodeBlock)) // TODO: Handle this case
var isInFencedBlock = state.LastBlock is FencedCodeBlock;
2016-02-23 00:01:43 +09:00
if (isBlankLine)
{
// TODO: Check with a generic way (allow a block to have multiple empty lines)
if (!isInFencedBlock)
2016-02-23 00:01:43 +09:00
{
if (!(state.NextPending is ListBlock))
{
2016-02-24 01:01:40 +09:00
list.CountAllBlankLines++;
listItem.Children.Add(BlankLineBlock.Instance);
}
2016-02-24 01:01:40 +09:00
list.CountBlankLinesReset++;
2016-02-23 00:01:43 +09:00
}
2016-02-24 01:01:40 +09:00
if (list.CountBlankLinesReset > 1)
{
2016-02-23 00:01:43 +09:00
// TODO: Close all lists and not only this one
return MatchLineResult.LastDiscard;
}
2016-02-24 01:01:40 +09:00
if (list.CountBlankLinesReset == 1 && listItem.NumberOfSpaces < 0)
{
state.Close(listItem);
// Leave the list open
list.IsOpen = true;
return MatchLineResult.Continue;
}
return MatchLineResult.Continue;
}
2016-02-24 01:01:40 +09:00
list.CountBlankLinesReset = 0;
2016-02-25 20:30:37 +09:00
var c = state.Line.CurrentChar;
var startPosition = state.Line.Start;
2016-02-23 00:01:43 +09:00
// List Item starting with a blank line (-1)
if (listItem.NumberOfSpaces < 0)
{
2016-02-23 00:01:43 +09:00
int expectedCount = -listItem.NumberOfSpaces;
int countSpaces = 0;
2016-02-25 20:30:37 +09:00
var saved = new StringSlice();
2016-02-23 00:01:43 +09:00
while (c.IsSpaceOrTab())
{
2016-02-25 20:30:37 +09:00
c = state.Line.NextChar();
countSpaces = preIndent + state.Line.Column - startPosition;
2016-02-23 00:01:43 +09:00
if (countSpaces == expectedCount)
{
2016-02-25 20:30:37 +09:00
saved = state.Line;
2016-02-23 00:01:43 +09:00
}
else if (countSpaces >= 4)
{
2016-02-25 20:30:37 +09:00
state.SetCurrentLine(ref saved);
2016-02-23 00:01:43 +09:00
countSpaces = expectedCount;
break;
}
}
if (countSpaces == expectedCount)
{
2016-02-23 00:01:43 +09:00
listItem.NumberOfSpaces = countSpaces;
return MatchLineResult.Continue;
}
}
2016-02-23 00:01:43 +09:00
else
{
while (c.IsSpaceOrTab())
{
2016-02-25 20:30:37 +09:00
c = state.Line.NextChar();
var countSpaces = preIndent + state.Line.Column - startPosition;
2016-02-23 00:01:43 +09:00
if (countSpaces >= listItem.NumberOfSpaces)
{
return MatchLineResult.Continue;
}
}
}
2016-02-25 20:30:37 +09:00
state.SetCurrentLine(ref saveLiner);
}
2016-02-23 00:01:43 +09:00
return TryParseListItem(ref state, preIndent);
}
private MatchLineResult TryParseListItem(ref BlockParserState state, int preIndent)
{
var isInList = state.Pending is ListItemBlock;
2016-02-25 20:30:37 +09:00
var preStartPosition = state.Line.Start;
2016-02-25 20:30:37 +09:00
var c = state.Line.CurrentChar;
if (isInList)
{
while (c.IsSpaceOrTab())
{
2016-02-25 20:30:37 +09:00
c = state.Line.NextChar();
}
}
else
{
2016-02-25 20:30:37 +09:00
// TODO
//state.Line.SkipLeadingSpaces3();
c = state.Line.CurrentChar;
}
2016-02-25 20:30:37 +09:00
preIndent = preIndent + state.Line.Start - preStartPosition;
2016-02-23 00:01:43 +09:00
var isOrdered = false;
var bulletChar = (char) 0;
int orderedStart = 0;
var orderedDelimiter = (char) 0;
2016-02-25 20:30:37 +09:00
var column = state.Line.Start;
2016-02-23 00:01:43 +09:00
if (c.IsBulletListMarker())
{
bulletChar = c;
preIndent++;
}
else if (c.IsDigit())
{
int countDigit = 0;
while (c.IsDigit())
{
orderedStart = orderedStart*10 + c - '0';
2016-02-25 20:30:37 +09:00
c = state.Line.NextChar();
2016-02-23 00:01:43 +09:00
preIndent++;
countDigit++;
}
// Note that ordered list start numbers must be nine digits or less:
if (countDigit > 9)
{
return MatchLineResult.None;
}
// We don't have an ordered list
if (c != '.' && c != ')')
{
return MatchLineResult.None;
}
preIndent++;
isOrdered = true;
orderedDelimiter = c;
}
else
{
2016-02-23 00:01:43 +09:00
return MatchLineResult.None;
}
2016-02-23 00:01:43 +09:00
// Skip Bullet or '.'
2016-02-25 20:30:37 +09:00
state.Line.NextChar();
2016-02-23 00:01:43 +09:00
// Item starting with a blank line
int numberOfSpaces;
2016-02-25 20:30:37 +09:00
if (state.IsBlankLine)
2016-02-23 00:01:43 +09:00
{
// Use a negative number to store the number of expected chars
numberOfSpaces = -(preIndent + 1);
}
else
{
var startPosition = -1;
2016-02-23 00:01:43 +09:00
int countSpaceAfterBullet = 0;
2016-02-25 20:30:37 +09:00
var saved = new StringSlice();
2016-02-23 00:01:43 +09:00
for (int i = 0; i <= 4; i++)
{
2016-02-25 20:30:37 +09:00
c = state.Line.CurrentChar;
2016-02-23 00:01:43 +09:00
if (!c.IsSpaceOrTab())
{
break;
}
if (i == 0)
{
2016-02-25 20:30:37 +09:00
startPosition = state.Line.Column;
}
2016-02-25 20:30:37 +09:00
var endPosition = state.Line.Column;
2016-02-23 00:01:43 +09:00
countSpaceAfterBullet = endPosition - startPosition;
if (countSpaceAfterBullet == 1)
{
2016-02-25 20:30:37 +09:00
saved = state.Line;
2016-02-23 00:01:43 +09:00
}
else if (countSpaceAfterBullet >= 4)
{
2016-02-25 20:30:37 +09:00
//state.Line.SpaceHeaderCount = countSpaceAfterBullet - 4;
2016-02-23 00:01:43 +09:00
countSpaceAfterBullet = 0;
2016-02-25 20:30:37 +09:00
state.SetCurrentLine(ref saved);
break;
}
2016-02-25 20:30:37 +09:00
state.Line.NextChar();
}
// If we haven't matched any spaces, early exit
if (startPosition < 0)
{
return MatchLineResult.None;
}
// Number of spaces required for the following content to be part of this list item
2016-02-23 00:01:43 +09:00
numberOfSpaces = preIndent + countSpaceAfterBullet + 1;
}
var newListItem = new ListItemBlock(this)
2016-02-23 00:01:43 +09:00
{
Column = column,
2016-02-23 00:01:43 +09:00
NumberOfSpaces = numberOfSpaces
};
state.NewBlocks.Push(newListItem);
2016-02-23 07:50:13 +09:00
var currentListItem = state.Pending as ListItemBlock;
var currentParent = state.Pending as ListBlock ?? (ListBlock)currentListItem?.Parent;
if (currentParent != null)
{
// If we have a new list item, close the previous one
if (currentListItem != null)
{
state.Close(currentListItem);
}
// Reset the list if it is a new list or a new type of bullet
if (currentParent.IsOrdered != isOrdered ||
(isOrdered && currentParent.OrderedDelimiter != orderedDelimiter) ||
(!isOrdered && currentParent.BulletChar != bulletChar)
//(numberOfSpaces < ((ListItemBlock) currentParent.LastChild).NumberOfSpaces)
)
{
state.Close(currentParent);
currentParent = null;
}
}
if (currentParent == null)
2016-02-23 00:01:43 +09:00
{
var newList = new ListBlock(this)
{
Column = column,
IsOrdered = isOrdered,
2016-02-23 00:01:43 +09:00
BulletChar = bulletChar,
OrderedDelimiter = orderedDelimiter,
OrderedStart = orderedStart,
};
state.NewBlocks.Push(newList);
2016-02-23 07:50:13 +09:00
}
2016-02-23 00:01:43 +09:00
return MatchLineResult.Continue;
}
2016-02-23 07:50:13 +09:00
public override void Close(BlockParserState state)
2016-02-23 07:50:13 +09:00
{
var listBlock = state.Pending as ListBlock;
// Process only if we have blank lines
2016-02-24 01:01:40 +09:00
if (listBlock != null && listBlock.CountAllBlankLines > 0)
2016-02-23 07:50:13 +09:00
{
2016-02-24 01:01:40 +09:00
bool isLastListItem = true;
for (int listIndex = listBlock.Children.Count - 1; listIndex >= 0; listIndex--)
{
var block = listBlock.Children[listIndex];
var listItem = (ListItemBlock) block;
var children = listItem.Children;
2016-02-24 01:01:40 +09:00
bool isLastElement = true;
for (int i = children.Count - 1; i >= 0; i--)
{
var item = children[i];
if (item is BlankLineBlock)
{
2016-02-24 01:01:40 +09:00
if ((isLastElement && listIndex < listBlock.Children.Count - 1) || (children.Count > 2 && (i > 0 && i < (children.Count - 1))))
{
listBlock.IsLoose = true;
}
2016-02-24 01:01:40 +09:00
if (isLastElement && isLastListItem)
{
// Inform the outer list that we have a blank line
var parentListItemBlock = listBlock.Parent as ListItemBlock;
if (parentListItemBlock != null)
{
var parentList = (ListBlock) parentListItemBlock.Parent;
parentList.CountAllBlankLines++;
parentListItemBlock.Children.Add(BlankLineBlock.Instance);
}
}
children.RemoveAt(i);
2016-02-24 01:01:40 +09:00
// If we have remove all blank lines, we can exit
listBlock.CountAllBlankLines--;
if (listBlock.CountAllBlankLines == 0)
{
return;
}
}
2016-02-24 01:01:40 +09:00
isLastElement = false;
}
2016-02-24 01:01:40 +09:00
isLastListItem = false;
}
2016-02-23 07:50:13 +09:00
}
}
}
}
}