Разбор структуры селектора с чередованием токенов с использованием Boost Spirit X3

Я пытаюсь разобрать следующую структуру:

struct Selector {
    std::string element;
    std::string id;
    std::vector<std::string> classes;
};

Эта структура используется для анализа селекторов в форме element#id.class1.class2.classn. Эти селекторы всегда начинаются с 1 или без элементов, могут содержать 1 или не содержать идентификаторов и могут содержать от 0 до n классов.

Это становится еще более сложным, потому что классы и id могут появляться в любом порядке, поэтому все следующие селекторы действительны: element#id.class1, .class1#id.class2.class3, #id.class1.class2, .class1.class2#id. По этой причине мне не удалось использовать hold[] или at<T>() подходы, описанные здесь, и я также не смог использовать BOOST_FUSION_ADAPT_STRUCT.

Единственный способ, которым я смог синтезировать эту структуру, - это использовать следующие правила:

auto element = [](auto& ctx){x3::_val(ctx).element = x3::_attr(ctx);};
auto id = [](auto& ctx){x3::_val(ctx).id = x3::_attr(ctx);};
auto empty = [](auto& ctx){x3::_val(ctx) = "";};
auto classes = [](auto& ctx){x3::_val(ctx).classes.insert(x3::_val(ctx).classes.end(), x3::_attr(ctx).begin(), x3::_attr(ctx).end());};

auto elementRule = x3::rule<class EmptyIdClass, std::string>() = +x3::char_("a-zA-Z") | x3::attr("");
auto idRule = x3::rule<class EmptyIdClass, std::string>() = ("#" >> +x3::char_("a-zA-Z")) | x3::attr("");
auto classesRule = x3::rule<class ClassesClass, std::vector<std::string>>() = *("." >> +x3::char_("a-zA-Z"));
auto selectorRule = x3::rule<class TestClass, Selector>() = elementRule[element] >> classesRule[classes] >> idRule[id] >> classesRule[classes];

Как лучше всего разобрать эту структуру? Можно ли синтезировать эту структуру селектора естественным образом, используя BOOST_FUSION_ADAPT_STRUCT, и без семантических действий?

Кажется, каждый раз, когда я начинаю разбираться в Spirit X3, я натыкаюсь на новый вызов. В этом конкретном случае я узнал о проблемах с обратным отслеживанием, о проблеме с использованием at<T>() который был представлен в Boost 1.70 здесь, и я также узнал, что hold[] не поддерживается Х3.


person Jaime Ivan Cervantes    schedule 20.04.2020    source источник


Ответы (2)


Я уже писал подобные ответы раньше:

Я не думаю, что вы можете напрямую адаптироваться к слиянию. Хотя, если вы очень мотивированы (например, у вас уже есть адаптированные структуры), вы могли бы сделать из этого несколько общих помощников.

Честно говоря, небольшая реструктуризация вашего кода мне уже кажется довольно приятной. Вот мои усилия, чтобы сделать его более элегантным / удобным. Я представлю вспомогательный макрос, такой же, как BOOST_FUSION_ADAPT_XXX, но не требующий какого-либо Boost Fusion.

Начнем с AST

Как всегда, мне нравится начинать с основ. Понимание цели - это половина дела:

namespace Ast {
    using boost::optional;

    struct Selector {
        // These selectors always 
        //  - start with 1 or no elements, 
        //  - could contain 1 or no ids, and
        //  - could contain 0 to n classes.
        optional<std::string> element;
        optional<std::string> id;
        std::vector<std::string> classes;

        friend std::ostream& operator<<(std::ostream& os, Selector const&s) {
            if  (s.element.has_value()) os << s.element.value();
            if  (s.id.has_value())      os << "#" << s.id.value();
            for (auto& c : s.classes)   os << "." << c;
            return os;
        }
    };
}

Обратите внимание, что я исправил необязательность некоторых частей, чтобы они отражали реальную жизнь.

Вы можете использовать это для обнаружения повторной инициализации полей элемента / идентификатора.

Волшебный соус (см. Ниже)

#include "propagate.hpp"
DEF_PROPAGATOR(Selector, id, element, classes)

Мы рассмотрим это позже. Достаточно сказать, что он генерирует семантические действия, которые вам приходилось утомительно писать.

Главное блюдо

Теперь мы можем значительно упростить правила парсера и запустить тесты:

