Add tests for StringLine and StringLineGroup. Fix issues with StringLine and StringLineGroup. Restore tests passed (253)

This commit is contained in:
Alexandre Mutel
2016-02-18 22:32:45 +09:00
parent 8b54d0d833
commit 748bb6adc6
10 changed files with 686 additions and 171 deletions

View File

@@ -0,0 +1,144 @@
using NUnit.Framework;
using Textamina.Markdig.Syntax;
namespace Textamina.Markdig.Tests
{
[TestFixture]
public class TestLinkInline
{
[Test]
public void TestLinkSimple()
{
var text = new StringLineGroup("toto tutu");
string link;
Assert.True(LinkInline.TryParseLink(text, out link));
Assert.AreEqual("toto", link);
Assert.AreEqual(' ', text.Current);
}
[Test]
public void TestLinkUrl()
{
var text = new StringLineGroup("http://google.com)");
string link;
Assert.True(LinkInline.TryParseLink(text, out link));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual(')', text.Current);
}
[Test]
public void TestLinkNestedParenthesis()
{
var text = new StringLineGroup("(toto)tutu(tata) nooo");
string link;
Assert.True(LinkInline.TryParseLink(text, out link));
Assert.AreEqual("(toto)tutu(tata)", link);
Assert.AreEqual(' ', text.Current);
}
[Test]
public void TestLinkAlternate()
{
var text = new StringLineGroup("<toto_tata_tutu> nooo");
string link;
Assert.True(LinkInline.TryParseLink(text, out link));
Assert.AreEqual("toto_tata_tutu", link);
Assert.AreEqual(' ', text.Current);
}
[Test]
public void TestLinkAlternateInvalid()
{
var text = new StringLineGroup("<toto_tata_tutu");
string link;
Assert.False(LinkInline.TryParseLink(text, out link));
}
[Test]
public void TestTitleSimple()
{
var text = new StringLineGroup(@"'tata\tutu\''");
string title;
Assert.True(LinkInline.TryParseTitle(text, out title));
Assert.AreEqual(@"tata\tutu'", title);
}
[Test]
public void TestTitleSimpleAlternate()
{
var text = new StringLineGroup(@"""tata\tutu\"""" ");
string title;
Assert.True(LinkInline.TryParseTitle(text, out title));
Assert.AreEqual(@"tata\tutu""", title);
Assert.AreEqual(' ', text.Current);
}
[Test]
public void TestLinkAndTitle()
{
var text = new StringLineGroup(@"(http://google.com 'this is a title')ABC");
string link;
string title;
Assert.True(LinkInline.TryParseLinkAndTitle(text, out link, out title));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("this is a title", title);
Assert.AreEqual('A', text.Current);
}
[Test]
public void TestLinkAndTitleEmpty()
{
var text = new StringLineGroup(@"(<>)A");
string link;
string title;
Assert.True(LinkInline.TryParseLinkAndTitle(text, out link, out title));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual('A', text.Current);
}
[Test]
public void TestLinkAndTitleEmpty2()
{
var text = new StringLineGroup(@"( <> )A");
string link;
string title;
Assert.True(LinkInline.TryParseLinkAndTitle(text, out link, out title));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual(string.Empty, title);
Assert.AreEqual('A', text.Current);
}
[Test]
public void TestLinkEmptyWithTitleWithMultipleSpaces()
{
var text = new StringLineGroup(@"( <> 'toto' )A");
string link;
string title;
Assert.True(LinkInline.TryParseLinkAndTitle(text, out link, out title));
Assert.AreEqual(string.Empty, link);
Assert.AreEqual("toto", title);
Assert.AreEqual('A', text.Current);
}
[Test]
public void TestMultipleLines()
{
var text = new StringLineGroup()
{
new StringLine("("),
new StringLine(" <http://google.com> "),
new StringLine(" 'toto' )A")
};
string link;
string title;
Assert.True(LinkInline.TryParseLinkAndTitle(text, out link, out title));
Assert.AreEqual("http://google.com", link);
Assert.AreEqual("toto", title);
Assert.AreEqual('A', text.Current);
}
}
}

View File

@@ -0,0 +1,110 @@
using NUnit.Framework;
using System.Collections.Generic;
using System.Text;
using Textamina.Markdig.Syntax;
namespace Textamina.Markdig.Tests
{
[TestFixture]
public class TestStringLine
{
// TODO: Add tests for StringLine
// TODO: Add more tests for StringLineGroup
[Test]
public void TestStringLineGroupSimple()
{
var text = new StringLineGroup()
{
new StringLine("ABC"),
new StringLine("E"),
new StringLine("F")
};
var chars = ToString(text);
TextAssert.AreEqual("ABC\nE\nF", chars.ToString());
}
private static string ToString(StringLineGroup text)
{
var chars = new StringBuilder();
while (text.Current != '\0')
{
chars.Append(text.Current);
text.NextChar();
}
return chars.ToString();
}
[Test]
public void TestStringLineGroupSaveAndRestore()
{
var text = new StringLineGroup()
{
new StringLine("ABCD"),
new StringLine("EF"),
};
text.NextChar(); // B
var save = text.Save();
text.NextChar(); // C
text.NextChar(); // D
text.NextChar(); // \n
text.NextChar();
Assert.AreEqual('E', text.Current);
Assert.AreEqual(1, text.LinePosition);
Assert.AreEqual(0, text.ColumnPosition);
text.Restore(ref save);
Assert.AreEqual('B', text.Current);
Assert.AreEqual(0, text.LinePosition);
Assert.AreEqual(1, text.ColumnPosition);
}
[Test]
public void TestSkipWhitespaces()
{
var text = new StringLineGroup(" ABC");
Assert.True(text.SkipWhiteSpaces());
Assert.False(text.SkipWhiteSpaces());
Assert.AreEqual('A', text.Current);
}
[Test]
public void TestStringLineGroupWithModifiedStart()
{
var line1 = new StringLine(" ABC");
line1.NextChar();
line1.NextChar();
var line2 = new StringLine(" DEF ");
line2.Trim();
var text = new StringLineGroup() {line1, line2};
var result = ToString(text);
TextAssert.AreEqual("ABC\nDEF", result);
}
[Test]
public void TestStringLineGroupWithTrim()
{
var line1 = new StringLine(" ABC ");
line1.NextChar();
line1.NextChar();
var line2 = new StringLine(" DEF ");
var text = new StringLineGroup() { line1, line2 };
text.Trim();
var result = ToString(text);
TextAssert.AreEqual("ABC \n DEF", result);
}
}
}

View File

@@ -57,6 +57,8 @@
<DesignTime>True</DesignTime>
<DependentUpon>Specs.tt</DependentUpon>
</Compile>
<Compile Include="TestLinkInline.cs" />
<Compile Include="TestStringLine.cs" />
<Compile Include="TextAssert.cs" />
<Compile Include="TestParser.cs" />
</ItemGroup>

View File

@@ -31,6 +31,7 @@ namespace Textamina.Markdig.Formatters
[typeof(EscapeInline)] = o => Write((EscapeInline)o),
[typeof(LiteralInline)] = o => Write((LiteralInline)o),
[typeof(CodeInline)] = o => Write((CodeInline)o),
[typeof(ContainerInline)] = o => Write((ContainerInline)o),
};
}
@@ -123,6 +124,21 @@ namespace Textamina.Markdig.Formatters
writer.WriteConstant("</code>");
}
protected void Write(ContainerInline containerInline)
{
var inline = containerInline.FirstChild;
while (inline != null)
{
Action<object> writerAction;
if (registeredWriters.TryGetValue(inline.GetType(), out writerAction))
{
writerAction(inline);
}
inline = inline.NextSibling;
}
}
protected void WriteLeaf(LeafBlock leafBlock, bool writeEndOfLines)
{
var inline = leafBlock.Inline;

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using Textamina.Markdig.Syntax;
namespace Textamina.Markdig.Parsing
@@ -417,13 +418,15 @@ namespace Textamina.Markdig.Parsing
private void ProcessInlines(LeafBlock leafBlock)
{
var lines = leafBlock.Lines;
lines.Initialize();
var state = new MatchInlineState(lines) {Builder = new StringBuilder() };
leafBlock.Inline = new ContainerInline();
var state = new MatchInlineState(lines)
{
Builder = new StringBuilder(),
Inline = leafBlock.Inline
};
var opened = new List<Inline>();
Inline previousInline = null;
var previousInline = leafBlock.Inline;
while (!lines.IsEndOfLines)
{
var saveLines = lines.Save();
@@ -460,14 +463,17 @@ namespace Textamina.Markdig.Parsing
}
}
if (previousInline != state.Inline)
var nextInline = state.Inline;
if (previousInline != nextInline)
{
if (previousInline is LeafInline)
{
opened.Remove(previousInline);
previousInline.Close(state);
previousInline.InsertAfter(state.Inline);
opened.Add(state.Inline);
if (nextInline.Parent == null)
{
previousInline.InsertAfter(nextInline);
}
}
else if (previousInline != null)
{
@@ -475,25 +481,23 @@ namespace Textamina.Markdig.Parsing
if (container.IsClosed)
{
opened.Remove(previousInline);
container.InsertAfter(state.Inline);
container.InsertAfter(nextInline);
}
else
{
container.AppendChild(state.Inline);
container.AppendChild(nextInline);
}
}
// Store first inline
if (leafBlock.Inline == null)
{
leafBlock.Inline = state.Inline;
}
}
previousInline = state.Inline;
previousInline = nextInline;
}
while (previousInline != null)
{
previousInline.Close(state);
previousInline = previousInline.Parent;
}
// TODO: Close opened inlines

