如何使用 C++ 解析简单的类似 pango 的标记,获得类似树的节点结构?

How to parse simple pango-like markup using C++, getting a tree like structure of nodes?

提问人:TheEagle 提问时间:3/10/2023 更新时间:3/13/2023 访问量:97

问:

我正在尝试将 Pango 风格的标记解析为树状结构,其中叶子是文本元素,分支是具有属性的标记元素。例如:

<b>This is bold</b>, <i>italic and <span color="red">red text</span></i> !

到目前为止,我有下面的 C++ 代码(没有类定义,我认为它们无关紧要),如果您省略上面的部分,它可以正常工作 - 它只能解析每个级别的一个标签,但它在每个级别的多个标签上失败:<b></b>

        bool parseStartTag(const std::string& s, size_t start, size_t end, std::string& tag_name, std::map<std::string, std::string>& attrs) {
            size_t tag_name_end = s.find_first_of(" \t\n", start);
            if (tag_name_end == std::string::npos or tag_name_end >= end) {
                tag_name = s.substr(start, end - start);
                return true;
            } else {
                tag_name = s.substr(start, tag_name_end - start);
            }
            start = tag_name_end;

            char quote = '\0';
            std::string attr_name = "";
            std::string attr_value = "";
            while (start <= end) {
                if (quote != '\0') {
                    if (s[start] == quote) {
                        quote = '\0';
                        attrs[attr_name] = attr_value;
                        attr_name = "";
                        attr_value = "";
                    } else {
                        attr_value += s[start];
                    }
                } else if ((s[start] == ' ' or s[start] == '\t' or s[start] == '\n') and quote == '\0') {
                } else if (s[start] == '"' or s[start] == '\'') {
                    quote = s[start];
                } else if (s[start] != '=') {
                    attr_name += s[start];
                }
                start++;
            }

            return true;
        }

        std::shared_ptr<SGMarkupTag> parse(const std::string& s, std::string tag_name) {
            std::shared_ptr<SGMarkupTag> tag = std::make_shared<SGMarkupTag>();
            tag->setName(tag_name);
            
            size_t opening_index = 0;
            size_t closing_index = 0;
            size_t tag_content_start = 0;
            size_t tag_content_end = 0;
            while (closing_index != std::string::npos) {
                opening_index = s.find("<", closing_index);
                std::string text_string;
                if (opening_index != std::string::npos) {
                    text_string = s.substr(closing_index, opening_index - closing_index);
                } else {
                    text_string = s.substr(closing_index);
                }
                tag->addText(text_string);

                if (opening_index == std::string::npos) {
                    return tag;
                }

                closing_index = s.find(">", opening_index);
                if (opening_index != std::string::npos and closing_index == std::string::npos) {
                    SG_LOG(SG_GENERAL, SG_ALERT, "Markup parse error: Missing closing bracket for tag after opening bracket at char " << opening_index << ":");
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << s);
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << std::string(opening_index, '-') << "^");
                    return tag;
                }

                std::string tag_name = "";
                std::map<std::string, std::string> attrs;
                if (!parseStartTag(s, opening_index + 1, closing_index, tag_name, attrs)) {
                    return tag;
                };
                tag_content_start = closing_index + 1;

                opening_index = closing_index + s.substr(closing_index).rfind("</");
                if (opening_index == std::string::npos) {
                    SG_LOG(SG_GENERAL, SG_ALERT, "Markup parse error: Missing closing tag after opening tag at char  " << closing_index - 1 << ":");
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << s);
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << std::string(closing_index - 1, '-') << "^");
                    return tag;
                }
                tag_content_end = opening_index;
                closing_index = opening_index + s.substr(opening_index).rfind(">");
                if (closing_index == std::string::npos) {
                    SG_LOG(SG_GENERAL, SG_ALERT, "Markup parse error: Missing closing bracket for tag after opening bracket at char " << opening_index << ":");
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << s);
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << std::string(opening_index, '-') << "^");
                    return tag;
                }
                std::string closing_tag_name = s.substr(opening_index + 2, closing_index - opening_index - 2);
                if (closing_tag_name != tag_name) {
                    SG_LOG(SG_GENERAL, SG_ALERT, "Markup parse error: Unmatched closing tag '" << closing_tag_name << "' at char " << opening_index + 1 << " for opening tag '" << tag_name << "':");
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << s);
                    SG_LOG(SG_GENERAL, SG_ALERT, "\t" << std::string(opening_index + 1, '-') << "^");
                    return tag;
                }

                std::shared_ptr<SGMarkupElement> sub_tag = parse(s.substr(tag_content_start, tag_content_end - tag_content_start), tag_name);
                sub_tag->setAttributes(attrs);
                sub_tag->setParent(tag);
                sub_tag->setRoot(tag->getRoot());
                tag->addChild(sub_tag);

                closing_index++;
            }
            tag->addText(s.substr(closing_index));

            return tag;
        }