int main() {
    auto name        = as<std::string>[x3::alpha >> *x3::alnum];
    auto idRule      = "#" >> name;
    auto classesRule = +("." >> name);

    auto selectorRule
        = x3::rule<class TestClass, Ast::Selector>{"selectorRule"}
        = +( name        [ Selector.element ]
           | idRule      [ Selector.id ]
           | classesRule [ Selector.classes ]
           )
        ;

    for (std::string const& input : {
            "element#id.class1.class2.classn",
            "element#id.class1",
            ".class1#id.class2.class3",
            "#id.class1.class2",
            ".class1.class2#id",
        })
    {
        Ast::Selector sel;
        std::cout << std::quoted(input) << " -->\n";
        if (x3::parse(begin(input), end(input), selectorRule >> x3::eoi, sel)) {
            std::cout << "\tSuccess: " << sel << "\n";
        } else {
            std::cout << "\tFailed\n";
        }
    }
}

См. Live On Wandbox, напечатав:

"element#id.class1.class2.classn" -->
    Success: element#id.class1.class2.classn
"element#id.class1" -->
    Success: element#id.class1
".class1#id.class2.class3" -->
    Success: #id.class1.class2.class3
"#id.class1.class2" -->
    Success: #id.class1.class2
".class1.class2#id" -->
    Success: #id.class1.class2

Магия

Итак, как я сгенерировал эти действия? Используя немного препроцессора Boost:

#define MEM_PROPAGATOR(_, T, member) \
    Propagators::Prop<decltype(std::mem_fn(&T::member))> member { std::mem_fn(&T::member) };

#define DEF_PROPAGATOR(type, ...) \
    struct type##S { \
        using T = Ast::type; \
        BOOST_PP_SEQ_FOR_EACH(MEM_PROPAGATOR, T, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \
    } static const type {};

Теперь вы можете увидеть, что он определяет статические константные переменные, названные как типы Ast.

Вы можете вызвать этот макрос в другом пространстве имен, например namespace Actions { }

Настоящая магия - это Propagators::Prop<F>, у которого есть небольшая диспетчеризация, позволяющая использовать атрибуты и члены контейнера. В противном случае он просто передает x3::traits::move_to:

namespace Propagators {
    template <typename F>
    struct Prop {
        F f;
        template <typename Ctx>
        auto operator()(Ctx& ctx) const {
            return dispatch(x3::_attr(ctx), f(x3::_val(ctx)));
        }
      private:
        template <typename Attr, typename Dest>
        static inline void dispatch(Attr& attr, Dest& dest) {
            call(attr, dest, is_container(attr), is_container(dest));
        }

        template <typename T>
        static auto is_container(T const&)           { return x3::traits::is_container<T>{}; }
        static auto is_container(std::string const&) { return boost::mpl::false_{}; }

        // tags for dispatch
        using attr_is_container = boost::mpl::true_;
        using attr_is_scalar    = boost::mpl::false_;
        using dest_is_container = boost::mpl::true_;
        using dest_is_scalar    = boost::mpl::false_;

        template <typename Attr, typename Dest>
        static inline void call(Attr& attr, Dest& dest, attr_is_scalar, dest_is_scalar) {
            x3::traits::move_to(attr, dest);
        }
        template <typename Attr, typename Dest>
        static inline void call(Attr& attr, Dest& dest, attr_is_scalar, dest_is_container) {
            dest.insert(dest.end(), attr);
        }
        template <typename Attr, typename Dest>
        static inline void call(Attr& attr, Dest& dest, attr_is_container, dest_is_container) {
            dest.insert(dest.end(), attr.begin(), attr.end());
        }
    };
}

БОНУС

Большая часть сложности в типе пропагатора связана с обработкой атрибутов контейнера. Однако на самом деле вам это не нужно:

auto name = as<std::string>[x3::alpha >> *x3::alnum];

auto selectorRule
    = x3::rule<class selector_, Ast::Selector>{"selectorRule"}
    = +( name        [ Selector.element ]
       | '#' >> name [ Selector.id ]
       | '.' >> name [ Selector.classes ]
       )
    ;

Более чем достаточно, и помощник по распространению можно упростить до:

namespace Propagators {
    template <typename F> struct Prop {
        F f;
        template <typename Ctx>
        auto operator()(Ctx& ctx) const {
            return call(x3::_attr(ctx), f(x3::_val(ctx)));
        }
      private:
        template <typename Attr, typename Dest>
        static inline void call(Attr& attr, Dest& dest) {
            x3::traits::move_to(attr, dest);
        }
        template <typename Attr, typename Elem>
        static inline void call(Attr& attr, std::vector<Elem>& dest) {
            dest.insert(dest.end(), attr);
        }
    };
}

