Parsec, читать текст, заканчивающийся строкой

Я борюсь с Parsec проанализировать небольшое подмножество синтаксиса вики проекта Google и преобразовать его в HTML. Мой синтаксис ограничен текстовыми последовательностями и списками предметов. Вот пример того, что я хочу узнать:

Text that can contain any kind of characters,
except the string "\n *"
 * list item 1
 * list item 2

End of list 

Мой код до сих пор:

import Text.Blaze.Html5 (Html, toHtml)
import qualified Text.Blaze.Html5 as H
import Text.ParserCombinators.Parsec hiding (spaces)

parseList :: Parser Html
parseList = do
    items <- many1 parseItem
    return $ H.ul $ sequence_ items

parseItem :: Parser Html
parseItem = do
    string "\n *"
    item <- manyTill anyChar $
        (try $ lookAhead $ string "\n *") <|>
        (try $ string "\n\n")
    return $ H.li $ toHtml item

parseText :: Parser Html
parseText = do
    text <- manyTill anyChar $
        (try $ lookAhead $ string "\n *") <|>
        (eof >> (string ""))
    return $ toHtml text

parseAll :: Parser Html
parseAll = do
    l <- many (parseUl <|> parseText)
    return $ H.html $ sequence_ l

При подаче заявления parseAll для любой последовательности символов я получаю следующее сообщение об ошибке: "*** Exception: Text.ParserCombinators.Parsec.Prim.many: combinator 'many' is applied to a parser that accepts an empty string.Я так понимаю это потому что мой парсер parseText могу читать пустые строки, но я не вижу другого пути. Как я могу распознать текст, разделенный строкой? ("\n *" Вот).

Я также открыт для любых замечаний или предложений относительно того, как я использую Parsec. Я не могу помочь, но вижу, что мой код немного уродлив. Могу ли я сделать все это более простым способом? Например, есть репликация кода (что довольно болезненно) из-за строки "\n *", который используется для распознавания конца текстовой последовательности, начала элемента списка И конца элемента списка...

2 ответа

Решение
parseItem :: Parser String
parseItem = do
    manyTill anyChar $
        (try $ lookAhead $ string "\n *") <|>
        (try $ string "\n\n")

parseText :: Parser [String]
parseText = 
  string "\n *" >> -- remove this if text *can't* contain a leading '\n *'
  sepBy1 parseItem (string "\n *")

Я удалил материал HTML, потому что по какой-то причине я не мог получить blaze-html установить на мою машину. Но в принципе это должно быть по сути то же самое. Это анализирует строки, разделенные строкой "\n *" и заканчивающиеся строкой "\n\n". Я не знаю, есть ли ведущий \n это то, что вы хотите, но это легко исправить.

Кроме того, я не знаю, допустима ли пустая строка. Вы должны изменить sepBy1 в sepBy если это.

Что касается ошибки, которую вы получили: у вас есть string "" Внутри many, Это не только дает ошибку, которую вы получили, это не имеет никакого смысла! Парсер string "" всегда будет успешным без использования чего-либо, так как пустая строка является префиксом всех строк и "" ++ x == x, Если вы попытаетесь сделать это несколько раз, вы никогда не закончите анализ.

Помимо всего этого, ваш parseList должен разобрать ваш язык. По сути, это то же самое, что sepBy делает. я просто думаю sepBy чище:)

Проблема заключается в manyTill комбинатор соответствует нулю или более anyChar. Просто измените parseText, чтобы он соответствовал хотя бы одному anyChar, чтобы он не работал при чтении одного из разделителей - к сожалению, нет many1Till комбинатор.

Также я предпочитаю parseAll = fmap (H.html . sequence) $ many (parseUl <|> parseText), так как вы упомянули советы уродства.

parseText = do
               notFollowedBy $ string "\n *"
               first <- anyChar
               rest <- manyTill anyChar $
                       (try $ lookAhead $ string "\n *") <|>
                       (eof >> (string ""))
               return $ toHtml first:rest

parseAll = fmap (H.html . sequence) $ many (parseUl <|> parseText)

Тем не менее, "parseUl" в Google дает только этот вопрос, поэтому я не знаю лучшего решения без понимания этого парсера.


Отчаявшись о своем первом принятом ответе, я написал его полностью:), просто добавьте html-материал сверху с помощью fmap (предпочтительно) или return.

module Main where
import System.Environment
import Control.Monad
import Text.ParserCombinators.Parsec hiding (spaces)

parseList :: Parser [String]
parseList = many1 parseItem

parseItem :: Parser String
parseItem = string "\n *" >> (manyTill anyChar $ try $ lookAhead $ char '\n')

parseText :: Parser String
parseText = do
               notFollowedBy $ string "\n *" 
               first <- anyChar
               rest <- manyTill anyChar $
                   (try $ lookAhead $  string "\n *") <|>
                   (eof >> (string ""))
               return $ first:rest

parseAll :: Parser [String]
parseAll = many $ parseText <|> fmap concat parseList

parseIt :: String -> String
parseIt input = case parse parseAll "wiki" input of
    Left err -> "No match: " ++ show err
    Right val -> "It worked"

main = do
          args <- getArgs
          putStrLn (parseIt (args !! 0))

Я предполагал, что списки не могут содержать переводы строк, но try $ lookahead $ char '\n' легко настраивается. Вы можете выделить string "\n *" чтобы избежать дублирования. Здесь я раздавил все списки и проигнорировал анализ с последовательностью, но вам придется изменить это. Было бы проще, если бы вы разделили "текст" на строки текста, а затем просто проверили строку текста или строку из списка.

Другие вопросы по тегам