summaryrefslogtreecommitdiffstats
path: root/yql/essentials/utils/docs/markdown.cpp
blob: ca0f555a81521f2e268014934d124ae1bbf8f96b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#include "markdown.h"

#include <yql/essentials/utils/yql_panic.h>

#include <contrib/libs/re2/re2/re2.h>

#include <util/generic/yexception.h>
#include <util/charset/utf8.h>

namespace NYql::NDocs {

    class TMarkdownParser {
    private:
        static constexpr TStringBuf HeaderRegex = R"re(([^#]+)(\s+{#([a-z0-9\-_]+)})?)re";

    public:
        explicit TMarkdownParser(size_t headerDepth)
            : HeaderDepth_(headerDepth)
            , SectionHeaderRegex_(" *" + TString(HeaderDepth_, '#') + " " + HeaderRegex)
            , IsSkipping_(true)
        {
        }

        void Parse(IInputStream& markdown, TMarkdownCallback&& onSection) {
            for (TString line; markdown.ReadLine(line) != 0;) {
                if (IsSkipping_) {
                    if (IsSectionHeader(line)) {
                        ResetSection(std::move(line));
                        IsSkipping_ = false;
                    } else {
                        // Skip
                    }
                } else {
                    if (IsSectionHeader(line)) {
                        onSection(std::move(Section_));
                        ResetSection(std::move(line));
                    } else {
                        line.append('\n');
                        Section_.Body.append(std::move(line));
                    }
                }
            }

            if (!IsSkipping_) {
                onSection(std::move(Section_));
            }
        }

    private:
        void ResetSection(TString&& line) {
            Section_ = TMarkdownSection();

            TString content;
            std::optional<TString> dummy;
            std::optional<TString> anchor;
            if (!RE2::FullMatch(line, SectionHeaderRegex_, &content, &dummy, &anchor)) {
                Section_.Header.Content = std::move(line);
                return;
            }

            Section_.Header.Content = std::move(content);
            if (anchor) {
                Section_.Header.Anchor = std::move(*anchor);
            }
        }

        bool IsSectionHeader(TStringBuf line) const {
            return HeaderDepth(line) == HeaderDepth_;
        }

        size_t HeaderDepth(TStringBuf line) const {
            size_t begin = line.find('#');
            size_t end = line.find_first_not_of('#', begin);
            return end != TStringBuf::npos ? (end - begin) : 0;
        }

        size_t HeaderDepth_;
        RE2 SectionHeaderRegex_;
        bool IsSkipping_;
        TMarkdownSection Section_;
    };

    TMaybe<TString> Anchor(const TMarkdownHeader& header) {
        static RE2 Regex(R"re([0-9a-z\-_]+)re");

        if (header.Anchor) {
            return header.Anchor;
        }

        TString content = ToLowerUTF8(header.Content);
        SubstGlobal(content, ' ', '-');

        if (RE2::FullMatch(content, Regex)) {
            return content;
        }

        return Nothing();
    }

    TMarkdownPage ParseMarkdownPage(TString markdown) {
        TMarkdownPage page;

        const auto onSection = [&](TMarkdownSection&& section) {
            if (TMaybe<TString> anchor = Anchor(section.Header)) {
                section.Header.Anchor = anchor;
                page.SectionsByAnchor[*anchor] = std::move(section);
            }
        };

        {
            TMarkdownParser parser(/*headerDepth=*/2);
            TStringStream stream(markdown);
            parser.Parse(stream, onSection);
        }

        {
            TMarkdownParser parser(/*headerDepth=*/3);
            TStringStream stream(markdown);
            parser.Parse(stream, onSection);
        }

        page.Text = std::move(markdown);

        return page;
    }

} // namespace NYql::NDocs