F# 使用FParsec从大部分自由格式的文本中选择块

F# 使用FParsec从大部分自由格式的文本中选择块,f#,fparsec,F#,Fparsec,我试图从大部分自由格式的文本中解析一些信息。我尝试在FParsec中实现,但是我以前没有使用过它,我不确定我是否做得不对,或者它是否适合这个特定的问题 问题描述 我想从标记文档(“examplecode”和“requiredcode”标记)中解析出一组特定标记的内容。标记将主要是自由格式的文本,偶尔在液体标记中添加块,例如: Some free form text. Possibly lots of lines. Maybe `code` stuff. {% examplecode opt-l

我试图从大部分自由格式的文本中解析一些信息。我尝试在FParsec中实现,但是我以前没有使用过它,我不确定我是否做得不对,或者它是否适合这个特定的问题

问题描述 我想从标记文档(“examplecode”和“requiredcode”标记)中解析出一组特定标记的内容。标记将主要是自由格式的文本,偶尔在液体标记中添加块,例如:

Some free form text.
Possibly lots of lines. Maybe `code` stuff.

{% examplecode opt-lang-tag %}
ABC
DEF
{% endexamplecode %}

More text. Possibly multilines.

{% othertag %}
can ignore this tag
{% endothertag %}

{% requiredcode %}
GHI
{% endrequiredcode %}
在这种情况下,我需要解析出
[“ABC\nDEF”;“GHI”]

我所追求的解析逻辑可以强制表达。在每一行中循环,如果我们找到感兴趣的开始标记,则在与结束标记匹配之前提取行,并将这些行添加到结果列表中,否则跳过行直到下一个开始标记。重复一遍

这可以通过循环或折叠完成,或者:


\{%\s*(examplecode | requiredcode)。*\%}(.*)\{%\s*end\1\s*%\}

我的FParsec尝试 我发现很难在FParsec中表达上述逻辑。我想写一些类似于
between st(everythingUntil t)
的东西,但是我不知道如何在
everythingUntil
消耗end令牌的情况下实现它,从而导致
between
失败

