From 430496317177893eeb94579b2946dbafea6d0727 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 19 Jun 2013 16:26:58 -0400
Subject: [PATCH] Generate filterable project/repository list with FreeMarker

---
 src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java                  |  308 +++++++++++++++++++
 NOTICE                                                                            |   10 
 src/main/java/com/gitblit/wicket/freemarker/Freemarker.java                       |   46 ++
 src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html                       |   83 -----
 .classpath                                                                        |    1 
 src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java             |  154 +++++++++
 src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html                |   10 
 src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html             |   10 
 src/main/java/com/gitblit/wicket/pages/ProjectPage.html                           |   22 -
 src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm    |   15 
 src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java                       |   85 ----
 src/site/design.mkd                                                               |    1 
 src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm |   19 +
 build.moxie                                                                       |    1 
 src/main/java/com/gitblit/wicket/pages/ProjectPage.java                           |    7 
 src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java                |  139 ++++++++
 gitblit.iml                                                                       |   11 
 17 files changed, 747 insertions(+), 175 deletions(-)

diff --git a/.classpath b/.classpath
index e25f68c..8cd68de 100644
--- a/.classpath
+++ b/.classpath
@@ -40,6 +40,7 @@
 	<classpathentry kind="lib" path="ext/force-partner-api-24.0.0.jar" sourcepath="ext/src/force-partner-api-24.0.0.jar" />
 	<classpathentry kind="lib" path="ext/force-wsc-24.0.0.jar" sourcepath="ext/src/force-wsc-24.0.0.jar" />
 	<classpathentry kind="lib" path="ext/js-1.7R2.jar" sourcepath="ext/src/js-1.7R2.jar" />
+	<classpathentry kind="lib" path="ext/freemarker-2.3.19.jar" sourcepath="ext/src/freemarker-2.3.19.jar" />
 	<classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
 	<classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
 	<classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
diff --git a/NOTICE b/NOTICE
index 0e23d53..ab0a086 100644
--- a/NOTICE
+++ b/NOTICE
@@ -269,4 +269,12 @@
    AngularJS, release under the
    MIT License.
    
