【问题标题】:Parsing Selector struct with alternating tokens using Boost Spirit X3使用 Boost Spirit X3 使用交替标记解析 Selector 结构
【发布时间】:2020-04-20 05:10:12
【问题描述】:

我正在尝试解析以下结构:

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

此结构用于解析element#id.class1.class2.classn 形式的选择器。这些选择器总是以 1 个或没有元素开始,可以包含 1 个或不包含 id,并且可以包含 0 到 n 个类。

这变得更加复杂,因为类和 id 可以以任何顺序出现,所以以下选择器都是有效的:element#id.class1.class1#id.class2.class3#id.class1.class2.class1.class2#id。由于这个原因,我无法使用hold[]at&lt;T&gt;() 描述的here 方法,我也无法使用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 的窍门时,我都会偶然发现一个新的挑战。在这种特殊情况下,我了解了 backtracking 的问题,以及 Boost 1.70 中引入的 at&lt;T&gt;() 的使用问题 here,我还了解到 X3 不支持 hold[]

【问题讨论】:

    标签: c++ boost-spirit boost-spirit-x3 boost-fusion


    【解决方案1】:

    我之前写过类似的答案:

    我不认为你可以直接融合适应。尽管如果您非常有动力(例如,您已经有了适应的结构),您可以为此制作一些通用的助手。

    公平地说,对您的代码进行一点重组对我来说似乎已经很不错了。这是我努力使它更优雅/更方便。我将介绍一个类似于 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;
            }
        };
    }
    

    请注意,我修复了某些部分的可选性以反映现实生活。

    可以使用它来检测元素/id 字段的重复初始化。

    魔法酱(见下文)

    #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 Preprocessor:

    #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 类型的静态 const 变量。

    您可以在另一个命名空间中随意调用此宏,例如 namespace Actions { }

    真正的魔力是Propagators::Prop&lt;F&gt;,它有一点调度以允许容器属性和成员。否则它只会转发给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);
            }
        };
    }
    

    如您所见,蒸发tag dispatch 具有有益效果。

    再次查看简化版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 = {};
      }
      

    【讨论】:

    • 添加了另一个答案的链接,我将在其中进行更复杂的 CSS 解析 stackoverflow.com/a/53199872/85371
    • 非常感谢@sehe!我想知道为什么像 propagatorhold[]as&lt;T&gt;() 这样有用的工具不是 X3 的一部分(有些是 Qi 的一部分),这些对 Spirit 非专家很有帮助
    • 我将selectorRule 修改为-name[Selector.element] &gt;&gt; *( '#' &gt;&gt; name [Selector.id] | '.' &gt;&gt; name [Selector.classes]) 以更好地反映元素规则。但是,您提出的解决方案存在问题;具有多个 ID 的选择器应该会失败(例如:#id1#id2),但您的解决方案似乎使用了最后一个 ID。我不知道是否可以修改您的解决方案以强制使用单个 ID 或没有 ID,否则会失败。
    • @JaimeIvanCervantes 您可以做的最简单的事情,添加约 3 行代码来处理可选参数 wandbox.org/permlink/j3r7eKXuLr8WRqrY (propagate.hpp)
    • @JaimeIvanCervantes 完全不同的方法,零语义操作,没有宏:coliru.stacked-crooked.com/a/b6c47dd98f6e28fa(现在只有一个文件)。这基本上就是为什么您在库中找不到“这些功能”的原因。有很多不同的方法可以接近 ti。不过,这种方法开始感觉像是一个手动解析器。我不是很喜欢它(除了简单性和演示价值)。
    【解决方案2】:

    也许不是,你想要什么,那么请告诉我,我会删除答案,但是对于这种简单的解析,你不需要Boost,也不需要Spirit。

    一个简单的正则表达式可以将给定的字符串拆分为一个标记。我们可以观察到以下几点:

    • “元素”名称从行首开始,是一串字母数字字符。
    • “id”总是以哈希#开头
    • 而且,类名总是以点 . 开头

    因此,我们可以形成一个单一的正则表达式来匹配这 3 种类型的标记。

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

    您可以查看here 以了解正则表达式的解释。

    然后我们可以编写一个简单的程序来读取选择器,将其拆分为标记,然后将它们分配给 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;
    }
    

    当然,我们可以通过一些额外的检查来做一个更复杂的正则表达式。

    【讨论】:

    • 谢谢你,Armin,但不幸的是,这个 Selector 结构只是一个更复杂的解析器的一个非常简化的部分,它包括选择器图和复杂的 CSS 规则,因此需要一个解析器
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-16
    • 1970-01-01
    相关资源
    最近更新 更多