我最终得出以下结论,它不处理嵌套出现的
“{%”
,但似乎通过了我关心的主要测试用例:

let trimStr (s : string) = s.Trim()
let betweenStr s t = between (pstring s) (pstring t)
let allTill s = charsTillString s false maxInt
let skipAllTill s = skipCharsTillString s false maxInt
let word : Parser<string, unit> = many1Satisfy (not << Char.IsWhiteSpace)

type LiquidTag = private LiquidTag of name : string * contents : string
let makeTag n c = LiquidTag (n, trimStr c)

let liquidTag =
    let pStartTag = betweenStr "{%" "%}" (spaces >>. word .>> spaces .>> skipAllTill "%}")
    let pEndTag tagName = betweenStr "{%" "%}" (spaces >>. pstring ("end" + tagName) .>> spaces)
    let tagContents = allTill "{%"
    pStartTag >>= fun name -> 
                    tagContents 
                        .>> pEndTag name 
                        |>> makeTag name

let tags = many (skipAllTill "{%" >>. liquidTag)
问题: 有没有一种方法可以更紧密地表达FParsec中“问题描述”部分中描述的逻辑,或者输入的自由形式特性是否使FParsec比更基本的循环或正则表达式更不适合这种情况


(我还对允许在标记中嵌套
“{%”
字符串的方法感兴趣,并对我的FParsec尝试进行了改进。我很乐意根据需要将其分解为其他问题。)

我只会使用
开始>>。每一个直到结束
而不是开始-结束体之间的

以下实现与正则表达式中的逻辑相对接近:

let maxInt = System.Int32.MaxValue    
type LiquidTag = LiquidTag of string * string

let skipTillString str = skipCharsTillString str true maxInt

let skipTillStringOrEof str : Parser<unit, _> =
    fun stream -> 
        let mutable found = false
        stream.SkipCharsOrNewlinesUntilString(str, maxInt, &found) |> ignore
        Reply(())

let openingBrace = skipString "{%" >>. spaces

let tagName name = 
    skipString name 
    >>? nextCharSatisfies (fun c -> c = '%' || System.Char.IsWhiteSpace(c))

let endTag name =     
    openingBrace >>? (tagName ("end" + name) >>. (spaces >>. skipString "%}"))

let tagPair_afterOpeningBrace name = 
   tagName name  >>. skipTillString "%}"
   >>. (manyCharsTill anyChar (endTag name)
        |>> fun str -> LiquidTag(name, str))

let skipToOpeningBraceOrEof = skipTillStringOrEof "{%" 

let tagPairs =
    skipToOpeningBraceOrEof 
    >>. many (openingBrace
              >>. opt (    tagPair_afterOpeningBrace "examplecode"
                       <|> tagPair_afterOpeningBrace "requiredcode")
              .>> skipToOpeningBraceOrEof)
        |>> List.choose id
   .>> eof
让maxInt=System.Int32.MaxValue
类型LiquidTag=字符串的LiquidTag*string
让skipTillString str=skipCharsTillString str true maxInt
让skipTillStringOrEof str:Parser=
趣味流->
设mutable found=false
stream.SkipCharsOrNewlinesUntilString(str、maxInt和found)|>忽略
答复(())
让openingBrace=skipString“{%”>>。空格
让标记名名=
滑雪者名称
>>?NextCharsatifies(乐趣c->c='%'| | System.Char.IsWhiteSpace(c))
让endTag名称=
openingBrace>>?(标记名(“结束”+名称)>>(空格>>。skipString“%}”))
让tagPair_在打开支架名称后=
标记名名称>>.skipTillString“%}”
>>(manyCharsTill anyChar(endTag名称)
|>>趣味str->LiquidTag(名字,str))
设skipToOpeningBraceOrEof=skipTillStringOrEof“{%”
让标记对=
SkiptoOpeningBraceOrof
>>.许多
>>.opt(打开支架后标记对_“示例代码”
tagPair\u打开支架后的“所需代码”)
.>>SkipTopOpeningBraceOreof)
|>>列表。选择id
.>>eof
一些注意事项:

  • 我只分析你感兴趣的两个流动语句。这个 如果其中一条语句嵌套在 你不感兴趣的声明。它还有一个优点,那就是 必须在解析器运行时构造解析器

  • 我正在使用
    >?
    组合器来控制精确回溯的时间 可能发生

  • 此实现的性能不会很好,但如果需要,可以通过多种方式对其进行优化。最慢的组件可能是
    manyCharsTill anyChar(endTag name)
    解析器,可轻松替换为自定义原语。
    tagPairs
    中的
    many…|>List.choose id也可轻松替换为更高效的自定义组合符


也许您可以通过扩展我在F#::中实现的标记解析器来实现这一点。我知道这并不是直接回答问题,但请看一下代码-它应该很容易扩展,以处理以
{%
%]开头和结尾的单行
。另外,还有一个免费的书籍章节解释了如何使用活动模式(基于上述项目)编写降价解析器,这可能是类似的问题:(PDF)谢谢!所以我尝试的主要问题是回溯?还是它有更根本的缺陷?
let maxInt = System.Int32.MaxValue    
type LiquidTag = LiquidTag of string * string

let skipTillString str = skipCharsTillString str true maxInt

let skipTillStringOrEof str : Parser<unit, _> =
    fun stream -> 
        let mutable found = false
        stream.SkipCharsOrNewlinesUntilString(str, maxInt, &found) |> ignore
        Reply(())

let openingBrace = skipString "{%" >>. spaces

let tagName name = 
    skipString name 
    >>? nextCharSatisfies (fun c -> c = '%' || System.Char.IsWhiteSpace(c))

let endTag name =     
    openingBrace >>? (tagName ("end" + name) >>. (spaces >>. skipString "%}"))

let tagPair_afterOpeningBrace name = 
   tagName name  >>. skipTillString "%}"
   >>. (manyCharsTill anyChar (endTag name)
        |>> fun str -> LiquidTag(name, str))

let skipToOpeningBraceOrEof = skipTillStringOrEof "{%" 

let tagPairs =
    skipToOpeningBraceOrEof 
    >>. many (openingBrace
              >>. opt (    tagPair_afterOpeningBrace "examplecode"
                       <|> tagPair_afterOpeningBrace "requiredcode")
              .>> skipToOpeningBraceOrEof)
        |>> List.choose id
   .>> eof