-   http://angularjs.org/   
\ No newline at end of file
+   http://angularjs.org/   
+   
+---------------------------------------------------------------------------
+FreeMarker
+---------------------------------------------------------------------------
+   FreeMarker, release under a
+   modified BSD License. (http://www.freemarker.org/docs/app_license.html)
+   
+   http://www.freemarker.org/
\ No newline at end of file
diff --git a/build.moxie b/build.moxie
index be9a21c..9fc08dc 100644
--- a/build.moxie
+++ b/build.moxie
@@ -148,6 +148,7 @@
 - compile 'com.toedter:jcalendar:1.3.2' :authority
 - compile 'org.apache.commons:commons-compress:1.4.1' :war
 - compile 'com.force.api:force-partner-api:24.0.0' :war
+- compile 'org.freemarker:freemarker:2.3.19' :war
 - test 'junit'
 # Dependencies for Selenium web page testing
 - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/gitblit.iml b/gitblit.iml
index b90adbd..38a014a 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -413,6 +413,17 @@
         </SOURCES>
       </library>
     </orderEntry>
+    <orderEntry type="module-library">
+      <library name="freemarker-2.3.19.jar">
+        <CLASSES>
+          <root url="jar://$MODULE_DIR$/ext/freemarker-2.3.19.jar!/" />
+        </CLASSES>
+        <JAVADOC />
+        <SOURCES>
+          <root url="jar://$MODULE_DIR$/ext/src/freemarker-2.3.19.jar!/" />
+        </SOURCES>
+      </library>
+    </orderEntry>
     <orderEntry type="module-library" scope="TEST">
       <library name="junit-4.11.jar">
         <CLASSES>
diff --git a/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java b/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java
new file mode 100644
index 0000000..ad7aa96
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/freemarker/Freemarker.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.freemarker;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Map;
+
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+public class Freemarker {
+
+	private static final Configuration fm;
+	
+	static {
+		fm = new Configuration();
+		fm.setObjectWrapper(new DefaultObjectWrapper());
+		fm.setOutputEncoding("UTF-8");
+		fm.setClassForTemplateLoading(Freemarker.class, "templates");
+	}
+	
+	public static Template getTemplate(String name) throws IOException {
+		return fm.getTemplate(name);
+	}
+	
+	public static void evaluate(Template template, Map<String, Object> values, Writer out) throws TemplateException, IOException {
+		template.process(values, out);
+	}
+
+}
diff --git a/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java b/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java
new file mode 100644
index 0000000..d57c3a0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/freemarker/FreemarkerPanel.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.freemarker;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Map;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.IMarkupCacheKeyProvider;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.string.Strings;
+
+import com.gitblit.utils.StringUtils;
+
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+
+/**
+ * This class allows FreeMarker to be used as a Wicket preprocessor or as a
+ * snippet injector for something like a CMS.  There are some cases where Wicket
+ * is not flexible enough to generate content, especially when you need to generate
+ * hybrid HTML/JS content outside the scope of Wicket.
+ * 
+ * @author James Moger
+ * 
+ */
+@SuppressWarnings("unchecked")
+public class FreemarkerPanel extends Panel
+		implements
+			IMarkupResourceStreamProvider,
+			IMarkupCacheKeyProvider
+{
+	private static final long serialVersionUID = 1L;
+
+	private final String template;
+	private boolean parseGeneratedMarkup;
+	private boolean escapeHtml;
+	private boolean throwFreemarkerExceptions;
+	private transient String stackTraceAsString;
+	private transient String evaluatedTemplate;
+
+	
+	/**
+	 * Construct.
+	 * 
+	 * @param id
+	 *            Component id
+	 * @param template
+	 *            The Freemarker template
+	 * @param values
+	 *            values map that can be substituted by Freemarker.
+	 */
+	public FreemarkerPanel(final String id, String template, final Map<String, Object> values)
+	{
+		this(id, template, Model.ofMap(values));
+	}
+	
+	/**
+	 * Construct.
+	 * 
+	 * @param id
+	 *            Component id
+	 * @param templateResource
+	 *            The Freemarker template as a string resource
+	 * @param model
+	 *            Model with variables that can be substituted by Freemarker.
+	 */
+	public FreemarkerPanel(final String id, final String template, final IModel< ? extends Map<String, Object>> model)
+	{
+		super(id, model);
+		this.template = template;
+	}
+
+	/**
+	 * Gets the Freemarker template.
+	 * 
+	 * @return the Freemarker template
+	 */
+	private Template getTemplate()
+	{
+		if (StringUtils.isEmpty(template))
+		{
+			throw new IllegalArgumentException("Template not specified!");
+		}
+
+		try {
+			return Freemarker.getTemplate(template);
+		} catch (IOException e) {
+			onException(e);
+		}
+
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.markup.html.panel.Panel#onComponentTagBody(org.apache.wicket.markup.
+	 *      MarkupStream, org.apache.wicket.markup.ComponentTag)
+	 */
+	@Override
+	protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag)
+	{
+		if (!Strings.isEmpty(stackTraceAsString))
+		{
+			// TODO: only display the Freemarker error/stacktrace in development
+			// mode?
+			replaceComponentTagBody(markupStream, openTag, Strings
+					.toMultilineMarkup(stackTraceAsString));
+		}
+		else if (!parseGeneratedMarkup)
+		{
+			// check that no components have been added in case the generated
+			// markup should not be
+			// parsed
+			if (size() > 0)
+			{
+				throw new WicketRuntimeException(
+						"Components cannot be added if the generated markup should not be parsed.");
+			}
+
+			if (evaluatedTemplate == null)
+			{
+				// initialize evaluatedTemplate
+				getMarkupResourceStream(null, null);
+			}
+			replaceComponentTagBody(markupStream, openTag, evaluatedTemplate);
+		}
+		else
+		{
+			super.onComponentTagBody(markupStream, openTag);
+		}
+	}
+
+	/**
+	 * Either print or rethrow the throwable.
+	 * 
+	 * @param exception
+	 *            the cause
+	 * @param markupStream
+	 *            the markup stream
+	 * @param openTag
+	 *            the open tag
+	 */
+	private void onException(final Exception exception)
+	{
+		if (!throwFreemarkerExceptions)
+		{
+			// print the exception on the panel
+			stackTraceAsString = Strings.toString(exception);
+		}
+		else
+		{
+			// rethrow the exception
+			throw new WicketRuntimeException(exception);
+		}
+	}
+
+	/**
+	 * Gets whether to escape HTML characters.
+	 * 
+	 * @return whether to escape HTML characters. The default value is false.
+	 */
+	public void setEscapeHtml(boolean value)
+	{
+		this.escapeHtml = value;
+	}
+
+	/**
+	 * Evaluates the template and returns the result.
+	 * 
+	 * @param templateReader
+	 *            used to read the template
+	 * @return the result of evaluating the velocity template
+	 */
+	private String evaluateFreemarkerTemplate(Template template)
+	{
+		if (evaluatedTemplate == null)
+		{
+			// Get model as a map
+			final Map<String, Object> map = (Map<String, Object>)getDefaultModelObject();
+
+			// create a writer for capturing the Velocity output
+			StringWriter writer = new StringWriter();
+
+			// string to be used as the template name for log messages in case
+			// of error
+			try
+			{
+				// execute the Freemarker script and capture the output in writer
+				Freemarker.evaluate(template, map, writer);
+
+				// replace the tag's body the Freemarker output
+				evaluatedTemplate = writer.toString();
+
+				if (escapeHtml)
+				{
+					// encode the result in order to get valid html output that
+					// does not break the rest of the page
+					evaluatedTemplate = Strings.escapeMarkup(evaluatedTemplate).toString();
+				}
+				return evaluatedTemplate;
+			}
+			catch (IOException e)
+			{
+				onException(e);
+			}
+			catch (TemplateException e)
+			{
+				onException(e);
+			}
+			return null;
+		}
+		return evaluatedTemplate;
+	}
+
+	/**
+	 * Gets whether to parse the resulting Wicket markup.
+	 * 
+	 * @return whether to parse the resulting Wicket markup. The default is false.
+	 */
+	public void setParseGeneratedMarkup(boolean value)
+	{
+		this.parseGeneratedMarkup = value;
+	}
+
+	/**
+	 * Whether any Freemarker exception should be trapped and displayed on the panel (false) or thrown
+	 * up to be handled by the exception mechanism of Wicket (true). The default is false, which
+	 * traps and displays any exception without having consequences for the other components on the
+	 * page.
+	 * <p>
+	 * Trapping these exceptions without disturbing the other components is especially useful in CMS
+	 * like applications, where 'normal' users are allowed to do basic scripting. On errors, you
+	 * want them to be able to have them correct them while the rest of the application keeps on
+	 * working.
+	 * </p>
+	 * 
+	 * @return Whether any Freemarker exceptions should be thrown or trapped. The default is false.
+	 */
+	public void setThrowFreemarkerExceptions(boolean value)
+	{
+		this.throwFreemarkerExceptions = value;
+	}
+
+	/**
+	 * @see org.apache.wicket.markup.IMarkupResourceStreamProvider#getMarkupResourceStream(org.apache
+	 *      .wicket.MarkupContainer, java.lang.Class)
+	 */
+	public final IResourceStream getMarkupResourceStream(MarkupContainer container,
+			Class< ? > containerClass)
+	{
+		Template template = getTemplate();
+		if (template == null)
+		{
+			throw new WicketRuntimeException("could not find Freemarker template for panel: " + this);
+		}
+
+		// evaluate the template and return a new StringResourceStream
+		StringBuffer sb = new StringBuffer();
+		sb.append("<wicket:panel>");
+		sb.append(evaluateFreemarkerTemplate(template));
+		sb.append("</wicket:panel>");
+		return new StringResourceStream(sb.toString());
+	}
+
+	/**
+	 * @see org.apache.wicket.markup.IMarkupCacheKeyProvider#getCacheKey(org.apache.wicket.
+	 *      MarkupContainer, java.lang.Class)
+	 */
+	public final String getCacheKey(MarkupContainer container, Class< ? > containerClass)
+	{
+		// don't cache the evaluated template
+		return null;
+	}
+
+	/**
+	 * @see org.apache.wicket.Component#onDetach()
+	 */
+	@Override
+	protected void onDetach()
+	{
+		super.onDetach();
+		stackTraceAsString = null;
+		evaluatedTemplate = null;
+	}
+}
diff --git a/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm
new file mode 100644
index 0000000..691f089
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableProjectList.fm
@@ -0,0 +1,15 @@
+<div ng-controller="${ngCtrl}" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
+	<div class="header" style="padding: 5px;border: none;"><i class="icon-folder-close"></i> <span wicket:id="${ngList}Title"></span>
+		<div style="padding: 5px 0px 0px;">
+			<input type="text" ng-model="query.n" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
+		</div>
+	</div>
+		
+	<div ng-repeat="item in ${ngList} | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
+		<a href="project/{{item.p}}" title="{{item.i}}"><b>{{item.n}}</b></a>
+		<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
+		<span class="pull-right">
+			<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;" wicket:message="title:gb.repositories">{{item.c | number}}</span>
+		</span>
+	</div>
+</div>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm
new file mode 100644
index 0000000..cf1b7a8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/freemarker/templates/FilterableRepositoryList.fm
@@ -0,0 +1,19 @@
+<div ng-controller="${ngCtrl}" style="border: 1px solid #ddd;border-radius: 4px;">
+	<div class="header" style="padding: 5px;border: none;"><i wicket:id="${ngList}Icon"></i> <span wicket:id="${ngList}Title"></span>
+		<div class="hidden-phone pull-right">
+			<span wicket:id="${ngList}Button"></span>
+		</div>
+		<div style="padding: 5px 0px 0px;">
+			<input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
+		</div>
+	</div>
+	
+	<div ng-repeat="item in ${ngList} | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
+		<b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
+		<a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
+		<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
+		<span ng-show="item.s" class="pull-right">
+			<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
+		</span>
+	</div>		
+</div>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html
index a9fd1ba..ce1f028 100644
--- a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.html
@@ -29,7 +29,7 @@
 			<div wicket:id="active">[recently active]</div>
 		</div>
 		<div class="tab-pane" id="projects">
