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 "$+", "$0" - "$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 }