Stephen McKamey avatar Stephen McKamey committed a31694d

adding hooks for intercepting/transforming link URLs

Comments (0)

Files changed (5)

duel/duel-compiler/src/main/java/org/duelengine/duel/ast/ElementNode.java

 public class ElementNode extends ContainerNode {
 
 	private static final String CONFIG_RESOURCE = "org.duelengine.duel.ast.HTMLTags";
-	private static Map<String, Boolean> voidTags;
+	private static final Map<String, Boolean> voidTags;
+	private static final Map<String, Boolean> linkTags;
+	private static final Map<String, Boolean> linkAttrs;
 
 	private final String tagName;
 	private final boolean isVoid;
+	private final boolean isLinkableTag;
 	private final Map<String, DuelNode> attributes = new LinkedHashMap<String, DuelNode>();
 
+	static {
+		// definitions maintained in HTMLTags.properties
+		ResourceBundle config = ResourceBundle.getBundle(CONFIG_RESOURCE);
+
+		String[] items = (config != null) && config.containsKey("voidTags") ?
+			config.getString("voidTags").split("\\s+") : new String[0];
+
+		Map<String, Boolean> map = new HashMap<String, Boolean>(items.length);
+		for (String value : items) {
+			map.put(value, true);
+		}
+
+		voidTags = map;
+
+		items = (config != null) && config.containsKey("linkTags") ?
+			config.getString("linkTags").split("\\s+") : new String[0];
+
+		map = new HashMap<String, Boolean>(items.length);
+		for (String value : items) {
+			map.put(value, true);
+		}
+
+		linkTags = map;
+
+		items = (config != null) && config.containsKey("linkAttrs") ?
+			config.getString("linkAttrs").split("\\s+") : new String[0];
+
+		map = new HashMap<String, Boolean>(items.length);
+		for (String value : items) {
+			map.put(value, true);
+		}
+
+		linkAttrs = map;
+	}
+
 	public ElementNode(String name, int index, int line, int column) {
 		super(index, line, column);
 
 		this.tagName = name;
-		this.isVoid = (name == null) || getVoidTags().containsKey(name);
+		this.isVoid = (name == null) || voidTags.containsKey(name);
+		this.isLinkableTag = (name != null) && linkTags.containsKey(name);
 	}
 
 	public ElementNode(String name) {
 		this.tagName = name;
-		this.isVoid = (name == null) || getVoidTags().containsKey(name);
+		this.isVoid = (name == null) || voidTags.containsKey(name);
+		this.isLinkableTag = (name != null) && linkTags.containsKey(name);
 	}
 
 	public ElementNode(String name, AttributePair[] attr, DuelNode... children) {
 		super(children);
 
 		this.tagName = name;
-		this.isVoid = (name == null) ? true : getVoidTags().containsKey(name);
+		this.isVoid = (name == null) || voidTags.containsKey(name);
+		this.isLinkableTag = (name != null) && linkTags.containsKey(name);
 
 		if (attr != null) {
 			for (AttributePair a : attr) {
 		return !this.isVoid;
 	}
 
+	public boolean isLinkAttribute(String name) {
+		return this.isLinkableTag && linkAttrs.containsKey(name);
+	}
+
 	public boolean hasAttributes() {
 		return !this.attributes.isEmpty();
 	}
 		return this.isSelf(tag) || this.isAncestor(tag);
 	}
 
-	private static Map<String, Boolean> getVoidTags() {
-
-		if (voidTags != null) {
-			return voidTags;
-		}
-
-		// definitions maintained in HTMLTags.properties
-		ResourceBundle config = ResourceBundle.getBundle(CONFIG_RESOURCE);
-
-		String[] tags = (config != null) && config.containsKey("voidTags") ?
-			config.getString("voidTags").split(",") : new String[0];
-
-		Map<String, Boolean> map = new HashMap<String, Boolean>(tags.length);
-		for (String value : tags) {
-			map.put(value, true);
-		}
-
-		return (voidTags = map);
-	}
-
 	@Override
 	StringBuilder toString(StringBuilder buffer) {
 		buffer.append("<").append(this.tagName);

duel/duel-compiler/src/main/java/org/duelengine/duel/codegen/CodeDOMBuilder.java

 	private void buildNode(DuelNode node) throws IOException {
 		if (node instanceof LiteralNode) {
 			String literal = ((LiteralNode)node).getValue();
-			if (this.tagMode == TagMode.SuspendMode) {
+			if (this.tagMode == TagMode.SuspendMode || node instanceof UnknownNode) {
 				this.buffer.append(literal);
 
 			} else {
 		Map<String, DataEncoder.Snippet> deferredAttrs = new LinkedHashMap<String, DataEncoder.Snippet>();
 		for (String attrName : element.getAttributeNames()) {
 			DuelNode attrVal = element.getAttribute(attrName);
-
+			
 			if (attrVal == null) {
 				this.formatter.writeAttribute(this.buffer, attrName, null);
 
+			} else if (element.isLinkAttribute(attrName)) {
+				this.formatter.writeOpenAttribute(this.buffer, attrName);
+				this.flushBuffer();
+
+				CodeStatement writeStatement;
+				if (attrVal instanceof LiteralNode) {
+					writeStatement = this.buildLinkIntercept(((LiteralNode)attrVal).getValue());
+
+				} else if (attrVal instanceof CodeBlockNode) {
+					try {
+						writeStatement = this.buildLinkIntercept((CodeBlockNode)attrVal);
+
+					} catch (Exception ex) {
+						
+						// only defer attributes that cannot be processed server-side
+						deferredAttrs.put(attrName, DataEncoder.asSnippet(((CodeBlockNode)attrVal).getClientCode()));
+						argSize = Math.max(argSize, ((CodeBlockNode)attrVal).getArgSize());
+						continue;
+					}
+
+				} else {
+					throw new InvalidNodeException("Invalid attribute node type: "+attrVal.getClass(), attrVal);
+				}
+
+				this.scopeStack.peek().add(writeStatement);
+
+				this.formatter.writeCloseAttribute(this.buffer);
+
 			} else if (attrVal instanceof LiteralNode) {
 				this.formatter.writeAttribute(this.buffer, attrName, ((LiteralNode)attrVal).getValue());
 
 					// only defer attributes that cannot be processed server-side
 					deferredAttrs.put(attrName, DataEncoder.asSnippet(((CodeBlockNode)attrVal).getClientCode()));
 					argSize = Math.max(argSize, ((CodeBlockNode)attrVal).getArgSize());
+					continue;
 				}
 
 			} else {
 	}
 
 	/**
+	 * @param literal
+	 * @return Code which emits the evaluated value of a link intercept
+	 */
+	private CodeStatement buildLinkIntercept(Object literal) {
+		CodeExpression codeExpr = new CodeMethodInvokeExpression(
+			String.class,
+			new CodeThisReferenceExpression(),
+			"interceptLink",
+			new CodeVariableReferenceExpression(DuelContext.class, "context"),
+			CodeDOMUtility.ensureString(new CodePrimitiveExpression(literal)));
+
+		return CodeDOMUtility.emitExpressionSafe(codeExpr);
+	}
+
+	/**
+	 * @param block
+	 * @return Code which emits the evaluated value of a link intercept
+	 */
+	private CodeStatement buildLinkIntercept(CodeBlockNode block) {
+		boolean htmlEncode = true;
+		if (block instanceof MarkupExpressionNode) {
+			htmlEncode = false;
+			block = new ExpressionNode(block.getValue(), block.getIndex(), block.getLine(), block.getColumn());
+		}
+
+		CodeExpression codeExpr = this.translateExpression(block, true);
+		if (codeExpr == null) {
+			return null;
+		}
+
+		codeExpr = new CodeMethodInvokeExpression(
+			String.class,
+			new CodeThisReferenceExpression(),
+			"interceptLink",
+			new CodeVariableReferenceExpression(DuelContext.class, "context"),
+			CodeDOMUtility.ensureString(codeExpr));
+
+		return htmlEncode ?
+			CodeDOMUtility.emitExpressionSafe(codeExpr) :
+			CodeDOMUtility.emitExpression(codeExpr);
+	}
+
+	/**
 	 * @param block
 	 * @return Code which emits the evaluated value of a code block
 	 */

duel/duel-compiler/src/main/resources/org/duelengine/duel/ast/HTMLTags.properties

 # http://www.w3.org/TR/xhtml-modularization/abstract_modules.html#sec_5.2.
 # http://www.w3.org/TR/WD-html40-970917/index/elements.html
 # NOTE: "$if" is an element from duel grammar
-voidTags=$if,area,base,basefont,br,col,frame,hr,img,input,isindex,keygen,link,meta,param,source,wbr
+voidTags=$if area base basefont br col frame hr img input isindex keygen link meta param source wbr
 
 # http://www.w3.org/TR/html5/semantics.html
 # http://www.w3.org/TR/html401/index/elements.html
 # http://www.w3.org/TR/WD-html40-970917/index/elements.html
 # NOTE: this is not yet used
-optionalClose=body,colgroup,dd,dt,embed,head,html,li,option,p,tbody,td,tfoot,th,thead,tr
+optionalClose=body colgroup dd dt embed head html li option p tbody td tfoot th thead tr
+
+# elements which have an attribute containing a URL
+linkTags=a link base area script iframe img audio video track source embed form
+
+# attribute names which contain a URL
+linkAttrs=href src action formaction poster lowsrc

duel/duel-runtime/src/main/java/org/duelengine/duel/DuelContext.java

 
 		return this.clientID.nextID();
 	}
+
+	String interceptLink(String url) {
+		// TODO: build mechanism for exposing URL transformation
+		return url;
+	}
 }

duel/duel-runtime/src/main/java/org/duelengine/duel/DuelView.java

 
 		context.getOutput().append(value);
 	}
-
+	
 	/**
 	 * Ensures the value is properly encoded as HTML text
 	 * @param context
 			output.append("\n\t");
 		}
 	}
-
+	
 	protected String nextID(DuelContext context) {
 		return context.nextID();
 	}
 
 	/**
+	 * Allows view to intercept and transform URLs within element attributes
+	 * @param url URL
+	 * @return transformed URL
+	 */
+	protected String interceptLink(DuelContext context, String url) {
+		return context.interceptLink(url);
+	}
+
+	/**
 	 * Retrieves the value of a property from the data object
 	 * @param data
 	 * @return
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.