diff --git a/Build/Sprache.proj b/Build/Sprache.proj index 70d5e29..33a3017 100755 --- a/Build/Sprache.proj +++ b/Build/Sprache.proj @@ -50,7 +50,7 @@ - + diff --git a/README.md b/README.md index 0737922..fae73f6 100755 --- a/README.md +++ b/README.md @@ -5,40 +5,62 @@ It doesn't compete with "industrial strength" language workbenches - it fits som Usage ----- +[![Join the chat at https://gitter.im/sprache/Sprache](https://badges.gitter.im/sprache/Sprache.svg)](https://gitter.im/sprache/Sprache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + Unlike most parser-building frameworks, you use Sprache directly from your program code, and don't need to set up any build-time code generation tasks. Sprache itself is a single tiny assembly. A simple parser might parse a sequence of characters: - // Parse any number of capital 'A's in a row - var parseA = Parse.Char('A').AtLeastOnce(); +```csharp +// Parse any number of capital 'A's in a row +var parseA = Parse.Char('A').AtLeastOnce(); +``` Sprache provides a number of built-in functions that can make bigger parsers from smaller ones, often callable via Linq query comprehensions: - Parser identifier = - from leading in Parse.Whitespace.Many() - from first in Parse.Letter.Once() - from rest in Parse.LetterOrDigit.Many() - from trailing in Parse.Whitespace.Many() - select new string(first.Concat(rest).ToArray()); +```csharp +Parser identifier = + from leading in Parse.WhiteSpace.Many() + from first in Parse.Letter.Once() + from rest in Parse.LetterOrDigit.Many() + from trailing in Parse.WhiteSpace.Many() + select new string(first.Concat(rest).ToArray()); + +var id = identifier.Parse(" abc123 "); - var id = identifier.Parse(" abc123 "); +Assert.AreEqual("abc123", id); +``` - Assert.AreEqual("abc123", id); - -Background and Tutorials ------------------------- +Examples and Tutorials +---------------------- -The best place to start is [this introductory article.](http://nblumhardt.com/2010/01/building-an-external-dsl-in-c/) +The best place to start is [this introductory article](http://nblumhardt.com/2010/01/building-an-external-dsl-in-c/). -Examples included with the source demonstrate: +**Examples** included with the source demonstrate: * [Parsing XML directly to a Document object](https://github.com/sprache/Sprache/blob/master/src/XmlExample/Program.cs) -* [Parsing numeric expressions to System.Linq.Expression objects](https://github.com/sprache/Sprache/blob/master/src/LinqyCalculator/ExpressionParser.cs) -* [Parsing comma-separated (CSV) 'files' into lists of strings](https://github.com/sprache/Sprache/blob/master/src/Sprache.Tests/Scenarios/CsvTests.cs) +* [Parsing numeric expressions to `System.Linq.Expression` objects](https://github.com/sprache/Sprache/blob/master/src/LinqyCalculator/ExpressionParser.cs) +* [Parsing comma-separated values (CSV) into lists of strings](https://github.com/sprache/Sprache/blob/master/src/Sprache.Tests/Scenarios/CsvTests.cs) + +**Tutorials** explaining Sprache: + + * A great [CodeProject article by Alexey Yakovlev ](http://www.codeproject.com/Articles/795056/Sprache-Calc-building-yet-another-expression-evalu) (and [in Russian](http://habrahabr.ru/post/228037/)) + * Mike Hadlow's example of [parsing connection strings](http://mikehadlow.blogspot.com.au/2012/09/parsing-connection-string-with-sprache.html) + +**Real-world** parsers built with Sprache: + + * The [template parser](https://github.com/OctopusDeploy/Octostache/blob/master/source/Octostache/Templates/TemplateParser.cs) in [Octostache](https://github.com/OctopusDeploy/Octostache), the variable substitution language of [Octopus Deploy](https://octopus.com) + * The [XAML binding expression parser](https://github.com/SuperJMN/OmniXAML/blob/master/Source/OmniXaml/Parsers/MarkupExtensions/MarkupExtensionParser.cs) in [OmniXaml](https://github.com/SuperJMN/OmniXAML), the cross-platform XAML implementation + * The query parser in [Seq](https://getseq.net), a structured log server for .NET + * The [connection string parser](https://github.com/EasyNetQ/EasyNetQ/blob/master/Source/EasyNetQ/ConnectionString/ConnectionStringGrammar.cs) in [EasyNetQ](http://easynetq.com/), a .NET API for RabbitMQ + * Sprache appears in the [credits for JetBrains ReSharper](https://confluence.jetbrains.com/display/ReSharper/Third-Party+Software+Shipped+With+ReSharper#Third-PartySoftwareShippedWithReSharper-Sprache) + +Background +---------- -Parser combinators are covered extensively on the web. The original paper describing the monadic implementation by [Graham Hutton and Eric Meijer](http://www.cs.nott.ac.uk/~gmh/monparsing.pdf) is very readable. Sprache grew out of some exercises in [Hutton's Haskell book](http://www.amazon.com/Programming-Haskell-Graham-Hutton/dp/0521692695). +Parser combinators are covered extensively on the web. The original paper describing the monadic implementation by [Graham Hutton and Eric Meijer](http://www.cs.nott.ac.uk/~gmh/monparsing.pdf) is very readable. Sprache was originally written by [Nicholas Blumhardt](http://nblumhardt.com) and grew out of some exercises in [Hutton's Haskell book](http://www.amazon.com/Programming-Haskell-Graham-Hutton/dp/0521692695). -Sprache itself draws on some great C# tutorials: +The implementation of Sprache draws on ideas from: * [Luke Hoban's Blog](http://blogs.msdn.com/b/lukeh/archive/2007/08/19/monadic-parser-combinators-using-c-3-0.aspx) * [Brian McNamara's Blog](http://lorgonblog.wordpress.com/2007/12/02/c-3-0-lambda-and-the-first-post-of-a-series-about-monadic-parser-combinators/) diff --git a/Tools/NuGet/NuGet.exe b/Tools/NuGet/NuGet.exe index e313ff9..c41a0d0 100755 Binary files a/Tools/NuGet/NuGet.exe and b/Tools/NuGet/NuGet.exe differ diff --git a/src/LinqyCalculator/LinqyCalculator.csproj b/src/LinqyCalculator/LinqyCalculator.csproj index 9b4bf46..9baf8ff 100644 --- a/src/LinqyCalculator/LinqyCalculator.csproj +++ b/src/LinqyCalculator/LinqyCalculator.csproj @@ -80,7 +80,7 @@ - {DF5FE6F0-5ABE-4363-9184-EB6EF64F0F61} + {82cc8cf8-b770-4756-850a-95dd14dd312f} Sprache diff --git a/src/Sprache.Tests/AssertInput.cs b/src/Sprache.Tests/AssertInput.cs index 19037f4..d5debd5 100644 --- a/src/Sprache.Tests/AssertInput.cs +++ b/src/Sprache.Tests/AssertInput.cs @@ -6,7 +6,7 @@ namespace Sprache.Tests { static class AssertInput { - public static Input AdvanceMany(this Input input, int count) + public static IInput AdvanceMany(this IInput input, int count) { for (int i = 0; i < count; i++) { @@ -16,14 +16,14 @@ public static Input AdvanceMany(this Input input, int count) return input; } - public static Input AdvanceAssert(this Input input, Action assertion) + public static IInput AdvanceAssert(this IInput input, Action assertion) { var result = input.Advance(); assertion(input, result); return result; } - public static Input AdvanceManyAssert(this Input input, int count, Action assertion) + public static IInput AdvanceManyAssert(this Input input, int count, Action assertion) { var result = input.AdvanceMany(count); assertion(input, result); diff --git a/src/Sprache.Tests/AssertParser.cs b/src/Sprache.Tests/AssertParser.cs index 57d2112..1a814af 100644 --- a/src/Sprache.Tests/AssertParser.cs +++ b/src/Sprache.Tests/AssertParser.cs @@ -31,7 +31,7 @@ public static void SucceedsWith(Parser parser, string input, Action res parser.TryParse(input) .IfFailure(f => { - Assert.Fail("Parsing of \"{0}\" failed unexpectedly at position {1}: {2}", input, f.Remainder.Position, f.Message); + Assert.Fail("Parsing of \"{0}\" failed unexpectedly. {1}", input, f); return f; }) .IfSuccess(s => diff --git a/src/Sprache.Tests/DecimalTests.cs b/src/Sprache.Tests/DecimalTests.cs index 944ed68..a616ae9 100644 --- a/src/Sprache.Tests/DecimalTests.cs +++ b/src/Sprache.Tests/DecimalTests.cs @@ -8,6 +8,7 @@ namespace Sprache.Tests public class DecimalTests { private static readonly Parser DecimalParser = Parse.Decimal.End(); + private static readonly Parser DecimalInvariantParser = Parse.DecimalInvariant.End(); private CultureInfo _previousCulture; @@ -49,5 +50,12 @@ public void Letters() { DecimalParser.Parse("1A.5"); } + + [Test] + public void LeadingDigitsInvariant() + { + Assert.AreEqual("12.23", DecimalInvariantParser.Parse("12.23")); + } + } } \ No newline at end of file diff --git a/src/Sprache.Tests/InputTests.cs b/src/Sprache.Tests/InputTests.cs index 132242d..e553b68 100644 --- a/src/Sprache.Tests/InputTests.cs +++ b/src/Sprache.Tests/InputTests.cs @@ -96,7 +96,7 @@ public void AdvancingInputAtEOL_ResetsColumnNumber() [Test] public void LineCountingSmokeTest() { - var i = new Input("abc\ndef"); + IInput i = new Input("abc\ndef"); Assert.AreEqual(0, i.Position); Assert.AreEqual(1, i.Line); Assert.AreEqual(1, i.Column); diff --git a/src/Sprache.Tests/ParseTests.cs b/src/Sprache.Tests/ParseTests.cs index 62ce10c..3baa22e 100644 --- a/src/Sprache.Tests/ParseTests.cs +++ b/src/Sprache.Tests/ParseTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -152,12 +153,6 @@ from rest in Parse.Char('a').Once() select first.Concat(rest)) .Or(Parse.Char('a').Once()); - [Test, Ignore("Not Implemented")] - public void CanParseLeftRecursiveGrammar() - { - AssertParser.SucceedsWith(ASeq.End(), "a,a,a", r => Assert.AreEqual(new string(r.ToArray()), "aaa")); - } - [Test] public void DetectsLeftRecursion() { @@ -176,12 +171,6 @@ from rest in Parse.Char('b').Once() select first.Concat(rest)) .Or(Parse.Char('b').Once()); - [Test, Ignore("Not Implemented")] - public void CanParseMutuallyLeftRecursiveGrammar() - { - AssertParser.SucceedsWithAll(ABSeq.End(), "baba"); - } - [Test] public void DetectsMutualLeftRecursion() { @@ -270,6 +259,26 @@ public void RegexParserDoesNotConsumeInputOnFailedMatch() Assert.AreEqual(0, r.Remainder.Position); } + [Test] + public void RegexMatchParserConsumesInputOnSuccessfulMatch() + { + var digits = Parse.RegexMatch(@"\d(\d*)"); + var r = digits.TryParse("123d"); + Assert.IsTrue(r.WasSuccessful); + Assert.AreEqual("123", r.Value.Value); + Assert.AreEqual("23", r.Value.Groups[1].Value); + Assert.AreEqual(3, r.Remainder.Position); + } + + [Test] + public void RegexMatchParserDoesNotConsumeInputOnFailedMatch() + { + var digits = Parse.RegexMatch(@"\d+"); + var r = digits.TryParse("d123"); + Assert.IsFalse(r.WasSuccessful); + Assert.AreEqual(0, r.Remainder.Position); + } + [Test] public void PositionedParser() { @@ -348,6 +357,57 @@ public void RepeatParserCanParseWithCountOfZero() Assert.AreEqual(0, r.Remainder.Position); } + [Test] + public void RepeatParserCanParseAMinimumNumberOfValues() + { + var repeated = Parse.Char('a').Repeat(4, 5); + + // Test failure. + var r = repeated.TryParse("aaa"); + Assert.IsFalse(r.WasSuccessful); + Assert.AreEqual(0, r.Remainder.Position); + + // Test success. + r = repeated.TryParse("aaaa"); + Assert.IsTrue(r.WasSuccessful); + Assert.AreEqual(4, r.Remainder.Position); + } + + [Test] + public void RepeatParserCanParseAMaximumNumberOfValues() + { + var repeated = Parse.Char('a').Repeat(4, 5); + + var r = repeated.TryParse("aaaa"); + Assert.IsTrue(r.WasSuccessful); + Assert.AreEqual(4, r.Remainder.Position); + + r = repeated.TryParse("aaaaa"); + Assert.IsTrue(r.WasSuccessful); + Assert.AreEqual(5, r.Remainder.Position); + + r = repeated.TryParse("aaaaaa"); + Assert.IsTrue(r.WasSuccessful); + Assert.AreEqual(5, r.Remainder.Position); + } + + [Test] + public void RepeatParserErrorMessagesAreReadable() + { + var repeated = Parse.Char('a').Repeat(4, 5); + + var expectedMessage = "Parsing failure: Unexpected 'end of input'; expected 'a' between 4 and 5 times, but found 3"; + + try + { + var r = repeated.Parse("aaa"); + } + catch(ParseException ex) + { + Assert.That(ex.Message, Is.StringStarting(expectedMessage)); + } + } + [Test] public void CanParseSequence() { @@ -357,6 +417,17 @@ public void CanParseSequence() Assert.IsTrue(r.Remainder.AtEnd); } + [Test] + public void FailGracefullyOnSequence() + { + var sequence = Parse.Char('a').XDelimitedBy(Parse.Char(',')); + AssertParser.FailsWith(sequence, "a,a,b", result => + { + StringAssert.Contains("unexpected 'b'", result.Message); + CollectionAssert.Contains(result.Expectations, "a"); + }); + } + [Test] public void CanParseContained() { diff --git a/src/Sprache.Tests/RegexTests.cs b/src/Sprache.Tests/RegexTests.cs new file mode 100644 index 0000000..fc53f42 --- /dev/null +++ b/src/Sprache.Tests/RegexTests.cs @@ -0,0 +1,90 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using NUnit.Framework; + +namespace Sprache.Tests +{ + /// + /// These tests exist in order to verify that the modification that is applied to + /// the regex passed to every call to the + /// or methods does not change the results + /// in any way. + /// + public class RegexTests + { + private const string _startsWithCarrot = "^([a-z]{3})([0-9]{3})$"; + private const string _alternation = "(this)|(that)|(the other)"; + + private static readonly MethodInfo _optimizeRegexMethod = typeof(Parse).GetMethod("OptimizeRegex", BindingFlags.NonPublic | BindingFlags.Static); + + [Test] + public void OptimizedRegexIsNotSuccessfulWhenTheMatchIsNotAtTheBeginningOfTheInput() + { + var regexOriginal = new Regex("[a-z]+"); + var regexOptimized = OptimizeRegex(regexOriginal); + + const string input = "123abc"; + + Assert.That(regexOriginal.IsMatch(input), Is.True); + Assert.That(regexOptimized.IsMatch(input), Is.False); + } + + [Test] + public void OptimizedRegexIsSuccessfulWhenTheMatchIsAtTheBeginningOfTheInput() + { + var regexOriginal = new Regex("[a-z]+"); + var regexOptimized = OptimizeRegex(regexOriginal); + + const string input = "abc123"; + + Assert.That(regexOriginal.IsMatch(input), Is.True); + Assert.That(regexOptimized.IsMatch(input), Is.True); + } + + [TestCase(_startsWithCarrot, RegexOptions.None, "abc123", TestName = "Starts with ^, no options, success")] + [TestCase(_startsWithCarrot, RegexOptions.ExplicitCapture, "abc123", TestName = "Starts with ^, explicit capture, success")] + [TestCase(_startsWithCarrot, RegexOptions.None, "123abc", TestName = "Starts with ^, no options, failure")] + [TestCase(_startsWithCarrot, RegexOptions.ExplicitCapture, "123abc", TestName = "Starts with ^, explicit capture, failure")] + [TestCase(_alternation, RegexOptions.None, "abc123", TestName = "Alternation, no options, success")] + [TestCase(_alternation, RegexOptions.ExplicitCapture, "that", TestName = "Alternation, explicit capture, success")] + [TestCase(_alternation, RegexOptions.None, "that", TestName = "Alternation, no options, failure")] + [TestCase(_alternation, RegexOptions.ExplicitCapture, "that", TestName = "Alternation, explicit capture, failure")] + public void RegexOptimizationDoesNotChangeRegexBehavior(string pattern, RegexOptions options, string input) + { + var regexOriginal = new Regex(pattern, options); + var regexOptimized = OptimizeRegex(regexOriginal); + + var matchOriginal = regexOriginal.Match(input); + var matchModified = regexOptimized.Match(input); + + Assert.That(matchModified.Success, Is.EqualTo(matchOriginal.Success)); + Assert.That(matchModified.Value, Is.EqualTo(matchOriginal.Value)); + Assert.That(matchModified.Groups.Count, Is.EqualTo(matchOriginal.Groups.Count)); + + for (int i = 0; i < matchModified.Groups.Count; i++) + { + Assert.That(matchModified.Groups[i].Success, Is.EqualTo(matchOriginal.Groups[i].Success)); + Assert.That(matchModified.Groups[i].Value, Is.EqualTo(matchOriginal.Groups[i].Value)); + } + } + + /// + /// Calls the method via reflection. + /// + private static Regex OptimizeRegex(Regex regex) + { + // Reflection isn't the best way of verifying behavior, + // but cluttering the public api sucks worse. + + if (_optimizeRegexMethod == null) + { + throw new Exception("Unable to locate a private static method named " + + "\"OptimizeRegex\" in the Parse class. Has it been renamed?"); + } + + var optimizedRegex = (Regex)_optimizeRegexMethod.Invoke(null, new object[] { regex }); + return optimizedRegex; + } + } +} diff --git a/src/Sprache.Tests/Scenarios/AssemblerTests.cs b/src/Sprache.Tests/Scenarios/AssemblerTests.cs new file mode 100644 index 0000000..c8216a3 --- /dev/null +++ b/src/Sprache.Tests/Scenarios/AssemblerTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace Sprache.Tests.Scenarios +{ + public class AssemblerParser + { + public static Parser Identifier = Parse.Identifier(Parse.Letter, Parse.LetterOrDigit); + + public static Parser AsmToken(Parser parser) + { + var whitespace = Parse.WhiteSpace.Except(Parse.LineEnd); + return from leading in whitespace.Many() + from item in parser + from trailing in whitespace.Many() + select item; + } + + public static Parser> Instruction = + from instructionName in AsmToken(Parse.LetterOrDigit.Many().Text()) + from operands in AsmToken(Identifier).XDelimitedBy(Parse.Char(',')) + select Tuple.Create(instructionName, operands.ToArray()); + + public static CommentParser Comment = new CommentParser { Single = ";", NewLine = Environment.NewLine }; + + public static Parser LabelId = + Parse.Identifier(Parse.Letter.Or(Parse.Chars("._?")), Parse.LetterOrDigit.Or(Parse.Chars("_@#$~.?"))); + + public static Parser Label = + from labelName in AsmToken(LabelId) + from colon in AsmToken(Parse.Char(':')) + select labelName; + + public static readonly Parser> Assembler = ( + from label in Label.Optional() + from instruction in Instruction.Optional() + from comment in AsmToken(Comment.SingleLineComment).Optional() + from lineTerminator in Parse.LineTerminator + select new AssemblerLine( + label.GetOrDefault(), + instruction.IsEmpty ? null : instruction.Get().Item1, + instruction.IsEmpty ? null : instruction.Get().Item2, + comment.GetOrDefault() + ) + ).XMany().End(); + } + + [TestFixture] + public class AssemblerTests + { + [Test] + public void CanParseEmpty() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, "", CollectionAssert.IsEmpty); + } + + [Test] + public void CanParseComment() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, ";comment", + lines => Assert.AreEqual(new AssemblerLine(null, null, null, "comment"), lines.Single())); + } + + [Test] + public void CanParseCommentWithSpaces() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, " ; comment ", + lines => Assert.AreEqual(new AssemblerLine(null, null, null, " comment "), lines.Single())); + } + + [Test] + public void CanParseLabel() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, "label:", + lines => Assert.AreEqual(new AssemblerLine("label", null, null, null), lines.Single())); + } + + [Test] + public void CanParseLabelWithSpaces() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, " label : ", + lines => Assert.AreEqual(new AssemblerLine("label", null, null, null), lines.Single())); + } + + [Test] + public void CanParseIntruction() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, "mov a,b", + lines => Assert.AreEqual(new AssemblerLine(null, "mov", new[] { "a", "b" }, null), lines.Single())); + } + + [Test] + public void CanParseIntructionWithSpaces() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, " mov a , b ", + lines => Assert.AreEqual(new AssemblerLine(null, "mov", new[] { "a", "b" }, null), lines.Single())); + } + + [Test] + public void CanParseAllTogether() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, " label: mov a , b ; comment ", + lines => + Assert.AreEqual(new AssemblerLine("label", "mov", new[] { "a", "b" }, " comment "), lines.Single())); + } + + [Test] + public void CanParseSeceralLines() + { + AssertParser.SucceedsWith(AssemblerParser.Assembler, + @" ;multiline sample + label: + mov a , b;", + lines => CollectionAssert.AreEqual( + new[] + { + new AssemblerLine(null, null, null, "multiline sample"), + new AssemblerLine("label", null, null, null), + new AssemblerLine(null, "mov", new[] {"a", "b"}, "") + }, + lines)); + } + } + + public class AssemblerLine + { + public readonly string Label; + public readonly string InstructionName; + public readonly string[] Operands; + public readonly string Comment; + + public AssemblerLine(string label, string instructionName, string[] operands, string comment) + { + Label = label; + InstructionName = instructionName; + Operands = operands; + Comment = comment; + } + + protected bool Equals(AssemblerLine other) + { + return ToString() == other.ToString(); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((AssemblerLine)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (Label != null ? Label.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (InstructionName != null ? InstructionName.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Operands != null ? Operands.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (Comment != null ? Comment.GetHashCode() : 0); + return hashCode; + } + } + + public override string ToString() + { + return string.Join(" ", + Label == null ? "" : (Label + ":"), + InstructionName == null ? "" : InstructionName + string.Join(", ", Operands), + Comment == null ? "" : ";" + Comment); + } + } +} \ No newline at end of file diff --git a/src/Sprache.Tests/Scenarios/StarDateTest.cs b/src/Sprache.Tests/Scenarios/StarDateTest.cs new file mode 100644 index 0000000..6061b7f --- /dev/null +++ b/src/Sprache.Tests/Scenarios/StarDateTest.cs @@ -0,0 +1,30 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Sprache.Tests.Scenarios +{ + [TestFixture] + public class StarDateTest + { + static readonly Parser StarTrek2009_StarDate = + from year in Parse.Digit.Many().Text() + from delimiter in Parse.Char('.') + from dayOfYear in Parse.Digit.Repeat(1,3).Text().End() + select new DateTime(int.Parse(year), 1, 1).AddDays(int.Parse(dayOfYear) - 1); + + [Test] + public void ItIsPossibleToParseAStarDate() + { + Assert.That(StarTrek2009_StarDate.Parse("2259.55"), Is.EqualTo(new DateTime(2259, 2, 24))); + } + + [Test, ExpectedException(typeof(ParseException))] + public void InvalidStarDatesAreNotParsed() + { + var date = StarTrek2009_StarDate.Parse("2259.4000"); + } + } +} diff --git a/src/Sprache.Tests/Sprache.Tests.csproj b/src/Sprache.Tests/Sprache.Tests.csproj index 61305a4..e4104f3 100644 --- a/src/Sprache.Tests/Sprache.Tests.csproj +++ b/src/Sprache.Tests/Sprache.Tests.csproj @@ -106,16 +106,13 @@ + + - - - - {DF5FE6F0-5ABE-4363-9184-EB6EF64F0F61} - Sprache - + @@ -137,6 +134,15 @@ + + + {82cc8cf8-b770-4756-850a-95dd14dd312f} + Sprache + + + + + - - Properties\Version.cs - + + + - + + + + + + - + + + Properties\Version.cs + - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - true - - - False - Windows Installer 3.1 - true - - - - - - - - - - - - + ", "\r\n"); + static readonly Parser Identifier = from first in Parse.Letter.Once() from rest in Parse.LetterOrDigit.XOr(Parse.Char('-')).XOr(Parse.Char('_')).Many() @@ -81,10 +84,14 @@ from end in EndTag(tag) static readonly Parser ShortNode = Tag(from id in Identifier from slash in Parse.Char('/') select new Node { Name = id }); - + static readonly Parser Node = ShortNode.Or(FullNode); - static readonly Parser Item = Node.Select(n => (Item)n).XOr(Content); + static readonly Parser Item = + from leading in Comment.MultiLineComment.Many() + from item in Node.Select(n => (Item)n).XOr(Content) + from trailing in Comment.MultiLineComment.Many() + select item; public static readonly Parser Document = from leading in Parse.WhiteSpace.Many() @@ -96,8 +103,9 @@ class Program { static void Main() { - const string doc = "

hello,
world!

"; - var parsed = XmlParser.Document.Parse(doc); + StreamReader reader = new StreamReader("TestFile.xml"); + var parsed = XmlParser.Document.Parse(reader.ReadToEnd()); + reader.Close(); Console.WriteLine(parsed); Console.ReadKey(true); } diff --git a/src/XmlExample/TestFile.xml b/src/XmlExample/TestFile.xml new file mode 100644 index 0000000..f6aae14 --- /dev/null +++ b/src/XmlExample/TestFile.xml @@ -0,0 +1,7 @@ + +

+ hello,
world! +

+ \ No newline at end of file diff --git a/src/XmlExample/XmlExample.csproj b/src/XmlExample/XmlExample.csproj index 9d56df8..b05958e 100644 --- a/src/XmlExample/XmlExample.csproj +++ b/src/XmlExample/XmlExample.csproj @@ -103,12 +103,6 @@ - - - {DF5FE6F0-5ABE-4363-9184-EB6EF64F0F61} - Sprache - - False @@ -129,6 +123,17 @@ + + + {82cc8cf8-b770-4756-850a-95dd14dd312f} + Sprache + + + + + Always + +