View File

@@ -4,7 +4,7 @@ using Textamina.Markdig.Parsing;
namespace Textamina.Markdig.Syntax
{
public abstract class ContainerInline : Inline
public class ContainerInline : Inline
{
public Inline FirstChild { get; private set; }

View File

@@ -11,134 +11,144 @@ namespace Textamina.Markdig.Syntax
[ThreadStatic]
private static readonly StringBuilder TempBuilder = new StringBuilder();
private class ParserInternal : InlineParser
{
public ParserInternal()
{
FirstChars = new[] {'[', ']', '!'};
}
public override bool Match(MatchInlineState state)
{
var lines = state.Lines;
var c = lines.Current;
bool isImage = false;
if (c == '!')
{
isImage = true;
c = lines.NextChar();
if (c != '[')
{
return false;
}
}
if (c == '[')
{
state.Inline = new LinkDelimiterInline(this)
{
Type = DelimiterType.Open,
IsImage = isImage
};
return true;
}
else if (c == ']')
{
if (state.Inline != null)
{
LinkDelimiterInline firstParent = null;
foreach (var parent in state.Inline.FindParentOfType<LinkDelimiterInline>())
{
firstParent = parent;
break;
}
// This will be matched as a literal
if (firstParent != null)
{
// TODO: continue parsing of ]
// We have a nested [ ]
// firstParent.Remove();
// The opening [ will be transformed to a literal followed by all the childrens of the [
var literal = new LiteralInline() {Content = firstParent.IsImage ? "![" : "["};
state.Inline = firstParent.ReplaceBy(literal);
return true;
}
//else
//{
// var link = firstParent.IsImage
// ? (LinkInline) new ImageLinkInline()
// : new TextLinkInline();
// // 1. Process all delimiters inside firstParent to convert them to inlines
// // 2. Replace firstParent with link, and move all child to this one
// // TODO: continue parsing of ]
// if (state.Inline == firstParent)
// {
// state.Inline = link;
// }
//}
}
return false;
// Match a close tag
}
// We don't have an emphasis
return false;
}
private bool TryParseLinkUrl(MatchInlineState state)
{
return false;
}
private bool TryParseLinkTitle(MatchInlineState state)
{
return false;
}
}
public static bool TryParseLinkTitle(StringLineGroup text, out string title)
public static bool TryParseLinkAndTitle(StringLineGroup text, out string link, out string title)
{
if (text == null) throw new ArgumentNullException(nameof(text));
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
// a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or
// a sequence of zero or more characters between matching parentheses ((...)), including a ) character only if it is backslash-escaped.
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
// 2. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces?
// 3. an optional link destination,
// 4. an optional link title separated from the link destination by whitespace,
// 5. optional whitespace, TODO: specs: is it whitespace or multiple whitespaces?
// 6. and a right parenthesis )
bool isValid = false;
var c = text.Current;
link = null;
title = null;
// 1. An inline link consists of a link text followed immediately by a left parenthesis (,
if (c == '(')
{
text.NextChar();
text.SkipWhiteSpaces();
if (TryParseLink(text, out link))
{
var hasWhiteSpaces = text.SkipWhiteSpaces();
c = text.Current;
if (c == ')')
{
isValid = true;
}
else if (hasWhiteSpaces)
{
c = text.Current;
if (c == ')')
{
isValid = true;
}
else if (TryParseTitle(text, out title))
{
text.SkipWhiteSpaces();
c = text.Current;
if (c == ')')
{
isValid = true;
}
}
}
}
}
if (isValid)
{
// Skip ')'
text.NextChar();
title = title ?? string.Empty;
}
return isValid;
}
public static bool TryParseTitle(StringLineGroup text, out string title)
{
if (text == null) throw new ArgumentNullException(nameof(text));
bool isValid = false;
var buffer = TempBuilder;
buffer.Clear();
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
// a sequence of zero or more characters between straight single-quote characters ('), including a ' character only if it is backslash-escaped, or
var c = text.Current;
if (c == '\'' || c == '"')
{
var quote = c;
bool hasEscape = false;
while (true)
{
c = text.NextChar();
if (c == '\0')
{
break;
}
if (c == quote)
{
if (hasEscape)
{
buffer.Append(quote);
hasEscape = false;
continue;
}
// Skip last quote
text.NextChar();
isValid = true;
break;
}
if (hasEscape)
{
buffer.Append('\\');
}
if (c == '\\')
{
hasEscape = true;
continue;
}
hasEscape = false;
buffer.Append(c);
}
}
else
{
// a sequence of zero or more characters between matching parentheses ((...)), including a ) character only if it is backslash-escaped.
// TODO
isValid = true;
}
return false;
title = isValid ? buffer.ToString() : null;
buffer.Clear();
return isValid;
}
public static bool TryParseLinkDestination(StringLineGroup text, out string link)
public static bool TryParseLink(StringLineGroup text, out string link)
{
if (text == null) throw new ArgumentNullException(nameof(text));
var destination = TempBuilder;
destination.Clear();
link = null;
bool isValid = false;
var buffer = TempBuilder;
buffer.Clear();
var c = text.Current;
@@ -152,9 +162,9 @@ namespace Textamina.Markdig.Syntax
c = text.NextChar();
if (!nextEscape && c == '>')
{
link = destination.ToString();
destination.Clear();
return true;
text.NextChar();
isValid = true;
break;
}
if (!nextEscape && c == '<')
@@ -175,9 +185,11 @@ namespace Textamina.Markdig.Syntax
break;
}
destination.Append(c);
buffer.Append(c);
} while (c != '\0');
link = isValid ? buffer.ToString() : null;
}
else
{
@@ -187,10 +199,8 @@ namespace Textamina.Markdig.Syntax
// parentheses.
bool isEscaped = false;
int openedParent = 0;
do
while (c != '\0')
{
c = text.NextChar();
// Match opening and closing parenthesis
if (c == '(')
{
@@ -211,11 +221,17 @@ namespace Textamina.Markdig.Syntax
openedParent--;
if (openedParent < 0)
{
isValid = true;
break;
}
}
}
if (isEscaped)
{
buffer.Append('\\');
}
// If we have an escape
if (c == '\\')
{
@@ -225,26 +241,134 @@ namespace Textamina.Markdig.Syntax
isEscaped = false;
var isSpace = Utility.IsSpaceOrTab(c);
if (isSpace || Utility.IsControl(c)) // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
if (Utility.IsSpaceOrTab(c) || Utility.IsControl(c)) // TODO: specs unclear. space is strict or relaxed? (includes tabs?)
{
if (isSpace && destination.Length > 0)
{
link = destination.ToString();
destination.Clear();
return true;
}
isValid = true;
break;
}
destination.Append(c);
buffer.Append(c);
} while (c != '\0');
c = text.NextChar();
}
isValid = isValid && buffer.Length > 0;
link = isValid ? buffer.ToString() : null;
}
buffer.Clear();
return isValid;
}
private class ParserInternal : InlineParser
{
public ParserInternal()
{
FirstChars = new[] {'[', ']', '!'};
}
public override bool Match(MatchInlineState state)
{
var text = state.Lines;
var c = text.Current;
bool isImage = false;
if (c == '!')
{
isImage = true;
c = text.NextChar();
if (c != '[')
{
return false;
}
}
if (c == '[')
{
state.Inline = new LinkDelimiterInline(this)
{
Type = DelimiterType.Open,
IsImage = isImage
};
return true;
}
if (c == ']')
{
if (state.Inline != null)
{
return TryParseEndOfLinkOrImage(ref state.Inline, text);
}
return false;
// Match a close tag
}
// We don't have an emphasis
return false;
}
private bool TryParseEndOfLinkOrImage(ref Inline current, StringLineGroup text)
{
LinkDelimiterInline firstParent = null;
foreach (var parent in current.FindParentOfType<LinkDelimiterInline>())
{
firstParent = parent;
break;
}
// This will be matched as a literal
if (firstParent != null)
{
// TODO: continue parsing of ]
var savePoint = text.Save();
var link = string.Empty;
var title = string.Empty;
if (text.Current == '(')
{
if (TryParseLinkAndTitle(text, out link, out title))
{
// Inline Link
}
else
{
text.Restore(ref savePoint);
}
}
}
else {
// We have a nested [ ]
// firstParent.Remove();
// The opening [ will be transformed to a literal followed by all the childrens of the [
var literal = new LiteralInline() { Content = firstParent.IsImage ? "![" : "[" };
current = firstParent.ReplaceBy(literal);
return true;
}
return false;
}
private bool TryParseLinkTitle(MatchInlineState state)
{
return false;
}
// Clear the StringBuilder even after in order to avoid storing things around after using it
destination.Clear();
return false;
}
}
}