-			<div wicket:id="projectList">[all projects]</div>
+			<div wicket:id="projects">[all projects]</div>
 		</div>
 	</div>
 </wicket:fragment>
@@ -52,7 +52,7 @@
 			<div wicket:id="active">[recently active]</div>
 		</div>
 		<div class="tab-pane" id="projects">
-			<div wicket:id="projectList">[all projects]</div>
+			<div wicket:id="projects">[all projects]</div>
 		</div>
 	</div>
 </wicket:fragment>
@@ -72,85 +72,6 @@
 			<td><div id="chartAuthors" style="display:inline-block;width: 175px; height: 175px;"></div></td>
 		</tr>
 	</table>
-</wicket:fragment>
-
-<wicket:fragment wicket:id="starredListFragment">
-	<div ng-controller="starredCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
-		<div class="header" style="padding: 5px;border: none;"><i class="icon-star"></i> <wicket:message key="gb.starredRepositories"></wicket:message> ({{starred.length}})
-			<div style="padding: 5px 0px 0px;">
-				<input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
-			</div>
-		</div>
-		
-		<div ng-repeat="item in starred | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
-			<b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
-			<a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
-			<span class="link hidden-tablet hidden-phone" style="color: #aaa;" title="{{item.d}}">{{item.t}}</span>
-			<span ng-show="item.s" class="pull-right">
-				<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
-			</span>
-		</div>
-		
-	</div>
-</wicket:fragment>
-
-<wicket:fragment wicket:id="ownedListFragment">
-	<div ng-controller="ownedCtrl" style="border: 1px solid #ddd;border-radius: 4px;">
-		<div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.myRepositories"></wicket:message> ({{owned.length}})
-			<div class="hidden-phone pull-right">
-				<span wicket:id="create"></span>
-			</div>
-			<div style="padding: 5px 0px 0px;">
-				<input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
-			</div>
-		</div>
-		
-		<div ng-repeat="item in owned | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
-			<b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
-			<a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
-			<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
-			<span ng-show="item.s" class="pull-right">
-				<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
-			</span>
-		</div>		
-	</div>
-</wicket:fragment>
-
-<wicket:fragment wicket:id="activeListFragment">
-	<div ng-controller="activeCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
-		<div class="header" style="padding: 5px;border: none;"><i class="icon-user"></i> <wicket:message key="gb.activeRepositories"></wicket:message> ({{active.length}})
-			<div style="padding: 5px 0px 0px;">
-				<input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
-			</div>
-		</div>
-		
-		<div ng-repeat="item in active | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
-			<b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
-			<a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
-			<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
-			<span ng-show="item.s" class="pull-right">
-				<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
-			</span>
-		</div>		
-	</div>
-</wicket:fragment>
-
-<wicket:fragment wicket:id="projectListFragment">
-	<div ng-controller="projectListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
-		<div class="header" style="padding: 5px;border: none;"><i class="icon-folder-close"></i> <wicket:message key="gb.projects"></wicket:message> ({{projectList.length}})
-			<div style="padding: 5px 0px 0px;">
-				<input type="text" ng-model="query.n" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
-			</div>
-		</div>
-		
-		<div ng-repeat="item in projectList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
-			<a href="project/{{item.p}}" title="{{item.i}}"><b>{{item.n}}</b></a>
-			<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
-			<span class="pull-right">
-				<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;" wicket:message="title:gb.repositories">{{item.c | number}}</span>
-			</span>
-		</div>
-	</div>
 </wicket:fragment>
 
 </wicket:extend>
