1 /** 2 * Functions to work with DDOC macros. 3 * 4 * Provide functionalities to perform various macro-related operations, including: 5 * - Expand a text, with $(D expand). 6 * - Expand a macro, with $(D expandMacro); 7 * - Parse macro files (.ddoc), with $(D parseMacrosFile); 8 * - Parse a "Macros:" section, with $(D parseKeyValuePair). 9 * 10 * Most functions provide two interfaces. 11 * One takes an $(D OutputRange) to write to, and the other one is 12 * a convenience wrapper around it, which returns a string. 13 * It uses an $(D std.array.Appender) as the output range. 14 * 15 * Most functions take a 'macros' parameter. The user is not required to pass 16 * the standard D macros in it if he wants HTML output, the same macros that 17 * are hardwired into DDOC are hardwired into libddoc (B, I, D_CODE, etc...). 18 * 19 * Note: 20 * The code can contains embedded code, which will be highlighted by 21 * macros substitution (see corresponding DDOC macros). 22 * However, the substitution is *NOT* performed by this module, you should 23 * call $(D ddoc.highlight.highlight) first. 24 * If you forget to do so, libddoc will consider this as a developper 25 * mistake, and will kindly inform you with an assertion error. 26 * 27 * Copyright: © 2014 Economic Modeling Specialists, Intl. 28 * Authors: Brian Schott, Mathias 'Geod24' Lang 29 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 30 */ 31 module ddoc.macros; 32 33 /// 34 unittest 35 { 36 import std.conv : text; 37 import ddoc.lexer : Lexer; 38 39 // Ddoc has some hardwired macros, which will be automatically searched. 40 // List here: dlang.org/ddoc.html 41 auto l1 = Lexer(`A simple $(B Hello $(I world))`); 42 immutable r1 = expand(l1, null); 43 assert(r1 == `A simple <b>Hello <i>world</i></b>`, r1); 44 45 // Example on how to parse ddoc file / macros sections. 46 KeyValuePair[] pairs; 47 auto lm2 = Lexer(`GREETINGS = Hello $(B $0) 48 IDENTITY = $0`); 49 // Acts as we are parsing a ddoc file. 50 assert(parseKeyValuePair(lm2, pairs)); 51 // parseKeyValuePair parses up to the first invalid token, or until 52 // a section is reached. It returns false on parsing failure. 53 assert(lm2.empty, lm2.front.text); 54 assert(pairs.length == 2, text("Expected length 2, got: ", pairs.length)); 55 string[string] m2; 56 foreach (kv; pairs) 57 m2[kv[0]] = kv[1]; 58 // Macros are not expanded until the final call site. 59 // This allow for forward reference of macro and recursive macros. 60 assert(m2.get(`GREETINGS`, null) == `Hello $(B $0)`, m2.get(`GREETINGS`, null)); 61 assert(m2.get(`IDENTITY`, null) == `$0`, m2.get(`IDENTITY`, null)); 62 63 // There are some more specialized functions in this module, such as 64 // expandMacro which expects the lexer to be placed on a macro, and 65 // will consume the input (unlike expand, which exhaust a copy). 66 auto l2 = Lexer(`$(GREETINGS $(IDENTITY John Doe))`); 67 immutable r2 = expand(l2, m2); 68 assert(r2 == `Hello <b>John Doe</b>`, r2); 69 70 // Note that the expansions are not processed recursively. 71 // Hence, it's possible to have DDOC-formatted code inside DDOC. 72 auto l3 = Lexer(`This $(DOLLAR)(MACRO) do not expand recursively.`); 73 immutable r3 = expand(l3, null); 74 immutable e3 = `This $(MACRO) do not expand recursively.`; 75 assert(e3 == r3, r3); 76 } 77 78 import ddoc.lexer; 79 import std.exception; 80 import std.range; 81 import std.algorithm; 82 import std.stdio; 83 import std.typecons : Tuple; 84 85 alias KeyValuePair = Tuple!(string, string); 86 87 /// The set of ddoc's predefined macros. 88 immutable string[string] DEFAULT_MACROS; 89 90 shared static this() 91 { 92 DEFAULT_MACROS = [`B` : `<b>$0</b>`, `I` : `<i>$0</i>`, `U` : `<u>$0</u>`, 93 `P` : `<p>$0</p>`, `DL` : `<dl>$0</dl>`, `DT` : `<dt>$0</dt>`, 94 `DD` : `<dd>$0</dd>`, `TABLE` : `<table>$0</table>`, `TR` : `<tr>$0</tr>`, 95 `TH` : `<th>$0</th>`, `TD` : `<td>$0</td>`, `OL` : `<ol>$0</ol>`, 96 `UL` : `<ul>$0</ul>`, `LI` : `<li>$0</li>`, 97 `LINK` : `<a href="$0">$0</a>`, `LINK2` : `<a href="$1">$+</a>`, 98 `LPAREN` : `(`, `RPAREN` : `)`, `DOLLAR` : `$`, `BACKTICK` : "`", 99 `DEPRECATED` : `$0`, `RED` : `<font color=red>$0</font>`, 100 `BLUE` : `<font color=blue>$0</font>`, 101 `GREEN` : `<font color=green>$0</font>`, 102 `YELLOW` : `<font color=yellow>$0</font>`, 103 `BLACK` : `<font color=black>$0</font>`, 104 `WHITE` : `<font color=white>$0</font>`, 105 106 `D_CODE` : `<pre class="d_code">$0</pre>`, 107 `D_INLINECODE` : `<pre style="display:inline;" class="d_inline_code">$0</pre>`, 108 `D_COMMENT` : `$(GREEN $0)`, `D_STRING` : `$(RED $0)`, 109 `D_KEYWORD` : `$(BLUE $0)`, `D_PSYMBOL` : `$(U $0)`, 110 `D_PARAM` : `$(I $0)`, `DDOC` : `<html> 111 <head> 112 <META http-equiv="content-type" content="text/html; charset=utf-8"> 113 <title>$(TITLE)</title> 114 </head> 115 <body> 116 <h1>$(TITLE)</h1> 117 $(BODY) 118 <hr>$(SMALL Page generated by $(LINK2 https://github.com/economicmodeling/libddoc, libddoc). $(COPYRIGHT)) 119 </body> 120 </html>`, 121 122 `DDOC_BACKQUOTED` : `$(D_INLINECODE $0)`, `DDOC_COMMENT` : `<!-- $0 -->`, 123 `DDOC_DECL` : `$(DT $(BIG $0))`, `DDOC_DECL_DD` : `$(DD $0)`, 124 `DDOC_DITTO` : `$(BR)$0`, `DDOC_SECTIONS` : `$0`, 125 `DDOC_SUMMARY` : `$0$(BR)$(BR)`, `DDOC_DESCRIPTION` : `$0$(BR)$(BR)`, 126 `DDOC_AUTHORS` : "$(B Authors:)$(BR)\n$0$(BR)$(BR)", 127 `DDOC_BUGS` : "$(RED BUGS:)$(BR)\n$0$(BR)$(BR)", 128 `DDOC_COPYRIGHT` : "$(B Copyright:)$(BR)\n$0$(BR)$(BR)", 129 `DDOC_DATE` : "$(B Date:)$(BR)\n$0$(BR)$(BR)", 130 `DDOC_DEPRECATED` : "$(RED Deprecated:)$(BR)\n$0$(BR)$(BR)", 131 `DDOC_EXAMPLES` : "$(B Examples:)$(BR)\n$0$(BR)$(BR)", 132 `DDOC_HISTORY` : "$(B History:)$(BR)\n$0$(BR)$(BR)", 133 `DDOC_LICENSE` : "$(B License:)$(BR)\n$0$(BR)$(BR)", 134 `DDOC_RETURNS` : "$(B Returns:)$(BR)\n$0$(BR)$(BR)", 135 `DDOC_SEE_ALSO` : "$(B See Also:)$(BR)\n$0$(BR)$(BR)", 136 `DDOC_STANDARDS` : "$(B Standards:)$(BR)\n$0$(BR)$(BR)", 137 `DDOC_THROWS` : "$(B Throws:)$(BR)\n$0$(BR)$(BR)", 138 `DDOC_VERSION` : "$(B Version:)$(BR)\n$0$(BR)$(BR)", 139 `DDOC_SECTION_H` : `$(B $0)$(BR)$(BR)`, `DDOC_SECTION` : `$0$(BR)$(BR)`, 140 `DDOC_MEMBERS` : `$(DL $0)`, 141 `DDOC_MODULE_MEMBERS` : `$(DDOC_MEMBERS $0)`, 142 `DDOC_CLASS_MEMBERS` : `$(DDOC_MEMBERS $0)`, 143 `DDOC_STRUCT_MEMBERS` : `$(DDOC_MEMBERS $0)`, 144 `DDOC_ENUM_MEMBERS` : `$(DDOC_MEMBERS $0)`, 145 `DDOC_TEMPLATE_MEMBERS` : `$(DDOC_MEMBERS $0)`, 146 `DDOC_ENUM_BASETYPE` : `$0`, 147 `DDOC_PARAMS` : "$(B Params:)$(BR)\n$(TABLE $0)$(BR)", 148 `DDOC_PARAM_ROW` : `$(TR $0)`, `DDOC_PARAM_ID` : `$(TD $0)`, 149 `DDOC_PARAM_DESC` : `$(TD $0)`, `DDOC_BLANKLINE` : `$(BR)$(BR)`, 150 151 `DDOC_ANCHOR` : `<a name="$1"></a>`, `DDOC_PSYMBOL` : `$(U $0)`, 152 `DDOC_PSUPER_SYMBOL` : `$(U $0)`, `DDOC_KEYWORD` : `$(B $0)`, 153 `DDOC_PARAM` : `$(I $0)`, `ESCAPES` : `/</</ 154 />/>/ 155 &/&/`,]; 156 } 157 158 /** 159 * Write the text from the lexer to the $(D OutputRange), and expand any macro in it.. 160 * 161 * expand takes a $(D ddoc.Lexer), and will, until it's empty, write it's expanded version to $(D output). 162 * 163 * Params: 164 * input = A reference to the lexer to use. When expandMacros successfully returns, it will be empty. 165 * macros = A list of DDOC macros to use for expansion. This override the previous definitions, hardwired in 166 * DDOC. Which means if an user provides a macro such as $(D macros["B"] = "<h1>$0</h1>";), 167 * it will be used, otherwise the default $(D macros["B"] = "<b>$0</b>";) will be used. 168 * To undefine hardwired macros, just set them to an empty string: $(D macros["B"] = "";). 169 * removeUnknown = Set to true to make unknown macros disappear from the output or false to make them output unprocessed. 170 * output = An object satisfying $(D std.range.isOutputRange), usually a $(D std.array.Appender). 171 */ 172 void expand(O)(Lexer input, in string[string] macros, O output, bool removeUnknown = true) if (isOutputRange!(O, 173 string)) 174 { 175 // First, we need to turn every embedded code into a $(D_CODE) 176 while (!input.empty) 177 { 178 assert(input.front.type != Type.embedded, callHighlightMsg); 179 if (input.front.type == Type.dollar) 180 { 181 input.popFront(); 182 if (input.front.type == Type.lParen) 183 { 184 auto mac = Lexer(matchParenthesis(input), true); 185 if (!mac.empty) 186 { 187 if (!expandMacroImpl(mac, macros, output) && !removeUnknown) 188 { 189 output.put("$"); 190 output.put("("); 191 foreach (val; mac) 192 output.put(val.text); 193 output.put(")"); 194 } 195 } 196 } 197 else 198 output.put("$"); 199 } 200 else 201 { 202 output.put(input.front.text); 203 input.popFront(); 204 } 205 } 206 } 207 208 /// Ditto 209 string expand(Lexer input, string[string] macros, bool removeUnknown = true) 210 { 211 import std.array : appender; 212 213 auto app = appender!string(); 214 expand(input, macros, app, removeUnknown); 215 return app.data; 216 } 217 218 unittest 219 { 220 auto lex = Lexer(`Dat logo: $(LOGO dlang, Beautiful dlang logo)`); 221 immutable r = expand(lex, [`LOGO` : `<img src="images/$1_logo.png" alt="$2">`]); 222 immutable exp = `Dat logo: <img src="images/dlang_logo.png" alt="Beautiful dlang logo">`; 223 assert(r == exp, r); 224 } 225 226 unittest 227 { 228 auto lex = Lexer(`$(DIV, Evil)`); 229 immutable r = expand(lex, [`DIV` : `<div $1>$+</div>`]); 230 immutable exp = `<div >Evil</div>`; 231 assert(r == exp, r); 232 } 233 234 unittest 235 { 236 auto lex = Lexer(`$(B this) $(UNKN $(B is)) unknown!`); 237 immutable r = expand(lex, [`B` : `<b>$0</b>`], false); 238 immutable exp = `<b>this</b> $(UNKN $(B is)) unknown!`; 239 assert(r == exp, r); 240 } 241 242 /** 243 * Expand a macro, and write the result to an $(D OutputRange). 244 * 245 * It's the responsability of the caller to ensure that the lexer contains the 246 * beginning of a macro. The front of the input should be either a dollar 247 * followed an opening parenthesis, or an opening parenthesis. 248 * 249 * If the macro does not have a closing parenthesis, input will be exhausted 250 * and a $(D DdocException) will be thrown. 251 * 252 * Params: 253 * input = A reference to a lexer with front pointing to the macro. 254 * macros = Additional macros to use, in addition of DDOC's ones. 255 * output = An $(D OutputRange) to write to. 256 */ 257 void expandMacro(O)(ref Lexer input, in string[string] macros, O output) if ( 258 isOutputRange!(O, string)) 259 in 260 { 261 import std.conv : text; 262 263 assert(input.front.type == Type.dollar || input.front.type == Type.lParen, 264 text("$ or ( expected, not ", input.front.type)); 265 } 266 do 267 { 268 import std.conv : text; 269 270 if (input.front.type == Type.dollar) 271 input.popFront(); 272 assert(input.front.type == Type.lParen, text(input.front.type)); 273 auto l = Lexer(matchParenthesis(input), true); 274 expandMacroImpl(l, macros, output); 275 } 276 277 /// Ditto 278 string expandMacro(ref Lexer input, in string[string] macros) 279 in 280 { 281 import std.conv : text; 282 283 assert(input.front.type == Type.dollar || input.front.type == Type.lParen, 284 text("$ or ( expected, not ", input.front.type)); 285 } 286 do 287 { 288 import std.array : appender; 289 290 auto app = appender!string(); 291 expandMacro(input, macros, app); 292 return app.data; 293 } 294 295 /// 296 unittest 297 { 298 import ddoc.lexer : Lexer; 299 import std.array : appender; 300 301 auto macros = [ 302 "IDENTITY" : "$0", "HWORLD" : "$(IDENTITY Hello world!)", 303 "ARGS" : "$(IDENTITY $1 $+)", "GREETINGS" : "$(IDENTITY $(ARGS Hello,$0))", 304 ]; 305 306 auto l1 = Lexer(`$(HWORLD)`); 307 immutable r1 = expandMacro(l1, macros); 308 assert(r1 == "Hello world!", r1); 309 310 auto l2 = Lexer(`$(B $(IDENTITY $(GREETINGS John Malkovich)))`); 311 immutable r2 = expandMacro(l2, macros); 312 assert(r2 == "<b>Hello John Malkovich</b>", r2); 313 } 314 315 /// A simple example, with recursive macros: 316 unittest 317 { 318 import ddoc.lexer : Lexer; 319 320 auto lex = Lexer(`$(MYTEST Un,jour,mon,prince,viendra)`); 321 auto macros = [`MYTEST` : `$1 $(MYTEST $+)`]; 322 // Note: There's also a version of expand that takes an OutputRange. 323 immutable result = expand(lex, macros); 324 assert(result == `Un jour mon prince viendra `, result); 325 } 326 327 unittest 328 { 329 auto macros = [ 330 "D" : "<b>$0</b>", "P" : "<p>$(D $0)</p>", "KP" : "<b>$1</b><i>$+</i>", 331 "LREF" : `<a href="#$1">$(D $1)</a>` 332 ]; 333 auto l = Lexer(`$(D something $(KP a, b) $(P else), abcd) $(LREF byLineAsync)`c); 334 immutable expected = `<b>something <b>a</b><i>b</i> <p><b>else</b></p>, abcd</b> <a href="#byLineAsync"><b>byLineAsync</b></a>`; 335 auto result = appender!string(); 336 expand(l, macros, result); 337 assert(result.data == expected, result.data); 338 } 339 340 unittest 341 { 342 auto l1 = Lexer("Do you have a $(RPAREN) problem with $(LPAREN) me?"); 343 immutable r1 = expand(l1, null); 344 assert(r1 == "Do you have a ) problem with ( me?", r1); 345 346 auto l2 = Lexer("And (with $(LPAREN) me) ?"); 347 immutable r2 = expand(l2, null); 348 assert(r2 == "And (with ( me) ?", r2); 349 350 auto l3 = Lexer("What about $(TEST me) ?"); 351 immutable r3 = expand(l3, ["TEST" : "($0"]); 352 assert(r3 == "What about (me ?", r3); 353 } 354 355 /** 356 * Parses macros files, usually with extension .ddoc. 357 * 358 * Macros files are files that only contains macros definitions. 359 * Newline after a macro is part of this macro, so a blank line between 360 * macro A and macro B will lead to macro A having a trailing newline. 361 * If you wish to split your file in blocks, terminate each block with 362 * a dummy macro, e.g: '_' (underscore). 363 * 364 * Params: 365 * paths = A variadic array with paths to ddoc files. 366 * 367 * Returns: 368 * An associative array containing all the macros parsed from the files. 369 * In case of multiple definitions, macros are overriden. 370 */ 371 string[string] parseMacrosFile(R)(R paths) if (isInputRange!(R)) 372 { 373 import std.exception : enforceEx; 374 import std.file : readText; 375 import std.conv : text; 376 377 string[string] ret; 378 foreach (file; paths) 379 { 380 KeyValuePair[] pairs; 381 auto txt = readText(file); 382 auto lexer = Lexer(txt, true); 383 parseKeyValuePair(lexer, pairs); 384 enforceEx!DdocException(lexer.empty, text("Unparsed data (", 385 lexer.offset, "): ", lexer.text[lexer.offset .. $])); 386 foreach (kv; pairs) 387 ret[kv[0]] = kv[1]; 388 } 389 return ret; 390 } 391 392 /** 393 * Parses macros (or params) declaration list until the lexer is empty. 394 * 395 * Macros are simple Key/Value pair. So, a macro is declared as: NAME=VALUE. 396 * Any number of whitespace (space / tab) can precede and follow the equal sign. 397 * 398 * Params: 399 * lexer = A reference to lexer consisting solely of macros definition (if $(D stopAtSection) is false), 400 * or consisting of a macro followed by other sections. 401 * Consequently, at the end of the parsing, the lexer will be empty or may point to a section. 402 * pairs = A reference to an array of $(D KeyValuePair), where the macros will be stored. 403 * 404 * Returns: true if the parsing succeeded. 405 */ 406 bool parseKeyValuePair(ref Lexer lexer, ref KeyValuePair[] pairs) 407 { 408 import std.array : appender; 409 import std.conv : text; 410 411 string prevKey, key; 412 string prevValue, value; 413 size_t start; 414 while (!lexer.empty) 415 { 416 // If parseAsKeyValuePair returns true, we stopped on a newline. 417 // If it returns false, we're either on a section (header), 418 // or the continuation of a macro. 419 if (!parseAsKeyValuePair(lexer, key, value)) 420 { 421 if (prevKey == null) // First pass and invalid data 422 return false; 423 if (lexer.front.type == Type.header) 424 break; 425 assert(lexer.offset >= prevValue.length); 426 if (prevValue.length == 0) 427 start = tokOffset(lexer); 428 while (!lexer.empty && lexer.front.type != Type.newline) 429 lexer.popFront(); 430 prevValue = lexer.text[start .. lexer.offset]; 431 } 432 else 433 { 434 // New macro, we can save the previous one. 435 // The only case when key would not be defined is on first pass. 436 if (prevKey) 437 pairs ~= KeyValuePair(prevKey, prevValue); 438 prevKey = key; 439 prevValue = value; 440 key = value = null; 441 start = tokOffset(lexer) - prevValue.length; 442 } 443 if (!lexer.empty) 444 { 445 assert(lexer.front.type == Type.newline, text("Front: ", 446 lexer.front.type, ", text: ", lexer.text[lexer.offset .. $])); 447 lexer.popFront(); 448 } 449 } 450 451 if (prevKey) 452 pairs ~= KeyValuePair(prevKey, prevValue); 453 454 return true; 455 } 456 457 private: 458 // upperArgs is a string[11] actually, or null. 459 bool expandMacroImpl(O)(Lexer input, in string[string] macros, O output) 460 { 461 import std.conv : text; 462 463 //debug writeln("Expanding: ", input.text); 464 // Check if the macro exist and get it's value. 465 if (input.front.type != Type.word) 466 return false; 467 string macroName = input.front.text; 468 //debug writeln("[EXPAND] Macro name: ", input.front.text); 469 string macroValue = lookup(macroName, macros); 470 // No point loosing time if the macro is undefined. 471 if (macroValue is null) 472 return false; 473 //debug writeln("[EXPAND] Macro value: ", macroValue); 474 input.popFront(); 475 476 // Special case for $(DDOC). It's ugly, but it gets the job done. 477 if (input.empty && macroName == "BODY") 478 { 479 output.put(lookup("BODY", macros)); 480 return true; 481 } 482 483 // Collect the arguments 484 if (!input.empty && (input.front.type == Type.whitespace || input.front.type == Type.newline)) 485 input.popFront(); 486 string[11] arguments; 487 collectMacroArguments(input, arguments); 488 489 // First pass 490 auto argOutput = appender!string(); 491 if (!replaceArgs(macroValue, arguments, argOutput)) 492 return true; 493 494 // Second pass 495 replaceMacs(argOutput.data, macros, output); 496 return true; 497 } 498 499 unittest 500 { 501 auto a1 = appender!string(); 502 expandMacroImpl(Lexer(`B value`), null, a1); 503 assert(a1.data == `<b>value</b>`, a1.data); 504 505 auto a2 = appender!string(); 506 expandMacroImpl(Lexer(`IDENTITY $(B value)`), ["IDENTITY" : "$0"], a2); 507 assert(a2.data == `<b>value</b>`, a2.data); 508 } 509 510 // Try to parse a line as a KeyValuePair, returns false if it fails 511 private bool parseAsKeyValuePair(ref Lexer olexer, ref string key, ref string value) 512 { 513 auto lexer = olexer; 514 while (!lexer.empty && (lexer.front.type == Type.whitespace || lexer.front.type == Type.newline)) 515 lexer.popFront(); 516 if (!lexer.empty && lexer.front.type == Type.word) 517 { 518 key = lexer.front.text; 519 lexer.popFront(); 520 } 521 else 522 return false; 523 while (!lexer.empty && lexer.front.type == Type.whitespace) 524 lexer.popFront(); 525 if (!lexer.empty && lexer.front.type == Type.equals) 526 lexer.popFront(); 527 else 528 return false; 529 while (!lexer.empty && lexer.front.type == Type.whitespace) 530 lexer.popFront(); 531 assert(lexer.offset > 0, "Something is wrong with the lexer"); 532 // Offset points to the END of the token, not the beginning. 533 immutable size_t start = tokOffset(lexer); 534 while (!lexer.empty && lexer.front.type != Type.newline) 535 { 536 assert(lexer.front.type != Type.header); 537 lexer.popFront(); 538 } 539 immutable size_t end = lexer.offset - ((start != lexer.offset 540 && lexer.offset != lexer.text.length) ? 1 : 0); 541 value = lexer.text[start .. end]; 542 olexer = lexer; 543 return true; 544 } 545 546 // Note: For macro $(NAME arg1,arg2), collectMacroArguments receive "arg1,arg2". 547 size_t collectMacroArguments(Lexer input, ref string[11] args) 548 { 549 import std.conv : text; 550 551 size_t argPos = 1; 552 size_t argStart = tokOffset(input); 553 args[] = null; 554 if (input.empty) 555 return 0; 556 args[0] = input.text[tokOffset(input) .. $]; 557 while (!input.empty) 558 { 559 assert(input.front.type != Type.embedded, callHighlightMsg); 560 switch (input.front.type) 561 { 562 case Type.comma: 563 if (argPos <= 9) 564 args[argPos++] = input.text[argStart .. (input.offset - 1)]; 565 input.popFront(); 566 stripWhitespace(input); 567 argStart = tokOffset(input); 568 // Set the $+ parameter. 569 if (argPos == 2) 570 args[10] = input.text[tokOffset(input) .. $]; 571 break; 572 case Type.lParen: 573 // Advance the lexer to the matching parenthesis. 574 matchParenthesis(input); 575 break; 576 // TODO: Implement ", ' and <-- pairing. 577 default: 578 input.popFront(); 579 } 580 } 581 assert(argPos >= 1 && argPos <= 10, text(argPos)); 582 if (argPos <= 9) 583 args[argPos] = input.text[argStart .. input.offset]; 584 return argPos; 585 } 586 587 unittest 588 { 589 import std.conv : text; 590 591 string[11] args; 592 593 auto l1 = Lexer(`Hello, world`); 594 auto c1 = collectMacroArguments(l1, args); 595 assert(c1 == 2, text(c1)); 596 assert(args[0] == `Hello, world`, args[0]); 597 assert(args[1] == `Hello`, args[1]); 598 assert(args[2] == `world`, args[2]); 599 for (size_t i = 3; i < 10; ++i) 600 assert(args[i] is null, args[i]); 601 assert(args[10] == `world`, args[10]); 602 603 auto l2 = Lexer(`goodbye,cruel,world,I,will,happily,return,home`); 604 auto c2 = collectMacroArguments(l2, args); 605 assert(c2 == 8, text(c2)); 606 assert(args[0] == `goodbye,cruel,world,I,will,happily,return,home`, args[0]); 607 assert(args[1] == `goodbye`, args[1]); 608 assert(args[2] == `cruel`, args[2]); 609 assert(args[3] == `world`, args[3]); 610 assert(args[4] == `I`, args[4]); 611 assert(args[5] == `will`, args[5]); 612 assert(args[6] == `happily`, args[6]); 613 assert(args[7] == `return`, args[7]); 614 assert(args[8] == `home`, args[8]); 615 assert(args[9] is null, args[9]); 616 assert(args[10] == `cruel,world,I,will,happily,return,home`, args[10]); 617 618 // It's not as easy as a split ! 619 auto l3 = Lexer(`this,(is,(just,two),args)`); 620 auto c3 = collectMacroArguments(l3, args); 621 assert(c3 == 2, text(c3)); 622 assert(args[0] == `this,(is,(just,two),args)`, args[0]); 623 assert(args[1] == `this`, args[1]); 624 assert(args[2] == `(is,(just,two),args)`, args[2]); 625 for (size_t i = 3; i < 10; ++i) 626 assert(args[i] is null, args[i]); 627 assert(args[10] == `(is,(just,two),args)`, args[10]); 628 629 auto l4 = Lexer(``); 630 auto c4 = collectMacroArguments(l4, args); 631 assert(c4 == 0, text(c4)); 632 for (size_t i = 0; i < 11; ++i) 633 assert(args[i] is null, args[i]); 634 635 import std.string : split; 636 637 enum first = `I,am,happy,to,join,with,you,today,in,what,will,go,down,in,history,as,the,greatest,demonstration,for,freedom,in,the,history,of,our,nation.`; 638 auto l5 = Lexer(first); 639 auto c5 = collectMacroArguments(l5, args); 640 assert(c5 == 10, text(c5)); 641 assert(args[0] == first, args[0]); 642 foreach (idx, word; first.split(",")[0 .. 9]) 643 assert(args[idx + 1] == word, text(word, " != ", args[idx + 1])); 644 assert(args[10] == first[2 .. $], args[10]); 645 646 // TODO: ", ', {, <--, matched and unmatched. 647 } 648 649 // Where the grunt work is done... 650 651 bool replaceArgs(O)(string val, in string[11] args, O output) 652 { 653 import std.conv : text; 654 import std.ascii : isDigit; 655 656 bool hasEnd; 657 auto lex = Lexer(val, true); 658 while (!lex.empty) 659 { 660 assert(lex.front.type != Type.embedded, callHighlightMsg); 661 switch (lex.front.type) 662 { 663 case Type.dollar: 664 lex.popFront(); 665 // It could be $1_test 666 if (isDigit(lex.front.text[0])) 667 { 668 auto idx = lex.front.text[0] - '0'; 669 assert(idx >= 0 && idx <= 9, text(idx)); 670 // Missing argument 671 if (args[idx] is null) 672 { 673 lex.popFront(); 674 continue; 675 } 676 output.put(args[idx]); 677 output.put(lex.front.text[1 .. $]); 678 lex.popFront(); 679 } 680 else if (lex.front.text == "+") 681 { 682 if (args == string[11].init) 683 return false; 684 685 lex.popFront(); 686 output.put(args[10]); 687 } 688 else 689 { 690 output.put("$"); 691 } 692 break; 693 case Type.lParen: 694 output.put("("); 695 if (!replaceArgs(matchParenthesis(lex, &hasEnd), args, output)) 696 return false; 697 if (hasEnd) 698 output.put(")"); 699 break; 700 default: 701 output.put(lex.front.text); 702 lex.popFront(); 703 } 704 } 705 return true; 706 } 707 708 unittest 709 { 710 string[11] args; 711 712 auto a1 = appender!string; 713 args[0] = "Some kind of test, I guess"; 714 args[1] = "Some kind of test"; 715 args[2] = " I guess"; 716 assert(replaceArgs("$(MY $(SUPER $(MACRO $0)))", args, a1)); 717 assert(a1.data == "$(MY $(SUPER $(MACRO Some kind of test, I guess)))", a1.data); 718 719 auto a2 = appender!string; 720 args[] = null; 721 args[0] = "Some,kind,of,test"; 722 args[1] = "Some"; 723 args[2] = "kind"; 724 args[3] = "of"; 725 args[4] = "test"; 726 args[10] = "kind,of,test"; 727 assert(replaceArgs("$(SOME $(MACRO $1 $+))", args, a2)); 728 assert(a2.data == "$(SOME $(MACRO Some kind,of,test))", a2.data); 729 730 auto a3 = appender!string; 731 args[] = null; 732 args[0] = "Some,kind"; 733 args[1] = "Some"; 734 args[2] = "kind"; 735 args[10] = "kind"; 736 assert(replaceArgs("$(SOME $(MACRO $1 $2 $3))", args, a3), a3.data); 737 738 auto a4 = appender!string; 739 args[] = null; 740 args[0] = "Some kind of test, I guess"; 741 assert(replaceArgs("$(MACRO $0 $1)", args, a4)); 742 assert(a4.data == "$(MACRO Some kind of test, I guess )", a4.data); 743 } 744 745 void replaceMacs(O)(string val, in string[string] macros, O output) 746 { 747 //debug writeln("[REPLACE] Arguments replaced: ", val); 748 bool hasEnd; 749 auto lex = Lexer(val, true); 750 while (!lex.empty) 751 { 752 assert(lex.front.type != Type.embedded, callHighlightMsg); 753 switch (lex.front.type) 754 { 755 case Type.dollar: 756 lex.popFront(); 757 if (lex.front.type == Type.lParen) 758 expandMacro(lex, macros, output); 759 else 760 output.put("$"); 761 break; 762 case Type.lParen: 763 output.put("("); 764 auto par = matchParenthesis(lex, &hasEnd); 765 expand(Lexer(par), macros, output); 766 if (hasEnd) 767 output.put(")"); 768 break; 769 default: 770 output.put(lex.front.text); 771 lex.popFront(); 772 } 773 } 774 } 775 776 // Some utilities functions 777 778 /** 779 * Must be called with a parenthesis as the front item of $(D lexer). 780 * Will move the lexer forward until a matching parenthesis is met, 781 * taking nesting into account. 782 * If no matching parenthesis is met, returns null (and $(D lexer) will be empty). 783 */ 784 string matchParenthesis(ref Lexer lexer, bool* hasEnd = null) 785 in 786 { 787 import std.conv : text; 788 789 assert(lexer.front.type == Type.lParen, text(lexer.front)); 790 assert(lexer.offset); 791 } 792 do 793 { 794 size_t count; 795 size_t start = lexer.offset; 796 do 797 { 798 if (lexer.front.type == Type.rParen) 799 --count; 800 else if (lexer.front.type == Type.lParen) 801 ++count; 802 lexer.popFront(); 803 } 804 while (count > 0 && !lexer.empty); 805 size_t end = (lexer.empty) ? lexer.text.length : tokOffset(lexer); 806 if (hasEnd !is null) 807 *hasEnd = (count == 0); 808 if (count == 0) 809 end -= 1; 810 return lexer.text[start .. end]; 811 } 812 813 unittest 814 { 815 auto l1 = Lexer(`(Hello) World`); 816 immutable r1 = matchParenthesis(l1); 817 assert(r1 == "Hello", r1); 818 assert(!l1.empty); 819 820 auto l2 = Lexer(`()`); 821 immutable r2 = matchParenthesis(l2); 822 assert(r2 == "", r2); 823 assert(l2.empty); 824 825 auto l3 = Lexer(`(())`); 826 immutable r3 = matchParenthesis(l3); 827 assert(r3 == "()", r3); 828 assert(l3.empty); 829 830 auto l4 = Lexer(`W (He(l)lo)`); 831 l4.popFront(); 832 l4.popFront(); 833 immutable r4 = matchParenthesis(l4); 834 assert(r4 == "He(l)lo", r4); 835 assert(l4.empty); 836 837 auto l5 = Lexer(` @(Hello()) ()`); 838 l5.popFront(); 839 l5.popFront(); 840 immutable r5 = matchParenthesis(l5); 841 assert(r5 == "Hello()", r5); 842 assert(!l5.empty); 843 844 auto l6 = Lexer(`(Hello() (`); 845 immutable r6 = matchParenthesis(l6); 846 assert(r6 == "Hello() (", r6); 847 assert(l6.empty); 848 } 849 850 package size_t tokOffset(in Lexer lex) 851 { 852 return lex.offset - lex.front.text.length; 853 } 854 855 unittest 856 { 857 import std.conv : text; 858 859 auto lex = Lexer(`My (friend) $ lives abroad`); 860 auto expected = [0, 2, 4, 5, 11, 12, 13, 14, 15, 20, 21]; 861 while (!lex.empty) 862 { 863 assert(expected.length > 0, "Test and results are not in sync"); 864 assert(tokOffset(lex) == expected[0], text(lex.front, " : ", 865 tokOffset(lex), " -- ", expected[0])); 866 lex.popFront(); 867 expected = expected[1 .. $]; 868 } 869 } 870 871 string lookup(in string name, in string[string] macros, string defVal = null) 872 { 873 auto p = name in macros; 874 if (p is null) 875 return DEFAULT_MACROS.get(name, defVal); 876 return *p; 877 } 878 879 /// Returns: The number of offset skipped. 880 package size_t stripWhitespace(ref Lexer lexer) 881 { 882 size_t start = lexer.offset; 883 while (!lexer.empty && (lexer.front.type == Type.whitespace || lexer.front.type == Type.newline)) 884 { 885 start = lexer.offset; 886 lexer.popFront(); 887 } 888 return start; 889 } 890 891 enum callHighlightMsg = "You should call ddoc.hightlight.hightlight(string) first.";