View File

@@ -107,6 +107,25 @@ namespace Textamina.Markdig.Syntax
return MatchLineResult.Continue;
}
public override void Close(Block block)
{
var paragraph = block as ParagraphBlock;
if (paragraph != null)
{
var lines = paragraph.Lines;
var lineCount = lines.Count;
for (int i = 0; i < lineCount; i++)
{
var line = lines[i];
line.TrimStart();
}
if (lineCount > 0)
{
lines[lineCount - 1].TrimEnd();
}
}
}
}
}
}

View File

@@ -182,6 +182,34 @@ namespace Textamina.Markdig.Syntax
return new StringSlice(Text, Start, End);
}
public void TrimStart()
{
// Strip leading spaces
var c = Current;
while (Utility.IsSpace(c))
{
c = NextChar();
}
}
public void TrimEnd()
{
for (int i = End; i >= Start; i--)
{
End = i;
if (!Utility.IsSpace(this[i]))
{
break;
}
}
}
public void Trim()
{
TrimStart();
TrimEnd();
}
public override string ToString()
{
return Start <= End ? Text.Substring(Start, End - Start + 1) : string.Empty;

View File

@@ -1,13 +1,24 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using Textamina.Markdig.Parsing;
namespace Textamina.Markdig.Syntax
{
public class StringLineGroup : List<StringLine>
public class StringLineGroup : Collection<StringLine>
{
private StringLine currentLine;
public StringLineGroup()
{
}
public StringLineGroup(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
Add(new StringLine(text));
}
public int LinePosition { get; private set; }
public int ColumnPosition { get; private set; }
@@ -18,13 +29,50 @@ namespace Textamina.Markdig.Syntax
public char PreviousChar2 { get; private set; }
internal void Initialize()
public void Reset()
{
ColumnPosition = -1;
currentLine = Count > 0 ? this[0] : null;
if (currentLine != null)
{
ColumnPosition = currentLine.Start - 1;
}
NextChar();
}
protected override void ClearItems()
{
base.ClearItems();
Reset();
}
protected override void InsertItem(int index, StringLine item)
{
base.InsertItem(index, item);
if (index == 0)
{
Reset();
}
}
protected override void SetItem(int index, StringLine item)
{
base.SetItem(index, item);
if (index == 0)
{
Reset();
}
}
protected override void RemoveItem(int index)
{
base.RemoveItem(index);
if (index == 0)
{
Reset();
}
}
public bool IsEndOfLines => Current == '\0';
public State Save()
@@ -55,9 +103,13 @@ namespace Textamina.Markdig.Syntax
}
else
{
ColumnPosition = -1;
LinePosition++;
currentLine = LinePosition < Count ? this[LinePosition] : null;
ColumnPosition = -1;
if (currentLine != null)
{
ColumnPosition = currentLine.Start - 1;
}
Current = currentLine != null ? '\n' : '\0';
}
}
@@ -69,6 +121,17 @@ namespace Textamina.Markdig.Syntax
return Current;
}
public bool SkipWhiteSpaces()
{
bool hasWhitespaces = false;
while (Utility.IsWhiteSpace(Current))
{
NextChar();
hasWhitespaces = true;
}
return hasWhitespaces;
}
public void TrimStart()
{
if (Count == 0)
@@ -77,11 +140,8 @@ namespace Textamina.Markdig.Syntax
}
// Strip leading and
var liner = this[0];
while (Utility.IsSpace(liner.Current))
{
liner.NextChar();
}
this[0].TrimStart();
Reset();
}
public void TrimEnd()
@@ -91,15 +151,8 @@ namespace Textamina.Markdig.Syntax
return;
}
var liner = this[Count - 1];
for (int i = liner.End; i >= liner.Start; i--)
{
liner.End = i;
if (!Utility.IsSpace(liner[i]))
{
break;
}
}
this[Count - 1].TrimEnd();
Reset();
}
public void Trim()
@@ -108,6 +161,21 @@ namespace Textamina.Markdig.Syntax
TrimEnd();
}
public override string ToString()
{
var stringBuilder = new StringBuilder();
bool firstLine = true;
foreach (var line in this)
{
if (!firstLine)
{
stringBuilder.Append('\n');
}
stringBuilder.Append(line);
firstLine = false;
}
return stringBuilder.ToString();
}
public class State
{