diff --git a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java
index 858821d..f6f9685 100644
--- a/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/MyDashboardPage.java
@@ -19,10 +19,7 @@
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.Serializable;
-import java.text.DateFormat;
 import java.text.MessageFormat;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collections;
@@ -34,7 +31,6 @@
 
 import org.apache.wicket.Component;
 import org.apache.wicket.PageParameters;
-import org.apache.wicket.behavior.HeaderContributor;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.panel.Fragment;
 import org.eclipse.jgit.lib.Constants;
@@ -49,8 +45,8 @@
 import com.gitblit.utils.StringUtils;
 import com.gitblit.wicket.GitBlitWebSession;
 import com.gitblit.wicket.WicketUtils;
-import com.gitblit.wicket.ng.NgController;
-import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.FilterableProjectList;
+import com.gitblit.wicket.panels.FilterableRepositoryList;
 
 public class MyDashboardPage extends DashboardPage {
 
@@ -152,38 +148,36 @@
 		
 		add(repositoryTabs);
 		
-		Fragment projectList = createProjectList();
-		repositoryTabs.add(projectList);
+		// projects list
+		List<ProjectModel> projects = GitBlit.self().getProjectModels(getRepositoryModels(), false);
+		repositoryTabs.add(new FilterableProjectList("projects", projects));
 		
 		// active repository list
 		if (active.isEmpty()) {
 			repositoryTabs.add(new Label("active").setVisible(false));
 		} else {
-			Fragment activeView = createNgList("active", "activeListFragment", "activeCtrl", active);
-			repositoryTabs.add(activeView);
+			FilterableRepositoryList repoList = new FilterableRepositoryList("active", active);
+			repoList.setTitle(getString("gb.activeRepositories"), "icon-time");
+			repositoryTabs.add(repoList);
 		}
 		
 		// starred repository list
 		if (ArrayUtils.isEmpty(starred)) {
 			repositoryTabs.add(new Label("starred").setVisible(false));
 		} else {
-			Fragment starredView = createNgList("starred", "starredListFragment", "starredCtrl", starred);
-			repositoryTabs.add(starredView);
+			FilterableRepositoryList repoList = new FilterableRepositoryList("starred", starred);
+			repoList.setTitle(getString("gb.starredRepositories"), "icon-star");
+			repositoryTabs.add(repoList);
 		}
 		
 		// owned repository list
 		if (ArrayUtils.isEmpty(owned)) {
 			repositoryTabs.add(new Label("owned").setVisible(false));
 		} else {
-			Fragment ownedView = createNgList("owned", "ownedListFragment", "ownedCtrl", owned);
-			if (user.canCreate) {
-				// create button
-				ownedView.add(new LinkPanel("create", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class));
-			} else {
-				// no button
-				ownedView.add(new Label("create").setVisible(false));
-			}
-			repositoryTabs.add(ownedView);
+			FilterableRepositoryList repoList = new FilterableRepositoryList("owned", starred);
+			repoList.setTitle(getString("gb.myRepositories"), "icon-user");
+			repoList.setAllowCreate(user.canCreate() || user.canAdmin());
+			repositoryTabs.add(repoList);
 		}
 	}
 	
