Files
markdig/src/Textamina.Markdig/Extensions/Tables/PipeTableInlineParser.cs
2016-03-05 20:43:06 +09:00

544 lines
19 KiB
C#

// Copyright (c) Alexandre Mutel. All rights reserved.
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Collections.Generic;
using System.Reflection;
using Textamina.Markdig.Helpers;
using Textamina.Markdig.Parsers;
using Textamina.Markdig.Parsers.Inlines;
using Textamina.Markdig.Renderers.Html;
using Textamina.Markdig.Syntax;
using Textamina.Markdig.Syntax.Inlines;
namespace Textamina.Markdig.Extensions.Tables
{
/// <summary>
/// The inline parser used to transform a <see cref="ParagraphBlock"/> into a <see cref="TableBlock"/> at inline parsing time.
/// </summary>
/// <seealso cref="Textamina.Markdig.Parsers.InlineParser" />
/// <seealso cref="Textamina.Markdig.Parsers.IDelimiterProcessor" />
public class PipeTableInlineParser : InlineParser, IDelimiterProcessor
{
private LineBreakInlineParser lineBreakParser;
/// <summary>
/// Initializes a new instance of the <see cref="PipeTableInlineParser"/> class.
/// </summary>
public PipeTableInlineParser()
{
OpeningCharacters = new[] { '|', '\n' };
RequireHeaderSeparator = true;
}
/// <summary>
/// Gets or sets a value indicating whether to require header separator. True by default (false to have Kramdown behaviour)
/// </summary>
public bool RequireHeaderSeparator { get; set; }
public override void Initialize(InlineParserState state)
{
// We are using the linebreak parser
lineBreakParser = state.Parsers.Find<LineBreakInlineParser>() ?? new LineBreakInlineParser();
}
public override bool Match(InlineParserState state, ref StringSlice slice)
{
// Only working on Paragraph block
if (!(state.Block is ParagraphBlock))
{
return false;
}
var c = slice.CurrentChar;
// If we have not a delimiter on the first line of a paragraph, don't bother to continue
// tracking other delimiters on following lines
var tableState = state.ParserStates[Index] as TableState;
bool isFirstLineEmpty = false;
if (tableState == null)
{
// A table could be preceded by an empty line or a line containing an inline
// that has not been added to the stack, so we consider this as a valid
// start for a table. Typically, with this, we can have an attributes {...}
// starting on the first line of a pipe table, even if the first line
// doesn't have a pipe
if (state.Inline != null &&(state.LocalLineIndex > 0 || c == '\n'))
{
return false;
}
if (state.Inline == null)
{
isFirstLineEmpty = true;
}
// Else setup a table state
tableState = new TableState();
state.ParserStates[Index] = tableState;
}
if (c == '\n')
{
if (!isFirstLineEmpty && !tableState.LineHasPipe)
{
tableState.IsInvalidTable = true;
}
tableState.LineHasPipe = false;
lineBreakParser.Match(state, ref slice);
tableState.LineIndex++;
if (!isFirstLineEmpty)
{
tableState.ColumnAndLineDelimiters.Add(state.Inline);
}
}
else
{
state.Inline = new PiprTableDelimiterInline(this) { LocalLineIndex = state.LocalLineIndex };
var deltaLine = state.LocalLineIndex - tableState.LineIndex;
if (deltaLine > 0)
{
tableState.IsInvalidTable = true;
}
tableState.LineHasPipe = true;
tableState.LineIndex = state.LocalLineIndex;
slice.NextChar(); // Skip the `|` character
tableState.ColumnAndLineDelimiters.Add(state.Inline);
}
return true;
}
public bool ProcessDelimiters(InlineParserState state, Inline root, Inline lastChild, int delimiterProcessorIndex, bool isFinalProcessing)
{
var container = root as ContainerInline;
var tableState = state.ParserStates[Index] as TableState;
// If the delimiters are being processed by an image link, we need to transform them back to literals
if (!isFinalProcessing)
{
if (container == null || tableState == null)
{
return true;
}
var child = container.LastChild;
List<PiprTableDelimiterInline> delimitersToRemove = null;
while (child != null)
{
var pipeDelimiter = child as PiprTableDelimiterInline;
if (pipeDelimiter != null)
{
if (delimitersToRemove == null)
{
delimitersToRemove = new List<PiprTableDelimiterInline>();
}
delimitersToRemove.Add(pipeDelimiter);
}
if (child == lastChild)
{
break;
}
var subContainer = child as ContainerInline;
child = subContainer?.LastChild;
}
// If we have found any delimiters, transform them to literals
if (delimitersToRemove != null)
{
bool leftIsDelimiter = false;
bool rightIsDelimiter = false;
for (int i = 0; i < delimitersToRemove.Count; i++)
{
var pipeDelimiter = delimitersToRemove[i];
pipeDelimiter.ReplaceBy(new LiteralInline() {Content = new StringSlice("|"), IsClosed = true});
// Check that the pipe that is being removed is not going to make a line without pipe delimiters
var tableDelimiters = tableState.ColumnAndLineDelimiters;
var delimiterIndex = tableDelimiters.IndexOf(pipeDelimiter);
if (i == 0)
{
leftIsDelimiter = delimiterIndex > 0 && tableDelimiters[delimiterIndex - 1] is PiprTableDelimiterInline;
}
else if (i + 1 == delimitersToRemove.Count)
{
rightIsDelimiter = delimiterIndex + 1 < tableDelimiters.Count &&
tableDelimiters[delimiterIndex + 1] is PiprTableDelimiterInline;
}
// Remove this delimiter from the table state
tableState.ColumnAndLineDelimiters.Remove(pipeDelimiter);
}
// If we didn't have any delimiter before and after the delimiters we jsut removed, we mark the state of the current line as no pipe
if (!leftIsDelimiter && !rightIsDelimiter)
{
tableState.LineHasPipe = false;
}
}
return true;
}
// Continue
if (tableState == null || container == null || tableState.IsInvalidTable || !tableState.LineHasPipe || tableState.LineIndex != state.LocalLineIndex)
{
return true;
}
var delimiters = tableState.ColumnAndLineDelimiters;
delimiters.Add(null);
var aligns = FindHeaderRow(delimiters);
if (RequireHeaderSeparator && aligns == null)
{
return true;
}
var table = new TableBlock();
// If the current paragraph block has any attributes attached, we can copy them
var attributes = state.Block.TryGetAttributes();
if (attributes != null)
{
attributes.CopyTo(table.GetAttributes());
}
state.BlockNew = table;
TableRowBlock firstRow = null;
int maxColumn = 0;
var cells = tableState.Cells;
cells.Clear();
TableRowBlock currentRow = null;
Inline column = container.FirstChild;
if (column is PiprTableDelimiterInline)
{
column = ((PiprTableDelimiterInline)column).FirstChild;
}
int lastIndex = 0;
for (int i = 0; i < delimiters.Count; i++)
{
var delimiter = delimiters[i];
if (delimiter == null || IsLine(delimiter))
{
var beforeDelimiter = delimiter?.PreviousSibling;
var nextLineColumn = delimiter?.NextSibling;
var row = new TableRowBlock { Parent = table };
table.Children.Add(row);
for (int j = lastIndex; j <= i; j++)
{
var columnSeparator = delimiters[j];
var pipeSeparator = columnSeparator as PiprTableDelimiterInline;
var endOfColumn = columnSeparator?.PreviousSibling;
// This is the first column empty
if (j == lastIndex && pipeSeparator != null && endOfColumn == null)
{
columnSeparator.Remove();
column = pipeSeparator.FirstChild;
continue;
}
if (pipeSeparator != null && IsTrailingColumnDelimiter(pipeSeparator))
{
TrimEnd(endOfColumn);
columnSeparator.Remove();
continue;
}
var columnContainer = new ContainerInline();
var item = column;
TrimStart(item);
while (item != null && !IsLine(item) && !(item is PiprTableDelimiterInline))
{
var nextSibling = item.NextSibling;
item.Remove();
columnContainer.AppendChild(item);
item = nextSibling;
}
var tableCell = new TableCellBlock { Inline = columnContainer, Parent = row };
row.Children.Add(tableCell);
cells.Add(tableCell);
// If we have reached the end, we can add remaining delimiters as pure child of the current cell
if (row.Children.Count == maxColumn && columnSeparator is PiprTableDelimiterInline)
{
columnSeparator.Remove();
tableCell.Inline.AppendChild(columnSeparator);
break;
}
TrimEnd(endOfColumn);
//TrimEnd(previousSibling);
if (columnSeparator != null)
{
if (pipeSeparator != null)
{
column = pipeSeparator.FirstChild;
}
columnSeparator.Remove();
}
}
TrimEnd(beforeDelimiter);
if (delimiter != null)
{
delimiter.Remove();
}
if (nextLineColumn != null)
{
column = nextLineColumn;
}
if (firstRow == null)
{
firstRow = row;
maxColumn = firstRow.Children.Count;
}
lastIndex = i + 1;
}
}
// If we have a header row, we can remove it
if (aligns != null)
{
table.Children.RemoveAt(1);
var tableRow = (TableRowBlock) table.Children[0];
tableRow.ColumnAlignments = aligns;
tableRow.IsHeader = true;
}
// Perform delimiter processor that are coming after this processor
var delimiterProcessors = state.Parsers.DelimiterProcessors;
for (int i = 0; i < delimiterProcessors.Length; i++)
{
if (delimiterProcessors[i] == this)
{
foreach (var cell in cells)
{
state.ProcessDelimiters(i + 1, cell.Inline, null, true);
}
break;
}
}
// Clear cells when we are done
cells.Clear();
// We don't want to continue procesing delimiters, as we are already processing them here
return false;
}
private static bool ParseHeaderString(Inline inline, out TableColumnAlign align)
{
align = 0;
var literal = inline as LiteralInline;
if (literal == null)
{
return false;
}
// Work on a copy of the slice
var line = literal.Content;
line.Trim();
var c = line.CurrentChar;
bool hasLeft = false;
bool hasRight = false;
if (c == ':')
{
hasLeft = true;
c = line.NextChar();
}
int count = 0;
while (c == '-')
{
c = line.NextChar();
count++;
}
if (count == 0)
{
return false;
}
if (c == ':')
{
hasRight = true;
c = line.NextChar();
}
if (c != '\0')
{
return false;
}
align = hasLeft && hasRight
? TableColumnAlign.Center
: hasRight ? TableColumnAlign.Right : TableColumnAlign.Left;
return true;
}
private List<TableColumnAlign> FindHeaderRow(List<Inline> delimiters)
{
bool isValidRow = false;
List<TableColumnAlign> aligns = null;
for (int i = 0; i < delimiters.Count; i++)
{
if (delimiters[i] != null && IsLine(delimiters[i]))
{
// The last delimiter is always null,
for (int j = i + 1; j < delimiters.Count - 1; j++)
{
var delimiter = delimiters[j];
var nextDelimiter = delimiters[j + 1];
var columnDelimiter = delimiter as PiprTableDelimiterInline;
if (j == i + 1 && IsStartOfLineColumnDelimiter(columnDelimiter))
{
continue;
}
// Check the left side of a `|` delimiter
TableColumnAlign align;
if (!ParseHeaderString(delimiter.PreviousSibling, out align))
{
break;
}
// Create aligns until we may have a header row
if (aligns == null)
{
aligns = new List<TableColumnAlign>();
}
aligns.Add(align);
// If this is the last delimiter, we need to check the right side of the `|` delimiter
if (nextDelimiter == null)
{
var nextSibling = columnDelimiter != null
? columnDelimiter.FirstChild
: delimiter.NextSibling;
if (!ParseHeaderString(nextSibling, out align))
{
break;
}
isValidRow = true;
aligns.Add(align);
break;
}
// If we are on a Line delimiter, exit
if (IsLine(delimiter))
{
isValidRow = true;
break;
}
}
break;
}
}
return isValidRow ? aligns : null;
}
private static bool IsLine(Inline inline)
{
return inline is LineBreakInline;
}
private static bool IsStartOfLineColumnDelimiter(Inline inline)
{
if (inline == null)
{
return false;
}
var previous = inline.PreviousSibling;
if (previous == null)
{
return true;
}
var literal = previous as LiteralInline;
if (literal != null)
{
if (!literal.Content.IsEmptyOrWhitespace())
{
return false;
}
previous = previous.PreviousSibling;
}
return previous == null || IsLine(previous);
}
private static bool IsTrailingColumnDelimiter(PiprTableDelimiterInline inline)
{
var child = inline.FirstChild;
var literal = child as LiteralInline;
if (literal != null)
{
if (!literal.Content.IsEmptyOrWhitespace())
{
return false;
}
child = child.NextSibling;
}
return child == null || IsLine(child);
}
private static void TrimStart(Inline inline)
{
while (inline is ContainerInline && !(inline is DelimiterInline))
{
inline = ((ContainerInline)inline).FirstChild;
}
var literal = inline as LiteralInline;
if (literal != null)
{
literal.Content.TrimStart();
}
}
private static void TrimEnd(Inline inline)
{
var literal = inline as LiteralInline;
if (literal != null)
{
literal.Content.TrimEnd();
}
}
private class TableState
{
public TableState()
{
ColumnAndLineDelimiters = new List<Inline>();
Cells = new List<TableCellBlock>();
}
public bool IsInvalidTable { get; set; }
public bool LineHasPipe { get; set; }
public int LineIndex { get; set; }
public List<Inline> ColumnAndLineDelimiters { get; }
public List<TableCellBlock> Cells { get; }
}
}
}