Разбор списка списков с помощью Superpower

Я хотел бы разобрать книги библиотеки, представленные в таком формате:

#Book title 1
Chapter 1
Chapter 2
#Book title 2
Chapter 1
Chapter 2
Chapter 3

Как видите, заголовкам загрузки предшествует символ #, а главы каждой книги - это следующие строки. Создать для этого парсер должно быть довольно просто.

Пока у меня есть такой код (парсеры + токенизатор):

void Main()
{
    var tokenizer = new TokenizerBuilder<PrjToken>()
                    .Match(Superpower.Parsers.Character.EqualTo('#'), PrjToken.Hash)
                    .Match(Span.Regex("[^\r\n#:=-]*"), PrjToken.Text)
                    .Match(Span.WhiteSpace, PrjToken.WhiteSpace)
                    .Build();


    var input = @"#Book 1
Chapter 1
Chapter 2
#Book 2
Chapter 1
Chapter 2
Chapter 3";

    var library = MyParsers.Library.Parse(tokenizer.Tokenize(input));
}


public enum PrjToken
{
    WhiteSpace,
    Hash,
    Text
}


public class Book
{
    public string Title { get; }
    public string[] Chapters { get; }

    public Book(string title, string[] chapters)
    {
        Title = title;
        Chapters = chapters;
    }
}

public class Library
{
    public Book[] Books { get; }

    public Library(Book[] books)
    {
        Books = books;
    }
}


public class MyParsers
{
    public static readonly TokenListParser<PrjToken, string> Text = from text in Token.EqualTo(PrjToken.Text)
                                                                    select text.ToStringValue();

    public static readonly TokenListParser<PrjToken, Superpower.Model.Token<PrjToken>> Whitespace = from text in Token.EqualTo(PrjToken.WhiteSpace)
                                                                                   select text;

    public static readonly TokenListParser<PrjToken, string> Title =
        from hash in Token.EqualTo(PrjToken.Hash)
        from text in Text
        from wh in Whitespace
        select text;

    public static readonly TokenListParser<PrjToken, Book> Book =
        from title in Title
        from chapters in Text.ManyDelimitedBy(Whitespace)
        select new Book(title, chapters);

    public static readonly TokenListParser<PrjToken, Library> Library =
        from books in Book.ManyDelimitedBy(Whitespace)
        select new Library(books);
}

Приведенный выше код готов к запуску в.NET Fiddle по этой ссылке https://dotnetfiddle.net/3P5dAJ

Все выглядит нормально. Однако что-то не так с парсером, потому что я получаю такую ​​ошибку:

Синтаксическая ошибка (строка 4, столбец 1): неожиданный хэш #, ожидаемый текст.

Что не так с моими парсерами?

1 ответ

Решение

Вы можете решить эту проблему, анализируя главы в виде отдельного списка, где каждая глава заканчивается символом пробела:

    public static readonly TokenListParser<PrjToken, string> Chapter =
        from chapterName in Text
        from wh in Whitespace
        select chapterName;

    public static readonly TokenListParser<PrjToken, Book> Book =
        from title in Title
        from chapters in Chapter.Many()
        select new Book(title, chapters);

По сути, я думаю, что когда Text.ManyDelimitedBy(Whitespace) встречает завершающий пробел (новую строку) в конце Chapter 2 он будет ожидать другого экземпляра названия главы, а не начала новой книги.

Парсер не может различить разделитель между Chapters и разделитель между Books (оба пробела (новая строка)), и поэтому он будет ожидать другую главу, а не начало новой Book.

Разбив парсер главы на Text за которым следует Whitespace токен вы преодолели эту двусмысленность.

Поскольку теперь вы проглотили Whitespace в конце главы каждая книга не ограничена Whitespace, и вам нужно изменить способ Book парсер тоже работает:

    public static readonly TokenListParser<PrjToken, Book> Book =
        from title in Title
        from chapters in Chapter.Many()
        select new Book(title, chapters);

В дополнение к этому, если вы хотите, чтобы файл анализировался без новой строки в конце файла, вы также должны сделать Whitespace в конце Chapter быть необязательным:

    public static readonly TokenListParser<PrjToken, string> Chapter =
        from chapterName in Text
        from wh in Whitespace.Optional()
        select chapterName;

В итоге мы получаем (Полный парсер):

public class MyParsers
{
    public static readonly TokenListParser<PrjToken, string> Text = from text in Token.EqualTo(PrjToken.Text)
        select text.ToStringValue();

    public static readonly TokenListParser<PrjToken, Superpower.Model.Token<PrjToken>> Whitespace = from text in Token.EqualTo(PrjToken.WhiteSpace)
        select text;

    public static readonly TokenListParser<PrjToken, string> Title =
        from hash in Token.EqualTo(PrjToken.Hash)
        from text in Text
        from wh in Whitespace
        select text;

    public static readonly TokenListParser<PrjToken, string> Chapter =
        from chapterName in Text
        from wh in Whitespace.Optional()
        select chapterName;

    public static readonly TokenListParser<PrjToken, Book> Book =
        from title in Title
        from chapters in Chapter.Many()
        select new Book(title, chapters);

    public static readonly TokenListParser<PrjToken, Library> Library =
        from books in Book.Many()
        select new Library(books);
}
Другие вопросы по тегам