@@ -258,54 +252,5 @@
 			}			
 		}
 		return MessageFormat.format(getString("gb.failedToReadMessage"), file);
-	}
-	
-	protected Fragment createProjectList() {
-		String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
-		final DateFormat df = new SimpleDateFormat(format);
-		df.setTimeZone(getTimeZone());
-		List<ProjectModel> projects = GitBlit.self().getProjectModels(getRepositoryModels(), false);
-		Collections.sort(projects, new Comparator<ProjectModel>() {
-			@Override
-			public int compare(ProjectModel o1, ProjectModel o2) {
-				return o2.lastChange.compareTo(o1.lastChange);
-			}
-		});
-
-		List<ProjectListItem> list = new ArrayList<ProjectListItem>();
-		for (ProjectModel proj : projects) {
-			if (proj.isUserProject() || proj.repositories.isEmpty()) {
-				// exclude user projects from list
-				continue;
-			}
-			ProjectListItem item = new ProjectListItem();
-			item.p = proj.name;
-			item.n = StringUtils.isEmpty(proj.title) ? proj.name : proj.title;
-			item.i = proj.description;
-			item.t = getTimeUtils().timeAgo(proj.lastChange);
-			item.d = df.format(proj.lastChange);
-			item.c = proj.repositories.size();
-			list.add(item);
-		}
-		
-		// inject an AngularJS controller with static data
-		NgController ctrl = new NgController("projectListCtrl");
-		ctrl.addVariable("projectList", list);
-		add(new HeaderContributor(ctrl));
-		
-		Fragment fragment = new Fragment("projectList", "projectListFragment", this);
-		return fragment;
-	}
-	
-	protected class ProjectListItem implements Serializable {
-
-		private static final long serialVersionUID = 1L;
-		
-		String p; // path
-		String n; // name
-		String t; // time ago
-		String d; // last updated
-		String i; // information/description
-		long c;   // repository count
 	}
 }
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.html b/src/main/java/com/gitblit/wicket/pages/ProjectPage.html
index 102a49e..32139d0 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectPage.html
@@ -51,25 +51,7 @@
 		</tr>
 	</table>
 </wicket:fragment>
