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` : `/</&lt;/
154 />/&gt;/
155 &/&amp;/`,];
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.";