1 module text.RecursiveDescentParser;
2 
3 import std.algorithm;
4 import std.range;
5 import std.string;
6 
7 @safe
8 struct RecursiveDescentParser
9 {
10     private string text;
11 
12     private size_t cursor;
13 
14     public this(string text) @nogc
15     {
16         this.text = text;
17     }
18 
19     invariant
20     {
21         import std.utf : stride;
22 
23         assert(this.cursor >= 0 && this.cursor <= this.text.length);
24 
25         // validate that this.cursor lies at the start of a utf-8 character
26         assert(this.cursor == this.text.length || this.text[this.cursor .. $].stride > 0);
27     }
28 
29     public bool matchGroup(scope bool delegate() @nogc @safe action) @nogc
30     {
31         auto backup = this.cursor;
32         auto result = action();
33 
34         if (!result) // parse failure, roll back state
35         {
36             this.cursor = backup;
37         }
38 
39         return result;
40     }
41 
42     public bool captureGroupInto(out string target, scope bool delegate() @nogc @safe action) @nogc
43     {
44         auto startCursor = this.cursor;
45         auto result = action();
46 
47         if (result)
48         {
49             auto endCursor = this.cursor;
50 
51             target = this.text[startCursor .. endCursor];
52         }
53 
54         return result;
55     }
56 
57     public bool matchZeroOrMore(scope bool delegate() @nogc @safe action) @nogc
58     {
59         while (action() == true) { }
60 
61         return true;
62     }
63 
64     public bool matchOptional(scope bool delegate() @nogc @safe action) @nogc
65     {
66         action();
67 
68         return true;
69     }
70 
71     public bool matchTimes(int num, scope bool delegate() @nogc @safe action) @nogc
72     {
73         return matchGroup({
74             foreach (_; 0 .. num)
75             {
76                 if (action() == false)
77                 {
78                     return false;
79                 }
80             }
81             return true;
82         });
83     }
84 
85     public bool acceptAsciiChar(scope bool delegate(char) @nogc @safe predicate) @nogc
86     {
87         import std.ascii : isASCII;
88 
89         bool advance()
90         {
91             this.cursor = this.cursor + 1;
92             return true;
93         }
94 
95         return !eof
96             // it's safe to do this check because we only advance in ways that cause text[cursor] to be valid utf-8
97             // (see invariant)
98             && this.text[this.cursor].isASCII
99             && predicate(this.text[this.cursor])
100             && advance;
101     }
102 
103     public bool eof() @nogc
104     {
105         return this.remainingText.length == 0;
106     }
107 
108     public bool accept(string needle) @nogc
109     {
110         bool advance()
111         {
112             this.cursor = this.cursor + needle.length;
113             return true;
114         }
115 
116         return this.remainingText.startsWith(needle) && advance;
117     }
118 
119     public @property string remainingText() const @nogc
120     {
121         return this.text[this.cursor .. $];
122     }
123 }
124 
125 unittest
126 {
127     import dshould : be, equal, should;
128 
129     with (RecursiveDescentParser("aaaaaaaa"))
130     {
131         matchTimes(8, () => accept("a")).should.be(true);
132         matchTimes(1, () => accept("a")).should.be(false);
133         accept("a").should.be(false);
134         remainingText.should.equal("");
135     }
136 }
137 
138 unittest
139 {
140     import dshould : be, equal, should;
141 
142     with (RecursiveDescentParser("aaaaaaaa"))
143     {
144         matchZeroOrMore(() => accept("a")).should.be(true);
145         remainingText.should.equal("");
146         matchZeroOrMore(() => accept("a")).should.be(true);
147     }
148 }