-		
-<wicket:fragment wicket:id="repositoryListFragment">
-	<div ng-controller="repositoryListCtrl" style="border: 1px solid #ddd;border-radius: 4px;margin-bottom: 20px;">
-		<div class="header" style="padding: 5px;border: none;"><img style="vertical-align: middle;" src="git-black-16x16.png"/> <wicket:message key="gb.repositories"></wicket:message> ({{repositoryList.length}})
-			<div style="padding: 5px 0px 0px;">
-				<input type="text" ng-model="query.r" class="input-large" wicket:message="placeholder:gb.filter" style="border-radius: 14px; padding: 3px 14px;margin: 0px;"></input>
-			</div>
-		</div>
-		
-		<div ng-repeat="item in repositoryList | filter:query" style="padding: 3px;border-top: 1px solid #ddd;">
-			<b><span class="repositorySwatch" style="background-color:{{item.c}};"><span ng-show="item.wc">!</span><span ng-show="!item.wc">&nbsp;</span></span></b>
-			<a href="summary/?r={{item.r}}" title="{{item.i}}">{{item.p}}<b>{{item.n}}</b></a>
-			<span class="link hidden-tablet hidden-phone" style="color: #bbb;" title="{{item.d}}">{{item.t}}</span>
-			<span ng-show="item.s" class="pull-right">
-				<span style="padding: 0px 5px;color: #888;font-weight:bold;vertical-align:middle;">{{item.s | number}} <i style="vertical-align:baseline;" class="iconic-star"></i></span>
-			</span>
-		</div>		
-	</div>
-</wicket:fragment>		
-	</wicket:extend>
+
+</wicket:extend>
 </body>
 </html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
index b101b40..bfc8493 100644
--- a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -24,7 +24,6 @@
 import org.apache.wicket.PageParameters;
 import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.link.ExternalLink;
-import org.apache.wicket.markup.html.panel.Fragment;
 
 import com.gitblit.GitBlit;
 import com.gitblit.Keys;
@@ -41,6 +40,7 @@
 import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
 import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
 import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.FilterableRepositoryList;
 
 public class ProjectPage extends DashboardPage {
 	
@@ -128,8 +128,9 @@
 		if (repositories.isEmpty()) {
 			add(new Label("repositoryList").setVisible(false));
 		} else {
-			Fragment activeView = createNgList("repositoryList", "repositoryListFragment", "repositoryListCtrl", repositories);
-			add(activeView);
+			FilterableRepositoryList repoList = new FilterableRepositoryList("repositoryList", repositories);
+			repoList.setAllowCreate(user.canCreate(project.name + "/"));
+			add(repoList);
 		}
 	}
 	
diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html
new file mode 100644
index 0000000..4c3aecd
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<wicket:panel>
+	<div wicket:id="listComponent">[component]</div>
+</wicket:panel>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java
new file mode 100644
index 0000000..a5b7413
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FilterableProjectList.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.io.Serializable;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.freemarker.FreemarkerPanel;
+import com.gitblit.wicket.ng.NgController;
+
+/**
+ * A client-side filterable rich project list which uses Freemarker, Wicket,
+ * and AngularJS. 
+ * 
+ * @author James Moger
+ *
+ */
+public class FilterableProjectList extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final List<ProjectModel> projects;
+	
+	private String title;
+	
+	private String iconClass;
+	
+	public FilterableProjectList(String id, List<ProjectModel> projects) {
+		super(id);
+		this.projects = projects;
+	}
+	
+	public void setTitle(String title, String iconClass) {
+		this.title = title;
+		this.iconClass = iconClass;
+	}
+	
+	@Override
+	protected void onInitialize() {
+		super.onInitialize();
+
+		String id = getId();
+		String ngCtrl = id + "Ctrl";
+		String ngList = id + "List";
+		
+		Map<String, Object> values = new HashMap<String, Object>();
+		values.put("ngCtrl",  ngCtrl);
+		values.put("ngList",  ngList);
+		
+		// use Freemarker to setup an AngularJS/Wicket html snippet
+		FreemarkerPanel panel = new FreemarkerPanel("listComponent", "FilterableProjectList.fm", values);
+		panel.setParseGeneratedMarkup(true);
+		panel.setRenderBodyOnly(true);
+		add(panel);
+		
+		// add the Wicket controls that are referenced in the snippet 
+		String listTitle = StringUtils.isEmpty(title) ? getString("gb.projects") : title;
+		panel.add(new Label(ngList + "Title", MessageFormat.format("{0} ({1})", listTitle, projects.size())));
+		if (StringUtils.isEmpty(iconClass)) {
+			panel.add(new Label(ngList + "Icon").setVisible(false));
+		} else {
+			Label icon = new Label(ngList + "Icon");
+			WicketUtils.setCssClass(icon, iconClass);
+			panel.add(icon);
+		}
+		
+		String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
+		final DateFormat df = new SimpleDateFormat(format);
+		df.setTimeZone(getTimeZone());
+		Collections.sort(projects, new Comparator<ProjectModel>() {
+			@Override
+			public int compare(ProjectModel o1, ProjectModel o2) {
+				return o2.lastChange.compareTo(o1.lastChange);
+			}
+		});
+
+		List<ProjectListItem> list = new ArrayList<ProjectListItem>();
+		for (ProjectModel proj : projects) {
+			if (proj.isUserProject() || proj.repositories.isEmpty()) {
+				// exclude user projects from list
+				continue;
+			}
+			ProjectListItem item = new ProjectListItem();
+			item.p = proj.name;
+			item.n = StringUtils.isEmpty(proj.title) ? proj.name : proj.title;
+			item.i = proj.description;
+			item.t = getTimeUtils().timeAgo(proj.lastChange);
+			item.d = df.format(proj.lastChange);
+			item.c = proj.repositories.size();
+			list.add(item);
+		}
+		
+		// inject an AngularJS controller with static data
+		NgController ctrl = new NgController(ngCtrl);
+		ctrl.addVariable(ngList, list);
+		add(new HeaderContributor(ctrl));
+	}
+
+	protected class ProjectListItem implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+		
+		String p; // path
+		String n; // name
+		String t; // time ago
+		String d; // last updated
+		String i; // information/description
+		long c;   // repository count
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html
new file mode 100644
index 0000000..4c3aecd
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"  
+      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"  
+      xml:lang="en"  
+      lang="en"> 
+
+<wicket:panel>
+	<div wicket:id="listComponent">[component]</div>
+</wicket:panel>
+</html>
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java
new file mode 100644
index 0000000..6c43b78
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.io.Serializable;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.freemarker.FreemarkerPanel;
+import com.gitblit.wicket.ng.NgController;
+import com.gitblit.wicket.pages.EditRepositoryPage;
+
+/**
+ * A client-side filterable rich repository list which uses Freemarker, Wicket,
+ * and AngularJS. 
+ * 
+ * @author James Moger
+ *
+ */
+public class FilterableRepositoryList extends BasePanel {
+
+	private static final long serialVersionUID = 1L;
+
+	private final List<RepositoryModel> repositories;
+	
+	private String title;
+	
+	private String iconClass;
+	
+	private boolean allowCreate;
+	
+	public FilterableRepositoryList(String id, List<RepositoryModel> repositories) {
+		super(id);
+		this.repositories = repositories;
+	}
+	
+	public void setTitle(String title, String iconClass) {
+		this.title = title;
+		this.iconClass = iconClass;
+	}
+	
+	public void setAllowCreate(boolean value) {
+		this.allowCreate = value;
+	}
+
+	@Override
+	protected void onInitialize() {
+		super.onInitialize();
+
+		String id = getId();
+		String ngCtrl = id + "Ctrl";
+		String ngList = id + "List";
+		
+		Map<String, Object> values = new HashMap<String, Object>();
+		values.put("ngCtrl",  ngCtrl);
+		values.put("ngList",  ngList);
+		
+		// use Freemarker to setup an AngularJS/Wicket html snippet
+		FreemarkerPanel panel = new FreemarkerPanel("listComponent", "FilterableRepositoryList.fm", values);
+		panel.setParseGeneratedMarkup(true);
+		panel.setRenderBodyOnly(true);
+		add(panel);
+		
+		// add the Wicket controls that are referenced in the snippet 
+		String listTitle = StringUtils.isEmpty(title) ? getString("gb.repositories") : title;
+		panel.add(new Label(ngList + "Title", MessageFormat.format("{0} ({1})", listTitle, repositories.size())));
+		if (StringUtils.isEmpty(iconClass)) {
+			panel.add(new Label(ngList + "Icon").setVisible(false));
+		} else {
+			Label icon = new Label(ngList + "Icon");
+			WicketUtils.setCssClass(icon, iconClass);
+			panel.add(icon);
+		}
+		
+		if (allowCreate) {
+			panel.add(new LinkPanel(ngList + "Button", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class));
+		} else {
+			panel.add(new Label(ngList + "Button").setVisible(false));
+		}
+		
+		String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
+		final DateFormat df = new SimpleDateFormat(format);
+		df.setTimeZone(getTimeZone());
+
+		// prepare the simplified repository models list
+		List<RepoListItem> list = new ArrayList<RepoListItem>();
+		for (RepositoryModel repo : repositories) {
+			String name = StringUtils.stripDotGit(repo.name); 
+			String path = "";
+			if (name.indexOf('/') > -1) {
+				path = name.substring(0, name.lastIndexOf('/') + 1);
+				name = name.substring(name.lastIndexOf('/') + 1);
+			}
+			
+			RepoListItem item = new RepoListItem();
+			item.n = name;
+			item.p = path;
+			item.r = repo.name;
+			item.i = repo.description;
+			item.s = GitBlit.self().getStarCount(repo);
+			item.t = getTimeUtils().timeAgo(repo.lastChange);
+			item.d = df.format(repo.lastChange);
+			item.c = StringUtils.getColor(StringUtils.stripDotGit(repo.name));
+			item.wc = repo.isBare ? 0 : 1;
+			list.add(item);
+		}
+		
+		// inject an AngularJS controller with static data
+		NgController ctrl = new NgController(ngCtrl);
+		ctrl.addVariable(ngList, list);
+		add(new HeaderContributor(ctrl));
+	}
+	
+	protected class RepoListItem implements Serializable {
+
+		private static final long serialVersionUID = 1L;
+		
+		String r; // repository
+		String n; // name
+		String p; // project/path
+		String t; // time ago
+		String d; // last updated
+		String i; // information/description
+		long s;   // stars
+		String c; // html color
+		int wc;   // working copy: 1 = true, 0 = false
+	}
+}
\ No newline at end of file
diff --git a/src/site/design.mkd b/src/site/design.mkd
index 8392a9f..7171197 100644
--- a/src/site/design.mkd
+++ b/src/site/design.mkd
@@ -47,6 +47,7 @@
 - [JCalendar](http://www.toedter.com/en/jcalendar) (LGPL 2.1)
 - [Commons-Compress](http://commons.apache.org/compress) (Apache 2.0)
 - [XZ for Java](http://tukaani.org/xz/java.html) (Public Domain)
+- [FreeMarker](http://www.freemarker.org) (modified BSD)
 
 ### Other Build Dependencies
 - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)

--
Gitblit v1.9.1