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 }