В 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 ответа
Вот хак, с которым я закончил. Есть лучшие способы сделать это, но они потребуют более масштабных изменений. Parser#parse
теперь возвращает Result
, Result#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
Произведенное дерево основано на использовании as
в вашем парсере.
Вы можете попробовать удалить их из чего-либо в выражении, чтобы получить совпадение с одной строкой для выражения. Похоже, это то, что вы после.
Если вы хотите проанализировать дерево и для этих выражений, то вам нужно либо:
- Преобразуйте деревья выражений обратно в сопоставленный текст.
- Пересмотрите сопоставленный текст обратно в дерево выражений.
Ни один из них не идеален, но если скорость не важна, я бы выбрал вариант повторного анализа. то есть. удалить as
атомы, а затем, при необходимости, повторно анализировать выражения для деревьев.
Так как вы по праву хотите повторно использовать те же правила, но на этот раз вам нужно as
захватывает по всем правилам, тогда вы могли бы реализовать это путем извлечения парсера из существующего парсера и реализации правил с одинаковыми именами в терминах rule :x { super.x.as(:x)}
ИЛИ ЖЕ
Вы можете иметь общее правило для выражения, которое соответствует всему выражению, не зная, что в нем.
например. "#{" >> (("}".absent >> any) | "\\}").repeat(0) >> "}"
Затем вы можете при необходимости анализировать каждое выражение в дереве. таким образом, вы не повторяете свои правила. Предполагается, что вы можете сказать, когда ваше выражение завершено, без разбора всего поддерева выражения.
Если это не удастся, то у нас останется взломанный петрушка.
У меня нет решения здесь, только некоторые подсказки.
В Parslet есть модуль CanFlatten, который реализует flatten
и используется as
преобразовать захваченное дерево обратно в одну строку. Вы захотите сделать что-то подобное.
В качестве альтернативы вам нужно изменить succ
метод в Atom::Base
возвращать "[success/fail, result, потребляется_upto_position]", чтобы каждое совпадение знало, где оно использовалось. Затем вы можете читать из источника между начальной и конечной позициями, чтобы получить необработанный текст обратно. current position
источника в том месте, где парсер соответствует, должно быть значение, которое вы хотите.
Удачи.
Примечание: мой пример синтаксического анализатора выражений не обрабатывает экранирование escape-символа.. (оставлено как упражнение для читателя)