现在,我怎样才能将 while 循环更改为每个级别的多个标签?我知道这不是代码编写服务,但是关于我需要添加或更改的内容的一些指示会很好。parse

C++ xml-解析 抽象语法树 标记

评论

0赞 jwezorek 3/11/2023
SGMarkupElement 和 SGMarkupTag 之间是什么关系?SGMarkupTag 是否继承自 SGMarkupElement ?ALso 可能是内存泄漏,除非 setParent 在共享指针上调用 .get()。sub_tag->setParent(tag); ... tag->addChild(sub_tag);
0赞 TheEagle 3/11/2023
@jwezorek是的,SGMarkupElement 是 SGMarkupText 和 SGMarkupTag 的公共基础。至于内存泄漏......我没有观察到一个? .get() 未在共享指针上调用
0赞 jwezorek 3/11/2023
也可以使用从孩子到父母的弱指针。问题是你不能有从父到子项以及从子项到父项的共享指针,因为引用计数无法处理周期。
0赞 TheEagle 3/11/2023
@jwezorek好吧,当我遇到任何问题时,我知道在哪里解决它们!;)

答:

5赞 jwezorek 3/11/2023 #1

要解析同一级别上的多个元素,您不需要假设与特定开始标记关联的结束标记将始终是字符串中的最后一个结束标记(我的意思是结束标记,等等)。事实上,特定元素的结束标记甚至可能不是开始标记之后的下一个结束标记,因为其他元素可能嵌套在正在解析的元素中,甚至是同一类型的下一个结束标记,因为相同类型的另一个元素可能嵌套在一个元素中。没有简单的技巧可以用来忽略任意递归结构。</b></i>

基本上,您应该将代码重构为递归下降解析器

为此,您需要一组相互递归的函数,这些函数返回解析输出输入中函数所使用内容右侧的位置。每个解析函数都知道如何解析一种东西,并且允许失败。通过“允许失败”,我的意思是在输入并非绝对无效的情况下,解析失败不应抛出异常或错误。如果输入不是绝对无效的情况,不要抛出或以其他方式恐慌,那么你可以通过尝试解析当前位置的 x 并查看 x 解析器是否成功来解析分离(or-clauses);如果没有,请转到 Y 解析器...

然后,您需要清楚您到底想要什么输出;也就是说,您需要清楚正在解析的语法。在您的示例中,我不清楚应该返回什么,因为我不知道粗体元素和斜体元素之间的逗号会去哪里。所以说你不允许这样做。假设您允许每个元素包含文本或其他元素序列。然后,要为该语法创建递归下降解析器,您可以使用带有如下签名的解析函数:<b>This is bold</b>, <i>italic and <span color="red">red text</span></i>

std::shared_ptr<Tag> parseOpeningTag(const std::string& inp, int& i);
bool parseClosingTag(const std::string& inp, int& i, const std::string& tag);
std::shared_ptr<Element> parseElement(const std::string& inp, int& i);
std::string parseText(const std::string& inp, int& i);
std::vector<std::shared_ptr<Element>> parseElementSequence(const std::string& inp, int& i);

请注意,上述参数是参考。如果解析成功,最终将包含要从中开始解析的新位置。如果分析失败,则返回值将为 null 或 false,并且保持不变。iii

在这样的设置中,元素解析函数将如下所示

std::shared_ptr<Element> parseElement(const std::string& inp, int& i) {
    auto tag = parseOpeningTag(inp, i);
    if (!tag) {
        return {};
    }
    auto element = std::make_shared<Element>(tag->name(), tag->attributes());

    // contents are either a sequence of elements or text; try elements first
    auto children = parseElementSequence(inp, i); 
    if (!children.empty()) {
        element->setChildren(children);
    } else {
        // We assume here that parseElementSequence does not change i if
        // it fails.
        auto txt = parseText(inp, i); 
        element->setText(txt);
    }
    bool success = parseClosingTag(inp, i, tag->name());
    if (!success) {
        throw std::runtime_error("bad element: " + tag->name());
    }
    return element;
}

你会用 来写,在循环中调用它,直到它失败,等等。parseElementSequenceparseElement