我试图从大部分自由格式的文本中解析一些信息.我尝试在FParsec中实现,但我之前没有使用它,我不确定我是否做错了,或者即使它非常适合这个特定的问题.
我想从markdown文档("examplecode"和"requiredcode"标签)中解析出一组特定Liquid标签的内容.降价主要是自由格式文本,偶尔会在Liquid标签内阻塞,例如:
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 %}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,我需要解析[ "ABC\nDEF"; "GHI" ].
我所追求的解析逻辑可以强制表达.循环遍历每一行,如果我们找到一个我们感兴趣的开始标记,则采用直到我们匹配结束标记并将这些行添加到结果列表中,否则跳过行直到下一个开始标记.重复.
这可以通过循环或折叠,或使用正则表达式来完成:
\{%\s*(examplecode|requiredcode).*\%}(.*?)\{%\s*end\1\s*%\}
我发现很难在FParsec中表达上面的逻辑.我想写类似的东西between s t (everythingUntil t),但我不知道如何在不everythingUntil消耗结束令牌的情况下实现它,导致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)
Run Code Online (Sandbox Code Playgroud)
然后,我可以过滤标签,只包含我感兴趣的标签.
这比基本实现(如正则表达式)所做的要多得多,例如描述性错误报告和更严格的输入格式验证(这既好又坏).
更严格的格式的一个结果是"{%"在标记内的嵌套子字符串上解析失败.我不确定我是如何调整它以处理这种情况(应该给[ "ABC {% DEF " ]):
{% examplecode %}
ABC {% DEF
{% endexamplecode %}
Run Code Online (Sandbox Code Playgroud)
有没有办法更密切地表达FParsec中"问题描述"部分中描述的逻辑,或者输入的自由形式本质是否使FParsec不适合这个而不是更基本的循环或正则表达式?
(我也对在"{%"标签中允许嵌套字符串的方法以及对我的FParsec尝试的改进感兴趣.我很乐意根据需要将其分解为其他问题.)
我只是用start >>. everythingUntil end而不是between start end body.
以下实现与正则表达式中的逻辑相对接近:
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
Run Code Online (Sandbox Code Playgroud)
一些说明:
我只解析你感兴趣的两个Liquid语句.如果这些语句中的一个嵌套在你不感兴趣的语句中,这就会有所不同.它还有一个优点,就是解析器不需要构造解析器.运行.
我正在使用>>?组合器控制何时可能发生回溯.
此实现的性能不会很好,但如有必要,有多种方法可以对其进行优化.最慢的组件可能是manyCharsTill anyChar (endTag name)解析器,可以很容易地用自定义原语替换.该many ... |> List.choose id中tagPairs也有一个更有效的定制组合子很容易地更换.