在Parslet中,如何从解析子树重构子串?

时间:2015-11-16 19:41:32

标签: ruby parslet

我正在为具有插值名称 - 值参数的字符串编写解析器,例如:'This sentence #{x: 2, y: (2 + 5) + 3} has stuff in it.'参数值是代码,它有自己的一组解析规则。

这是我的解析器的一个版本,简化为只允许基本算术作为代码:

require 'parslet'
require 'ap'
class TestParser < Parslet::Parser
  rule :integer do match('[0-9]').repeat(1).as :integer end
  rule :space do match('[\s\\n]').repeat(1) end
  rule :parens do str('(') >> code >> str(')') end
  rule :operand do integer | parens end
  rule :addition do (operand.as(:left) >> space >> str('+') >> space >> operand.as(:right)).as :addition end
  rule :code do addition | operand end
  rule :name do match('[a-z]').repeat 1 end
  rule :argument do name.as(:name) >> str(':') >> space >> code.as(:value) end
  rule :arguments do argument >> (str(',') >> space >> argument).repeat end
  rule :interpolation do str('#{') >> arguments.as(:arguments) >> str('}') end
  rule :text do (interpolation.absent? >> any).repeat(1).as(:text) end
  rule :segments do (interpolation | text).repeat end
  root :segments
end
string = 'This sentence #{x: 2, y: (2 + 5) + 3} has stuff in it.'
ap TestParser.new.parse(string), index: false

由于代码具有自己的解析规则(以确保有效的语法),因此将参数值解析为子树(括号等替换为子树内的嵌套):

[
    {
        :text => "This sentence "@0
    },
    {
        :arguments => [
            {
                 :name => "x"@16,
                :value => {
                    :integer => "2"@19
                }
            },
            {
                 :name => "y"@22,
                :value => {
                    :addition => {
                         :left => {
                            :addition => {
                                 :left => {
                                    :integer => "2"@26
                                },
                                :right => {
                                    :integer => "5"@30
                                }
                            }
                        },
                        :right => {
                            :integer => "3"@35
                        }
                    }
                }
            }
        ]
    },
    {
        :text => " has stuff in it."@37
    }
]

但是,我想将参数值存储为字符串,因此这将是理想的结果:

[
    {
        :text => "This sentence "@0
    },
    {
        :arguments => [
            {
                 :name => "x"@16,
                :value => "2"
            },
            {
                 :name => "y"@22,
                :value => "(2 + 5) + 3"
            }
        ]
    },
    {
        :text => " has stuff in it."@37
    }
]

如何使用Parslet子树重建参数值子串?我可以编写一个代码生成器,但这似乎有点过分 - Parslet显然可以在某个时刻访问子字符串位置信息(虽然它可能会丢弃它)。

是否可以利用或破解Parslet来返回子字符串?

2 个答案:

答案 0 :(得分:1)

生成的树基于解析器中""100"",""110"",""120"",""145"" 的使用。

您可以尝试从表达式中的任何内容中删除它们,以便为表达式获得单个字符串匹配。这似乎就是你所追求的。

如果你想要解析这些表达式的树,那么你需要:

  • 将表达式树转换回匹配的文本。
  • 将匹配的文本重新解析回表达式树。

这些都不是理想的,但如果速度不重要,我会去重新解析选项。即。删除as个原子,然后根据需要将表达式重新解析为树。

由于您正确地希望重用相同的规则,但这次您需要在整个规则中捕获as,然后您可以通过从现有解析器派生解析器并使用相同名称实现规则来实现此目的。 as

OR

你可以有一个表达式的一般规则,它匹配整个表达式而不知道它是什么。

例如。 rule :x { super.x.as(:x)}

然后,您可以根据需要将每个表达式解析为树。这样你就不会重复你的规则了。它假定您可以在不解析整个表达式子树的情况下判断表达式何时完成。

如果做不到这一点,就会给我们带来黑客攻击。

我这里没有解决方案,只是一些提示。

Parslet有一个名为“CanFlatten”的模块,它实现"#{" >> (("}".absent >> any) | "\\}").repeat(0) >> "}"并由flatten用于将捕获的树转换回单个字符串。你会想要做这样的事情。

或者,您需要更改as中的succ方法以返回“[success / fail,result,consume_upto_position]”,以便每个匹配都知道它消耗的位置。然后,您可以从源头读取起始位置和结束位置,以获取原始文本。解析器匹配时源的Atom::Base应该是您想要的值。

祝你好运。

注意:我的示例表达式解析器不处理转义字符的转义..(作为读者的练习)

答案 1 :(得分:1)

这是我最终的黑客攻击。有更好的方法来实现这一目标,但他们需要进行更广泛的更改。 Parser#parse现在返回ResultResult#tree给出了正常的解析结果,Result#strings是一个将子树结构映射到源字符串的哈希。

module Parslet

  class Parser
    class Result < Struct.new(:tree, :strings); end
    def parse(source, *args)
      source = Source.new(source) unless source.is_a? Source
      value = super source, *args 
      Result.new value, source.value_strings
    end
  end

  class Source
    prepend Module.new{
      attr_reader :value_strings
      def initialize(*args)
        super *args
        @value_strings = {}
      end
    }
  end

  class Atoms::Base
    prepend Module.new{
      def apply(source, *args)
        old_pos = source.bytepos
        super.tap do |success, value|
          next unless success
          string = source.instance_variable_get(:@str).string.slice(old_pos ... source.bytepos)
          source.value_strings[flatten(value)] = string
        end
      end    
    }
  end

end