1 /**
2     Parser utilities.
3 
4     Copyright: 
5         Copyright © 2020-2026, Inochi2D Project
6     
7     License:
8         $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License)
9     
10     Authors:
11         Luna Nielsen
12 */
13 module inp.format.parse;
14 import nulib.text.ascii;
15 import nulib.io.stream.rw;
16 import nulib.io.stream;
17 import nulib.string;
18 import nulib.math;
19 import numem;
20 
21 @nogc:
22 
23 // Alias to make things more obvious
24 alias ErrorString = string;
25 
26 
27 /**
28     Gets whether the end of the stream has been reached.
29 
30     Params:
31         reader = The stream reader.
32 
33     Returns:
34         $(D true) if the end of the stream was reached,
35         $(D false) otherwise.
36 */
37 bool eof(StreamReader reader) {
38     return reader.stream.tell() >= reader.stream.length();
39 }
40 
41 /**
42     Skips the given amount of characters in the stream.
43 
44     Params:
45         reader =    The stream reader.
46         count =     Amount of characters to skip.
47 */
48 pragma(inline, true)
49 void skip(StreamReader reader, ptrdiff_t count) {
50     reader.stream.seek(count, SeekOrigin.relative);
51 }
52 
53 /**
54     Skips ascii whitespace characters.
55 
56     Params:
57         reader = The stream reader.
58 */
59 void skipWhitespace(StreamReader reader) {
60     char c;
61     do { c = cast(char)reader.readU8(); } while(c == ' ' || c == '\t' || c == '\r' || c == '\n');
62     reader.skip(-1);
63 }
64 
65 /**
66     Peeks a single ASCII character from the stream.
67 
68     Params:
69         reader = The stream reader.
70 
71     Returns:
72         The character 1 byte ahead of the current read position.
73 */
74 char peekChar(StreamReader reader) {
75     char c = cast(char)reader.readU8();
76     reader.skip(-1);
77     return c;
78 }
79 
80 /**
81     Takes a peek at the upcoming string contents of the given stream.
82 
83     Params:
84         reader = The stream reader.
85         length = The length of the string.
86 
87     Returns:
88         The text read from the stream, length might be shorter
89         than the requested length.
90 */
91 nstring peekString(StreamReader reader, uint length) {
92     auto s = reader.readUTF8(length);
93     if (s.length > 0) {
94         reader.skip(-cast(ptrdiff_t)s.length);
95     }
96     return s;
97 }
98 
99 /**
100     Peeks a string from the stream and checks whether
101     it is the same as the given key.
102 
103     Params:
104         reader =    The stream reader.
105         key =       The key to compare against.
106 
107     Returns:
108         $(D true) if the upcoming characters in the stream match
109         $(D key), $(D false) otherwise.
110 */
111 bool peekString(StreamReader reader, string key) {
112     nstring read = reader.peekString(cast(uint)key.length);
113     return key == read[];
114 }
115 
116 /**
117     Reads JSON encoded numbers from the stream.
118 
119     Params:
120         reader =        The stream reader.
121         isFloating =    Whether the read number was floating point.
122 
123     Returns:
124         The JSON number read from the stream.
125 */
126 nstring readNumberJson(StreamReader reader, ref bool isFloating) {
127     char c = cast(char)reader.readU8();
128     if (!c.isNumeric && c != '-' && c != '+') {
129         reader.skip(-1);
130         return nstring.init;
131     }
132 
133     nstring result;
134     result ~= c;
135 
136     // Read characters until they're no longer number-like.
137     bool isExponent;
138     while(true) {
139         c = cast(char)reader.readU8();
140 
141         // Integral part
142         if (!isFloating) {
143             if (c == '.') {
144                 isFloating = true;
145                 result ~= c;
146                 continue;
147             }
148 
149             if (c.isNumeric) {
150                 result ~= c;
151                 continue;
152             }
153 
154             // No longer a number.
155             break;
156         }
157 
158         // Fractional part
159         if (!isExponent) {
160             if (c == 'e' || c == 'E') {
161                 isExponent = true;
162                 result ~= c;
163                 continue;
164             }
165 
166             if (c.isNumeric) {
167                 result ~= c;
168                 continue;
169             }
170 
171             // No longer a number.
172             break;
173         }
174 
175         // Exponential part.
176         if (!(c.isNumeric || c == '-' || c == '+'))
177             break;
178 
179         result ~= c;
180     }
181 
182     reader.skip(-1);
183     return result;
184 }
185 
186 /**
187     Reads a JSON string element from the given stream.
188 
189     Params:
190         reader = The stream reader.
191 
192     Returns:
193         The JSON string read from the stream.
194 */
195 nstring readStringJson(StreamReader reader) {
196     char c = cast(char)reader.readU8();
197 
198     // Not a string.
199     if (c != '"') {
200         reader.skip(-1);
201         return nstring.init;
202     }
203 
204     nstring result;
205     do {
206         c = cast(char)reader.readU8();
207         if (c == '"')
208             break;
209         
210         result ~= c;
211 
212         // Read escape codes.
213         if (c == '\\') {
214             c = cast(char)reader.readU8();
215             result ~= c;
216         }
217     } while(c != '"');
218     return result;
219 }
220 
221 /**
222     Gets whether the upcoming character is a numeric symbol.
223 
224     Params:
225         c = the character
226 
227     Returns:
228         Whether the upcoming character is a valid     
229 */
230 bool isNumeric(char c) {
231     return (c >= '0' && c <= '9');
232 }