1 /** 2 * Copyright © Yurai Web Framework 2021 3 * License: MIT (https://github.com/YuraiWeb/yurai/blob/main/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module yurai.templates.parser; 7 8 import yurai.templates.templatetype; 9 import yurai.templates.templatedata; 10 11 public final class Token 12 { 13 public: 14 final: 15 TemplateType templateType; 16 string content; 17 18 this(TemplateType templateType) 19 { 20 this.templateType = templateType; 21 22 content = ""; 23 } 24 25 override string toString() 26 { 27 return content; 28 } 29 } 30 31 public final class StringReader 32 { 33 private: 34 string _content; 35 size_t _index; 36 37 public: 38 final: 39 this(string content) 40 { 41 _content = content; 42 _index = 0; 43 } 44 45 StringReader move() 46 { 47 _index++; 48 49 return this; 50 } 51 52 StringReader back() 53 { 54 _index--; 55 56 return this; 57 } 58 59 char peek(size_t amount) 60 { 61 auto index = _index + amount; 62 63 return index < (_content.length - 1) ? _content[index] : '\0'; 64 } 65 66 @property 67 { 68 bool has() { return _index < _content.length; } 69 70 char last() { return _index > 0 ? _content[_index - 1] : '\0'; } 71 72 char current() { return _content[_index]; } 73 74 char next() { return peek(1); } 75 } 76 } 77 78 public class Scope 79 { 80 char scopeCharStart; 81 char scopeCharEnd; 82 size_t scopeCount; 83 84 this(char scopeCharStart, char scopeCharEnd) 85 { 86 this.scopeCharStart = scopeCharStart; 87 this.scopeCharEnd = scopeCharEnd; 88 this.scopeCount = 0; 89 } 90 } 91 92 public class ScopeStack 93 { 94 Scope[] _stack; 95 96 public: 97 final: 98 this() 99 { 100 _stack = []; 101 } 102 103 void push(Scope s) 104 { 105 _stack ~= s; 106 } 107 108 void pop() 109 { 110 if (_stack.length == 1) 111 { 112 _stack = []; 113 } 114 else if (_stack.length > 1) 115 { 116 _stack = _stack[0 .. $-1]; 117 } 118 } 119 120 @property 121 { 122 bool has() { return _stack.length > 0; } 123 124 Scope currentScope() { return _stack[$-1]; } 125 } 126 } 127 128 public: 129 Token[] parse(string content) 130 { 131 import std.stdio : writeln, writefln; 132 133 Token[] tokens = []; 134 135 bool inComment = false; 136 137 auto reader = new StringReader(content); 138 139 auto scopeStack = new ScopeStack(); 140 141 auto contentToken = new Token(TemplateType.content); 142 143 while (reader.has) 144 { 145 if (inComment) 146 { 147 if (reader.current == '@' && reader.last == '*') 148 { 149 inComment = false; 150 continue; 151 } 152 } 153 154 if (reader.current == '*' && reader.last == '@') 155 { 156 inComment = true; 157 } 158 else 159 { 160 if (reader.current == '@' && reader.last != '\\') 161 { 162 if (reader.next == '[') 163 { 164 if (contentToken && contentToken.content && contentToken.content.length) 165 { 166 tokens ~= contentToken; 167 contentToken = new Token(TemplateType.content); 168 } 169 170 auto token = new Token(TemplateType.meta); 171 // skip @ and [ 172 reader.move().move(); 173 174 // parse content as meta 175 reader.basicDepthTagParsing('[', ']', token, tokens); 176 } 177 else if (reader.next == '#' && reader.peek(2) == '(') 178 { 179 if (contentToken && contentToken.content && contentToken.content.length) 180 { 181 tokens ~= contentToken; 182 contentToken = new Token(TemplateType.content); 183 } 184 185 auto token = new Token(TemplateType.placeholderValue); 186 // skip @ # and ( 187 reader.move().move().move(); 188 189 // parse content as placeholderValue 190 reader.basicDepthTagParsing('(', ')', token, tokens); 191 } 192 else if (reader.next == '(') 193 { 194 if (contentToken && contentToken.content && contentToken.content.length) 195 { 196 tokens ~= contentToken; 197 contentToken = new Token(TemplateType.content); 198 } 199 200 auto token = new Token(TemplateType.placeholder); 201 // skip @ and ( 202 reader.move().move(); 203 204 // parse content as placeholder 205 reader.basicDepthTagParsing('(', ')', token, tokens); 206 } 207 else if (reader.next == '{') 208 { 209 if (contentToken && contentToken.content && contentToken.content.length) 210 { 211 tokens ~= contentToken; 212 contentToken = new Token(TemplateType.content); 213 } 214 215 auto token = new Token(TemplateType.mixinCodeBlock); 216 // skip @ and { 217 reader.move().move(); 218 219 // parse content as mixinCodeBlock 220 reader.basicDepthTagParsing('{', '}', token, tokens); 221 } 222 else if (reader.next == '$' && reader.peek(2) == '=') 223 { 224 if (contentToken && contentToken.content && contentToken.content.length) 225 { 226 tokens ~= contentToken; 227 contentToken = new Token(TemplateType.content); 228 } 229 230 auto token = new Token(TemplateType.mixinExpression); 231 // skip @ $ and = 232 reader.move().move().move(); 233 234 // parse content as mixinExpression 235 reader.basicTagParsing(';', token, tokens); 236 } 237 else if (reader.next == '=') 238 { 239 if (contentToken && contentToken.content && contentToken.content.length) 240 { 241 tokens ~= contentToken; 242 contentToken = new Token(TemplateType.content); 243 } 244 245 auto token = new Token(TemplateType.mixinEscapeExpression); 246 // skip @ and = 247 reader.move().move(); 248 249 // parse content as mixinEscapeExpression 250 reader.basicTagParsing(';', token, tokens); 251 } 252 else if (reader.next == ':') 253 { 254 if (contentToken && contentToken.content && contentToken.content.length) 255 { 256 tokens ~= contentToken; 257 contentToken = new Token(TemplateType.content); 258 } 259 260 auto token = new Token(TemplateType.mixinStatement); 261 // skip @ and : 262 reader.move().move(); 263 264 // parse content as mixinStatement 265 auto scopeCharacter = reader.basicScopedTagParsing(token, tokens); 266 267 if (scopeCharacter == '{') 268 { 269 scopeStack.push(new Scope(scopeCharacter, '}')); 270 } 271 else if (scopeCharacter == '(') 272 { 273 scopeStack.push(new Scope(scopeCharacter, ')')); 274 } 275 else if (scopeCharacter == '[') 276 { 277 scopeStack.push(new Scope(scopeCharacter, ']')); 278 } 279 } 280 else if (reader.next == '<') 281 { 282 if (contentToken && contentToken.content && contentToken.content.length) 283 { 284 tokens ~= contentToken; 285 contentToken = new Token(TemplateType.content); 286 } 287 288 auto token = new Token(TemplateType.partialView); 289 // skip @ and < 290 reader.move().move(); 291 292 // parse content as partialView 293 reader.basicDepthTagParsing('<', '>', token, tokens); 294 } 295 else 296 { 297 contentToken.content ~= reader.current; 298 } 299 } 300 else if (reader.current != '\\' || reader.next != '@') 301 { 302 if (scopeStack.has) 303 { 304 if (reader.current == scopeStack.currentScope.scopeCharStart) 305 { 306 scopeStack.currentScope.scopeCount++; 307 } 308 else if (reader.current == scopeStack.currentScope.scopeCharEnd) 309 { 310 if (scopeStack.currentScope.scopeCount == 0) 311 { 312 if (contentToken && contentToken.content && contentToken.content.length) 313 { 314 tokens ~= contentToken; 315 contentToken = new Token(TemplateType.content); 316 } 317 318 auto token = new Token(TemplateType.mixinStatement); 319 token.content ~= scopeStack.currentScope.scopeCharEnd; 320 tokens ~= token; 321 322 scopeStack.pop(); 323 reader.move(); 324 continue; 325 } 326 327 scopeStack.currentScope.scopeCount--; 328 } 329 } 330 331 contentToken.content ~= reader.current; 332 } 333 } 334 335 reader.move(); 336 } 337 338 if (contentToken && contentToken.content && contentToken.content.length) 339 { 340 tokens ~= contentToken; 341 contentToken = new Token(TemplateType.content); 342 } 343 344 return tokens; 345 } 346 347 private: 348 void basicDepthTagParsing(StringReader reader, char tagStart, char tagEnd, Token token, ref Token[] tokens) 349 { 350 ptrdiff_t tagDepth = 0; 351 352 while (reader.has) 353 { 354 if (reader.current == tagStart) 355 { 356 tagDepth++; 357 } 358 else if (reader.current == tagEnd) 359 { 360 if (tagDepth == 0) 361 { 362 tokens ~= token; 363 break; 364 } 365 tagDepth--; 366 } 367 368 token.content ~= reader.current; 369 370 reader.move(); 371 } 372 } 373 374 void basicTagParsing(StringReader reader, char tagEnd, Token token, ref Token[] tokens) 375 { 376 while (reader.has) 377 { 378 if (reader.current == tagEnd) 379 { 380 tokens ~= token; 381 break; 382 } 383 384 token.content ~= reader.current; 385 386 reader.move(); 387 } 388 } 389 390 char basicScopedTagParsing(StringReader reader, Token token, ref Token[] tokens) 391 { 392 while (reader.has) 393 { 394 if (reader.current == '\r' || reader.current == '\n') 395 { 396 auto scopeCharacter = reader.last; 397 398 if (reader.current == '\r' && reader.next == '\n') 399 { 400 reader.move(); // skip the \n too ... 401 } 402 403 tokens ~= token; 404 return scopeCharacter; 405 } 406 407 token.content ~= reader.current; 408 409 reader.move(); 410 } 411 412 return '\0'; 413 }