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