Как видите, удаление отправки тегов имеет положительный эффект.

Снова просмотрите упрощенную версию Live On Wandbox.

ПОЛНЫЙ ПЕРЕЧЕНЬ

Для потомков на этом сайте:

  • test.cpp

    //#define BOOST_SPIRIT_X3_DEBUG
    #include <boost/spirit/home/x3.hpp>
    #include <iostream>
    #include <iomanip>
    
    namespace x3 = boost::spirit::x3;
    
    namespace Ast {
        using boost::optional;
    
        struct Selector {
            // These selectors always 
            //  - start with 1 or no elements, 
            //  - could contain 1 or no ids, and
            //  - could contain 0 to n classes.
            optional<std::string> element;
            optional<std::string> id;
            std::vector<std::string> classes;
    
            friend std::ostream& operator<<(std::ostream& os, Selector const&s) {
                if  (s.element.has_value()) os << s.element.value();
                if  (s.id.has_value())      os << "#" << s.id.value();
                for (auto& c : s.classes)   os << "." << c;
                return os;
            }
        };
    }
    
    #include "propagate.hpp"
    DEF_PROPAGATOR(Selector, id, element, classes)
    
    #include "as.hpp"
    int main() {
        auto name = as<std::string>[x3::alpha >> *x3::alnum];
    
        auto selectorRule
            = x3::rule<class selector_, Ast::Selector>{"selectorRule"}
            = +( name        [ Selector.element ]
               | '#' >> name [ Selector.id ]
               | '.' >> name [ Selector.classes ]
               )
            ;
    
        for (std::string const& input : {
                "element#id.class1.class2.classn",
                "element#id.class1",
                ".class1#id.class2.class3",
                "#id.class1.class2",
                ".class1.class2#id",
            })
        {
            Ast::Selector sel;
            std::cout << std::quoted(input) << " -->\n";
            if (x3::parse(begin(input), end(input), selectorRule >> x3::eoi, sel)) {
                std::cout << "\tSuccess: " << sel << "\n";
            } else {
                std::cout << "\tFailed\n";
            }
        }
    }
    
  • распространять.hpp

    #pragma once
    #include <boost/preprocessor/cat.hpp>
    #include <boost/preprocessor/seq/for_each.hpp>
    #include <functional>
    
    namespace Propagators {
        template <typename F> struct Prop {
            F f;
            template <typename Ctx>
            auto operator()(Ctx& ctx) const {
                return call(x3::_attr(ctx), f(x3::_val(ctx)));
            }
          private:
            template <typename Attr, typename Dest>
            static inline void call(Attr& attr, Dest& dest) {
                x3::traits::move_to(attr, dest);
            }
            template <typename Attr, typename Elem>
            static inline void call(Attr& attr, std::vector<Elem>& dest) {
                dest.insert(dest.end(), attr);
            }
        };
    }
    
    #define MEM_PROPAGATOR(_, T, member) \
        Propagators::Prop<decltype(std::mem_fn(&T::member))> member { std::mem_fn(&T::member) };
    
    #define DEF_PROPAGATOR(type, ...) \
        struct type##S { \
            using T = Ast::type; \
            BOOST_PP_SEQ_FOR_EACH(MEM_PROPAGATOR, T, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) \
        } static const type {};
    
  • as.hpp

    #pragma once
    #include <boost/spirit/home/x3.hpp>
    
    namespace {
        template <typename T>
        struct as_type {
            template <typename...> struct tag{};
            template <typename P>
            auto operator[](P p) const {
                return boost::spirit::x3::rule<tag<T,P>, T> {"as"}
                       = p;
            }
        };
    
        template <typename T>
            static inline const as_type<T> as = {};
    }
    
