/*******************************************************************************
* Copyright (c) 2008, 2013 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Ary Borenszweig - initial API and implementation?
* Bruno Medeiros - refactoring and some bugfixes
*******************************************************************************/
package dtool.ddoc;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull;
import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility class to do ddoc macro replacing.
*/
public class DdocMacros {
private final static Map<String, String> defaultMacros;
static {
// See http://dlang.org/ddoc.html
Map<String, String> map = new HashMap<String, String>();
map.put("B", "<b>$0</b>");
map.put("I", "<i>$0</i>");
map.put("U", "<u>$0</u>");
map.put("P", "<p>$0</p>");
map.put("DL", "<dl>$0</dl>");
map.put("DT", "<dt>$0</dt>");
map.put("DD", "<dd>$0</dd>");
map.put("TABLE", "<table border=\"1\" cellpadding=\"4\">$0</table>");
map.put("TR", "<tr>$0</tr>");
map.put("TH", "<th>$0</th>");
map.put("TD", "<td>$0</td>");
map.put("OL", "<ol>$0</ol>");
map.put("UL", "<ul>$0</ul>");
map.put("LI", "<li>$0</li>");
map.put("BIG", "<big>$0</big>");
map.put("SMALL", "<small>$0</small>");
map.put("BR", "<br>");
map.put("LINK", "<a href=\"$0\" target=\"_blank\">$0</a>");
map.put("LINK2", "<a href=\"$1\" target=\"_blank\">$+</a>");
map.put("LPAREN", "(");
map.put("RPAREN", ")");
map.put("RED", "<font color=red>$0</font>");
map.put("BLUE", "<font color=blue>$0</font>");
map.put("GREEN", "<font color=green>$0</font>");
map.put("YELLOW", "<font color=yellow>$0</font>");
map.put("BLACK", "<font color=black>$0</font>");
map.put("WHITE", "<font color=white>$0</font>");
// These two macros are the same, the "D" one is a newer version that was renamed.
map.put("D", "<span class=\"code\">$0</span>");
map.put("D_CODE", "<span class=\"code\">$0</span>");
// TODO ddoc macro provider
map.put("D_COMMENT", "<span class=\"java_single_line_comment\">$0</span>");
map.put("D_STRING", "<span class=\"java_string\">$0</span>");
map.put("D_KEYWORD", "<span class=\"java_keyword\">$0</span>");
map.put("D_PSYMBOL", "$(U $0)");
map.put("D_PARAM", "$(I $0)");
map.put("DDOC",
"<html><head>"+
"<META http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">"+
"<title>$(TITLE)</title>"+
// "<link rel="stylesheet" type="text/css" href="style.css">"
"</head><body>"+
"<h1>$(TITLE)</h1>"+
"$(BODY)"+
"</body></html>"
);
map.put("DDOC_COMMENT", "<!-- $0 -->");
map.put("DDOC_DECL", "$(DT $(BIG $0))");
map.put("DDOC_DECL_DD", "$(DD $0)");
map.put("DDOC_DITTO", "$(BR) $0");
map.put("DDOC_SECTIONS", "$0");
map.put("DDOC_SUMMARY", "$0$(BR)$(BR)");
map.put("DDOC_DESCRIPTION", "$0$(BR)$(BR)");
map.put("DDOC_AUTHORS", "$(B Authors:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_BUGS", "$(RED BUGS:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_COPYRIGHT", "$(B Copyright:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_DATE", "$(B Date:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_DEPRECATED", "$(RED Deprecated:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_EXAMPLES", "$(B Examples:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_HISTORY", "$(B History:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_LICENSE", "$(B License:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_RETURNS", "$(B Returns:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_SEE_ALSO", "$(B See Also:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_STANDARDS", "$(B Standards:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_THROWS", "$(B Throws:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_VERSION", "$(B Version:)$(BR) $0$(BR)$(BR)");
map.put("DDOC_SECTION_H", "$(B $0)$(BR)$(BR)");
map.put("DDOC_SECTION", "$0$(BR)$(BR)");
map.put("DDOC_MEMBERS", "$(DL $0)");
map.put("DDOC_MODULE_MEMBERS", "$(DDOC_MEMBERS $0)");
map.put("DDOC_CLASS_MEMBERS", "$(DDOC_MEMBERS $0)");
map.put("DDOC_STRUCT_MEMBERS", "$(DDOC_MEMBERS $0)");
map.put("DDOC_ENUM_MEMBERS", "$(DDOC_MEMBERS $0)");
map.put("DDOC_TEMPLATE_MEMBERS", "$(DDOC_MEMBERS $0)");
map.put("DDOC_PARAMS", "$(B Params:)$(BR)\n$(TABLE $0)$(BR)");
map.put("DDOC_PARAM_ROW", "$(TR $0)");
map.put("DDOC_PARAM_ID", "$(TD $0)");
map.put("DDOC_PARAM_DESC", "$(TD $0)");
map.put("DDOC_BLANKLINE", "$(BR)$(BR)");
map.put("DDOC_ANCHOR", "<a name=\"$1\"></a>");
map.put("DDOC_PSYMBOL", "$(U $0)");
map.put("DDOC_KEYWORD", "$(B $0)");
map.put("DDOC_PARAM", "$(I $0)");
defaultMacros = Collections.unmodifiableMap(map);
}
/**
* Returns a map of the default macros. The key is the macro name,
* the value is the replacement. This map in unmodifiable.
*/
public static Map<String, String> getDefaultMacros() {
return defaultMacros;
}
/**
* Replaces the macros found in the given string with the given macros
* map.
* @param source the string to replace
* @param macros the macros map
* @return the replaced string
*/
public static String replaceMacros(String source, Map<String, String> macros) {
TreeSet<String> expandedMacros = new TreeSet<String>();
return replaceMacros(source, macros, expandedMacros);
}
public static String replaceMacros(String source, Map<String, String> macroDefinitions,
Set<String> expandedMacros) {
DdocMacros ddocMacroProcessor = new DdocMacros(source, 0, macroDefinitions);
return ddocMacroProcessor.replaceMacros(expandedMacros);
}
protected String source;
protected int position;
protected Map<String, String> macroDefinitions;
public DdocMacros(String source, int position, Map<String, String> macroDefinitions) {
this.source = source;
this.position = position;
this.macroDefinitions = macroDefinitions;
}
/** Gets the character from absolute position index, or EOF if index exceeds source.length. */
public static int getCharacter(String source, int index) {
if(index >= source.length()) {
return -1;
}
return source.charAt(index);
}
public final int lookAhead(int offset) {
return getCharacter(source, position + offset);
}
public final int lookAhead() {
return getCharacter(source, position);
}
public final char lookAheadChar() {
int character = getCharacter(source, position);
assertTrue(character != -1);
return (char) character;
}
/**
* @param expandedMacros the expanded macros so far. Used for cycle detection.
* @return
*/
private String replaceMacros(Set<String> expandedMacros) {
// Total string
StringBuilder sb = new StringBuilder();
int length = source.length();
for(; position < length; position++) {
char ch = source.charAt(position);
if (ch != '$') {
sb.append(ch);
continue;
}
if(lookAhead(1) != '(') {
sb.append(ch);
continue;
}
String result = assertNotNull(evaluateMacro(expandedMacros));
sb.append(result);
}
return sb.toString();
}
private String evaluateMacro(Set<String> expandedMacros) {
int length = source.length();
// In case a macro is started but not finished
StringBuilder rawSource = new StringBuilder();
rawSource.append("$(");
position += 2;
if (position == length) {
return rawSource.toString();
}
char ch = source.charAt(position);
// The current argument in the macro
StringBuilder currentArgument = new StringBuilder();
// Argument $0
StringBuilder $0 = new StringBuilder();
// Argument $+
StringBuilder $plus = new StringBuilder();
List<String> arguments = new ArrayList<String>();
boolean foundSpace = false;
boolean foundComma = false;
int parensCount = 0;
for(; position < length; position++) {
ch = source.charAt(position);
if (ch == '$' && lookAhead(1) == '(') {
String result = evaluateMacro(expandedMacros);
assertNotNull(result);
currentArgument.append(result);
rawSource.append(result);
if (foundSpace) {
$0.append(result);
}
if (foundComma) {
$plus.append(result);
}
continue;
} else if (ch == ' ' && !foundSpace) {
foundSpace = true;
arguments.add(currentArgument.toString());
currentArgument.setLength(0);
rawSource.append(ch);
continue;
} else if (ch == ')' && parensCount == 0) {
arguments.add(currentArgument.toString());
String macroName = arguments.get(0);
String replacement = macroDefinitions.get(macroName);
if (replacement == null) {
rawSource.append(ch);
// If macro not found, return raw source
return rawSource.toString();
}
// Recursive step: replace macros in replacement
if (expandedMacros.contains(macroName)) {
return cycleErrorString(macroName);
} else {
expandedMacros.add(macroName);
replacement = replaceMacros(replacement, macroDefinitions, expandedMacros);
expandedMacros.remove(macroName);
replacement = replaceParameters(replacement, arguments, $0.toString(), $plus.toString());
return replacement;
}
} else if (ch == ',') {
if (foundComma) {
$plus.append(ch);
}
foundComma = true;
arguments.add(currentArgument.toString());
currentArgument.setLength(0);
$0.append(ch);
continue;
} else if (ch == '(') {
parensCount++;
} else if (ch == ')') {
parensCount--;
}
currentArgument.append(ch);
rawSource.append(ch);
if (foundSpace) {
$0.append(ch);
}
if (foundComma) {
$plus.append(ch);
}
}
return rawSource.toString();
}
public static String cycleErrorString(String macroName) {
return "$DDOC ERROR - CYCLE DETECTED WITH MACRO: "+macroName+" $";
}
private static String replaceParameters(String string, List<String> arguments, String $0, String $plus) {
StringBuilder sb = new StringBuilder();
int length = string.length();
for(int i = 0; i < length; i++) {
char c = string.charAt(i);
if (c == '$' && i < length - 1) {
i++;
c = string.charAt(i);
if ('0' <= c && c <= '9') {
int index = c - '0';
if (index == 0) {
sb.append($0);
} else if (1 <= index && index < arguments.size()) {
sb.append(arguments.get(index));
} else {
// Default behaviour of DMD
sb.append($0);
}
} else if (c == '+') {
sb.append($plus);
} else {
sb.append('$');
sb.append(c);
}
} else {
sb.append(c);
}
}
return sb.toString();
}
}