1 /// Author: Aziz Köksal
2 /// License: GPL3
3 /// $(Maturity high)
4 module dil.doc.Macro;
5 
6 import dil.doc.Parser;
7 import dil.lexer.Funcs;
8 import dil.i18n.Messages;
9 import dil.Unicode,
10        dil.Diagnostics,
11        dil.String,
12        dil.Array;
13 import common;
14 
15 /// The DDoc macro class.
16 class Macro
17 {
18   /// Enum of special marker characters.
19   enum Marker
20   {
21     Opening   = '\1', /// Opening macro character.
22     Closing   = '\2', /// Closing macro character.
23     Unclosed  = '\3', /// Unclosed macro character.
24     ArgsStart = '\4', /// Marks the start of a macro's arguments.
25   }
26 
27   cstring name; /// The name of the macro.
28   cstring text; /// The substitution text.
29   uint callLevel; /// The recursive call level.
30 
31   /// Constructs a Macro object.
32   this(cstring name, cstring text)
33   {
34     this.name = name;
35     this.text = text;
36   }
37 
38   /// Converts a macro text to the internal format.
39   static cstring convert(cstring text)
40   {
41     CharArray result;
42     auto p = text.ptr;
43     auto end = p + text.length;
44     auto prev = p;
45     CharArray parens; // Stack of parentheses and markers.
46     for (; p < end; p++)
47       switch (*p)
48       {
49       case '$':
50         auto p2 = p+2;
51         if (p2 < end && p[1] == '(' && isIdentifierStart(p2, end)) // IdStart
52         { // Scanned: "$(IdStart"
53           if (!result.ptr)
54             result.cap = text.length; // Reserve space the first time.
55           if (prev != p)
56             result ~= slice(prev, p); // Copy previous text.
57           parens ~= Macro.Marker.Opening;
58           result ~= Macro.Marker.Opening; // Relace "$(".
59           prev = p = p2; // Move to IdStart.
60         }
61         break;
62       case '(': // Only push on the stack, when inside a macro.
63         if (parens.len)
64           parens ~= '(';
65         break;
66       case ')':
67         if (!parens.len)
68           break; // Ignore parentheses outside macro.
69         if (parens[Neg(1)] == Macro.Marker.Opening)
70         { // Found matching closing parenthesis.
71           if (prev != p) {
72             result ~= slice(prev, p);
73             prev = p+1;
74           }
75           result ~= Macro.Marker.Closing; // Replace ')'.
76         }
77         else
78           assert(parens[Neg(1)] == '(');
79         parens.cur--;
80         break;
81       default:
82       }
83     if (prev == text.ptr)
84       return text; // No macros found. Return original text.
85     if (prev < end)
86       result ~= slice(prev, end);
87     foreach (c; parens[])
88       if (c == Macro.Marker.Opening) // Unclosed macros?
89         result ~= Macro.Marker.Unclosed; // Add marker for errors.
90     return result[];
91   }
92 }
93 
94 void testMacroConvert()
95 {
96   scope msg = new UnittestMsg("Testing function Macro.convert().");
97   alias fn = Macro.convert;
98   auto r = fn("$(bla())");
99   assert(r == "\1bla()\2");
100   r = fn("($(ÖÜTER ( $(NestedMacro ?,ds()))))");
101   assert(r == "(\1ÖÜTER ( \1NestedMacro ?,ds()\2)\2)");
102   r = fn("$(Unclosed macro ");
103   assert(r == "\1Unclosed macro \3");
104 }
105 
106 /// Maps macro names to Macro objects.
107 ///
108 /// MacroTables can be chained so that they build a linear hierarchy.
109 /// Macro definitions in the current table
110 /// override the ones in the parent tables.
111 class MacroTable
112 {
113   /// The parent in the hierarchy. Or null if this is the root.
114   MacroTable parent;
115   /// The associative array that holds the macro definitions.
116   Macro[hash_t] table;
117 
118   /// Constructs a MacroTable instance.
119   this(MacroTable parent = null)
120   {
121     this.parent = parent;
122   }
123 
124   /// Inserts the macro m into the table.
125   /// Overwrites the current macro if one exists.
126   /// Params:
127   ///   m = The macro.
128   ///   convertText = Convert the macro text to the internal format.
129   void insert(Macro m, bool convertText = true)
130   {
131     if (convertText)
132       m.text = Macro.convert(m.text);
133     table[hashOf(m.name)] = m;
134   }
135 
136   /// Inserts an array of macros into the table.
137   void insert(Macro[] macros)
138   {
139     foreach (m; macros)
140       insert(m);
141   }
142 
143   /// Creates a macro using name and text and inserts it into the table.
144   void insert(cstring name, cstring text)
145   {
146     insert(new Macro(name, text));
147   }
148 
149   /// Creates a macro using name[n] and text[n] pairs
150   /// and inserts it into the table.
151   void insert(cstring[] names, cstring[] texts)
152   {
153     assert(names.length == texts.length);
154     foreach (i, name; names)
155       insert(name, texts[i]);
156   }
157 
158   /// Searches for a macro.
159   ///
160   /// If the macro isn't found in this table the search
161   /// continues upwards in the table hierarchy.
162   /// Returns: the macro if found, or null if not.
163   Macro search(cstring name)
164   {
165     if (auto pmacro = hashOf(name) in table)
166       return *pmacro;
167     if (!isRoot())
168       return parent.search(name);
169     return null;
170   }
171 
172   /// Returns: true if this is the root of the hierarchy.
173   bool isRoot()
174   { return parent is null; }
175 }
176 
177 /// Parses a text with macro definitions.
178 struct MacroParser
179 {
180 static:
181   Macro[] parse(cstring text)
182   {
183     IdentValueParser parser;
184     auto idvalues = parser.parse(text);
185     auto macros = new Macro[idvalues.length];
186     foreach (i, idvalue; idvalues)
187       macros[i] = new Macro(idvalue.ident, idvalue.value);
188     return macros;
189   }
190 }
191 
192 /// Expands DDoc macros in a text.
193 struct MacroExpander
194 {
195   MacroTable mtable; /// Used to look up macros.
196   Diagnostics diag; /// Collects warning messages.
197   cstring filePath; /// Used in warning messages.
198   CharArray buffer; /// Text buffer.
199   cstring[10] margs; /// Currently parsed macro arguments.
200 
201   /// Starts expanding the macros.
202   static cstring expand(MacroTable mtable, cstring text, cstring filePath,
203                         Diagnostics diag = null)
204   {
205     MacroExpander me;
206     me.mtable = mtable;
207     me.diag = diag;
208     me.filePath = filePath;
209     me.expandMacros(text);
210     return me.buffer[];
211   }
212 
213   /// Reports a warning message.
214   void warning(MID mid, cstring macroName)
215   {
216     if (diag !is null)
217       diag ~= new Warning(new Location(filePath, 0),
218         diag.formatMsg(mid, macroName));
219   }
220 
221   /// Expands the macros in the text using the definitions from the table.
222   void expandMacros(cstring text
223     /+, cstring prevArg0 = null, uint depth = 1000+/)
224   { // prevArg0 and depth are commented out, causes problems with recursion.
225     // if (depth == 0)
226     //   return  text;
227     // depth--;
228     auto p = text.ptr;
229     auto textEnd = p + text.length;
230     auto macroEnd = p;
231 
232     // Scan for: "\1MacroName ...\2"
233     for (; p+2 < textEnd; p++) // 2 chars look-ahead.
234       if (*p == Macro.Marker.Opening)
235       {
236         // Copy string between macros.
237         if (macroEnd != p)
238           buffer ~= slice(macroEnd, p);
239         p++;
240         if (auto macroName = scanIdentifier(p, textEnd))
241         { // Scanned "\1MacroName" so far.
242           // Get arguments.
243           auto macroArgs = scanArguments(margs, p, textEnd);
244           macroEnd = p;
245           // Closing parenthesis not found?
246           if (p == textEnd || *p == Macro.Marker.Unclosed)
247             warning(MID.UnterminatedDDocMacro, macroName),
248             (buffer ~= "$(" ~ macroName ~ " ");
249           else // p points to the closing marker.
250             macroEnd = p+1; // Point past the closing marker.
251 
252           auto macro_ = mtable.search(macroName);
253           if (!macro_)
254             warning(MID.UndefinedDDocMacro, macroName),
255             // Insert into the table to avoid more warnings.
256             mtable.insert(macro_ = new Macro(macroName, "$0"));
257           // Ignore recursive macro if:
258           //auto macroArg0 = macroArgs.length ? macroArgs[0] : null;
259           if (macro_.callLevel != 0 &&
260               (macroArgs.length == 0/+ || // Macro has no arguments.
261                 prevArg0 == macroArg0+/)) // macroArg0 equals previous arg0.
262             continue;
263           macro_.callLevel++;
264           // Expand the arguments in the macro text.
265           auto expandedText = expandArguments(macro_.text, macroArgs);
266           // Expand macros inside that text.
267           expandMacros(expandedText/+, macroArg0, depth+/);
268           macro_.callLevel--;
269         }
270       }
271     if (macroEnd < textEnd)
272       buffer ~= slice(macroEnd, textEnd);
273   }
274 
275   /// Scans until the closing parenthesis is found. Sets p to one char past it.
276   /// Returns: [arg0, arg1, arg2 ...].
277   /// Params:
278   ///   args = Provides space for at least 10 arguments.
279   ///   ref_p = Will point to Macro.Marker.Closing or Marker.Unclosed,
280   ///           or to textEnd if it wasn't found.
281   cstring[] scanArguments(cstring[] args, ref cchar* ref_p, cchar* textEnd)
282   in { assert(args.length == 10); }
283   out(outargs) { assert(outargs.length != 1); }
284   body
285   {
286     // D specs: "The argument text can contain nested parentheses,
287     //           "" or '' strings, comments, or tags."
288     uint mlevel = 1; // Nesting level of macros.
289     uint plevel = 0; // Nesting level of parentheses.
290     size_t nargs;
291     auto p = ref_p; // Use a non-ref variable to scan the text.
292 
293     if (p < textEnd && isspace(*p)) // Skip first space.
294       p++;
295     // Skip leading spaces.
296     //while (p < textEnd && isspace(*p))
297     //  p++;
298 
299     // Skip special arguments marker. (DIL extension!)
300     // This is needed to preserve the whitespace that comes after the marker.
301     if (p < textEnd && *p == Macro.Marker.ArgsStart)
302       p++;
303 
304     auto arg0Begin = p; // Begin of all arguments.
305     auto argBegin = p; // Begin of current argument.
306   MainLoop:
307     while (p < textEnd)
308     {
309       switch (*p)
310       {
311       case Macro.Marker.Opening:
312         mlevel++;
313         break;
314       case Macro.Marker.Closing, Macro.Marker.Unclosed:
315         if (--mlevel == 0) // Final closing macro character?
316           break MainLoop;
317         break;
318       case '(': plevel++; break;
319       case ')': if (plevel) plevel--; break;
320       case ',':
321         if ((plevel+mlevel) != 1) // Ignore comma if inside ( ).
322           break;
323         if (nargs < 9)
324         { // Set nth argument.
325           args[++nargs] = slice(argBegin, p);
326           if (++p < textEnd && isspace(*p)) // Skip first space.
327             p++;
328           //while (++p < textEnd && isspace(*p)) // Skip spaces.
329           //{}
330           argBegin = p;
331         }
332         else
333           p++; // Skip ','.
334         continue;
335       // Commented out: causes too many problems in the expansion pass.
336       // case '"', '\'':
337       //   auto c = *p;
338       //   while (++p < textEnd && *p != c) // Scan to next " or '.
339       //   {}
340       //   assert(*p == c || p == textEnd);
341       //   if (p == textEnd)
342       //     break MainLoop;
343       //   break;
344       case '<':
345         p++;
346         if (p+2 < textEnd && *p == '!' && p[1] == '-' && p[2] == '-') // <!--
347         {
348           p += 2; // Point to 2nd '-'.
349           // Scan to closing "-->".
350           while (++p < textEnd)
351             if (p+2 < textEnd && *p == '-' && p[1] == '-' && p[2] == '>') {
352               p += 2; // Point to '>'.
353               break;
354             }
355         } // <tag ...> or </tag>
356         else if (p < textEnd && (isalpha(*p) || *p == '/'))
357           while (++p < textEnd && *p != '>') // Skip to closing '>'.
358           {}
359         else
360           continue MainLoop;
361         assert(p <= textEnd);
362         if (p == textEnd)
363           break MainLoop;
364         assert(*p == '>');
365         break;
366       default:
367       }
368       p++;
369     }
370     assert(mlevel == 0 &&
371       (*p == Macro.Marker.Closing || *p == Macro.Marker.Unclosed) ||
372       p == textEnd);
373     ref_p = p;
374     if (arg0Begin == p)
375       return null; // No arguments.
376     args[0] = slice(arg0Begin, p); // arg0 spans the whole argument list.
377     if (nargs < 9)
378       args[++nargs] = slice(argBegin, p);
379     return args[0..nargs+1];
380   }
381 
382   /// Expands "&#36;+", "&#36;0" - "&#36;9" with args[n] in text.
383   /// Params:
384   ///   text = The text to scan for argument placeholders.
385   ///   args = The first element, args[0], is the whole argument string and
386   ///          the following elements are slices into it.$(BR)
387   ///          The array is empty if there are no arguments.
388   static cstring expandArguments(cstring text, cstring[] args)
389   in { assert(args.length != 1, "zero or more than 1 args expected"); }
390   body
391   {
392     CharArray buffer;
393     auto p = text.ptr;
394     auto textEnd = p + text.length;
395     auto placeholderEnd = p;
396 
397     while (p+1 < textEnd)
398     {
399       if (*p == '$' && (*++p == '+' || isdigit(*p)))
400       {
401         // Copy string between argument placeholders.
402         if (placeholderEnd != p-1)
403           buffer ~= slice(placeholderEnd, p-1);
404         placeholderEnd = p+1; // Set new placeholder end.
405 
406         if (args.length == 0)
407           continue;
408 
409         if (*p == '+')
410         { // $+ = $2 to $n
411           if (args.length > 2)
412           {
413             assert(String(args[2]).slices(args[0]),
414               Format("arg[2] ({}) is not a slice of arg[0] ({})",
415                 args[2], args[0]));
416             buffer ~= slice(args[2].ptr, String(args[0]).end);
417           }
418         }
419         else
420         { // 0 - 9
421           uint nthArg = *p - '0';
422           if (nthArg < args.length)
423             buffer ~= args[nthArg];
424           else // DMD uses args0 if nthArg is not available.
425             buffer ~= args[0];
426         }
427       }
428       p++;
429     }
430     if (placeholderEnd is text.ptr)
431       return text;
432     if (placeholderEnd < textEnd)
433       buffer ~= slice(placeholderEnd, textEnd);
434     return buffer[];
435   }
436 }