person sehe    schedule 20.04.2020
comment
Добавлена ​​ссылка на другой ответ, где я перехожу к более сложному синтаксическому анализу CSS stackoverflow.com/a/53199872/85371 - person sehe; 20.04.2020
comment
Большое спасибо @sehe! Интересно, почему полезные утилиты, такие как propagator, hold[] и as<T>(), не являются частью X3 (некоторые являются частью Qi), они были бы очень полезны для неспециалистов по Spirit - person Jaime Ivan Cervantes; 21.04.2020
comment
Я изменил selectorRule на -name[Selector.element] >> *( '#' >> name [Selector.id] | '.' >> name [Selector.classes]), чтобы лучше отразить правила Элементов. Однако есть проблема с предложенным вами решением; Селектор с несколькими идентификаторами должен выйти из строя (например: #id1#id2), но ваше решение, похоже, вместо этого использует последний идентификатор. Я не знаю, можно ли изменить ваше решение, чтобы принудительно использовать один идентификатор или его отсутствие, или иначе. - person Jaime Ivan Cervantes; 21.04.2020
comment
Это то, что я имел в виду с Вы можете использовать это для обнаружения повторной инициализации полей элемента / идентификатора. Я посмотрю, смогу ли я показать демонстрацию - person sehe; 21.04.2020
comment
@JaimeIvanCervantes Самое простое, что вы можете сделать, это добавить ~ 3 строки кода для обработки опций wandbox.org/permlink/j3r7eKXuLrY / а> (распространять.hpp) - person sehe; 21.04.2020
comment
@JaimeIvanCervantes Совершенно другой подход, нулевые семантические действия, никаких макросов: coliru.stacked-crooked.com/a / b6c47dd98f6e28fa (теперь один файл). По сути, именно поэтому вы не найдете этих функций в библиотеке. Есть так много разных подходов к ti. Однако этот подход начинает казаться скрученным вручную парсером. Мне это не очень нравится (кроме простоты и демонстрационной ценности). - person sehe; 21.04.2020

Может быть, это не то, что вы хотите иметь, тогда сообщите мне, и я удалю ответ, но для этого как-то простого разбора вам не нужны ни Boost, ни дух.

Простое регулярное выражение подойдет для разделения данной строки на токен. Мы можем наблюдать следующее:

  • Имя «элемента» начинается в начале строки и представляет собой строку буквенно-цифровых символов.
  • "id" всегда начинается с хеша #
  • и имена классов всегда начинаются с точки .

Итак, мы можем сформировать единое регулярное выражение для соответствия этим трем типам токенов.

((^\w+)|[\.#]\w+)

Вы можете найти здесь объяснение регулярного выражения.

Затем мы можем написать простую программу, которая считывает селекторы, разбивает их на токены, а затем назначает их структуре Selector.

См. Следующий пример. Это должно дать вам представление о том, как это можно сделать.

#include <iostream>
#include <vector>
#include <regex>
#include <sstream>
#include <string>
#include <iterator>
#include <cctype>

struct Selector {
    std::string element;
    std::string id;
    std::vector<std::string> classes;
};

std::stringstream inputFileStream{ R"(element1#id1.class11.class12.class13.class14
element2#id2.class21.class22
#id3.class31.class32.class33.class34.class35
.class41.class42,class43#id4
.class51#id5.class52.class53.class54.class55.class56
)"};

//std::regex re{R"(([\.#]?\w+))"};
std::regex re{ R"(((^\w+)|[\.#]\w+))" };

int main() {

    std::vector<Selector> selectors{};

    // Read all lines of the source file
    for (std::string line{}; std::getline(inputFileStream, line); ) {

        // Split the line with selector string into tokens
        std::vector<std::string> tokens(std::sregex_token_iterator(line.begin(), line.end(), re), {});

        // Here we will store the one single selector
        Selector tempSelector{};

        // Go though all tokens and check the type of them
        for (const std::string& token : tokens) {

            // Depending on the structure element type, add it to the correct structure element field
            if (token[0] == '#') tempSelector.id = std::move(token.substr(1));
            else if (token[0] == '.') tempSelector.classes.emplace_back(token.substr(1));
            else if (std::isalnum(token[0])) tempSelector.element = token;
            else std::cerr << "\n*** Error: Invalid token found: " << token << "\n";
        }
        // Add the new selector to the vector of selectors
        selectors.push_back(std::move(tempSelector));
    }


    // Show debug output
    for (const Selector& s : selectors) {
        std::cout << "\n\nSelector\n\tElement:\t" << s.element << "\n\tID:\t\t" << s.id << "\n\tClasses:\t";
        for (const std::string& c : s.classes)
            std::cout << c << " ";
    }
    std::cout << "\n\n";

    return 0;
}

Конечно, мы могли бы сделать более сложное регулярное выражение с некоторой дополнительной проверкой.

person Armin Montigny    schedule 20.04.2020
comment
спасибо, Армин, но, к сожалению, эта структура селектора является лишь очень упрощенной частью гораздо более сложного парсера, который включает в себя графы селекторов и сложные правила CSS, отсюда и необходимость в парсере. - person Jaime Ivan Cervantes; 21.04.2020