<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-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/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/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/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/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" /> | <classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" /> |
AngularJS, release under the | AngularJS, release under the | ||||
MIT License. | MIT License. | ||||
http://angularjs.org/ | |||||
http://angularjs.org/ | |||||
--------------------------------------------------------------------------- | |||||
FreeMarker | |||||
--------------------------------------------------------------------------- | |||||
FreeMarker, release under a | |||||
modified BSD License. (http://www.freemarker.org/docs/app_license.html) | |||||
http://www.freemarker.org/ |
- compile 'com.toedter:jcalendar:1.3.2' :authority | - compile 'com.toedter:jcalendar:1.3.2' :authority | ||||
- compile 'org.apache.commons:commons-compress:1.4.1' :war | - compile 'org.apache.commons:commons-compress:1.4.1' :war | ||||
- compile 'com.force.api:force-partner-api:24.0.0' :war | - compile 'com.force.api:force-partner-api:24.0.0' :war | ||||
- compile 'org.freemarker:freemarker:2.3.19' :war | |||||
- test 'junit' | - test 'junit' | ||||
# Dependencies for Selenium web page testing | # Dependencies for Selenium web page testing | ||||
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar | - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar |
</SOURCES> | </SOURCES> | ||||
</library> | </library> | ||||
</orderEntry> | </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"> | <orderEntry type="module-library" scope="TEST"> | ||||
<library name="junit-4.11.jar"> | <library name="junit-4.11.jar"> | ||||
<CLASSES> | <CLASSES> |
/* | |||||
* 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); | |||||
} | |||||
} |
/* | |||||
* 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; | |||||
} | |||||
} |
<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> |
<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"> </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> |
<div wicket:id="active">[recently active]</div> | <div wicket:id="active">[recently active]</div> | ||||
</div> | </div> | ||||
<div class="tab-pane" id="projects"> | <div class="tab-pane" id="projects"> | ||||
<div wicket:id="projectList">[all projects]</div> | |||||
<div wicket:id="projects">[all projects]</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</wicket:fragment> | </wicket:fragment> | ||||
<div wicket:id="active">[recently active]</div> | <div wicket:id="active">[recently active]</div> | ||||
</div> | </div> | ||||
<div class="tab-pane" id="projects"> | <div class="tab-pane" id="projects"> | ||||
<div wicket:id="projectList">[all projects]</div> | |||||
<div wicket:id="projects">[all projects]</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</wicket:fragment> | </wicket:fragment> | ||||
</table> | </table> | ||||
</wicket:fragment> | </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"> </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"> </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"> </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> | </wicket:extend> | ||||
</body> | </body> | ||||
</html> | </html> |
import java.io.FileInputStream; | import java.io.FileInputStream; | ||||
import java.io.InputStream; | import java.io.InputStream; | ||||
import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||
import java.io.Serializable; | |||||
import java.text.DateFormat; | |||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.text.SimpleDateFormat; | |||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Calendar; | import java.util.Calendar; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import org.apache.wicket.Component; | import org.apache.wicket.Component; | ||||
import org.apache.wicket.PageParameters; | 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.basic.Label; | ||||
import org.apache.wicket.markup.html.panel.Fragment; | import org.apache.wicket.markup.html.panel.Fragment; | ||||
import org.eclipse.jgit.lib.Constants; | import org.eclipse.jgit.lib.Constants; | ||||
import com.gitblit.utils.StringUtils; | import com.gitblit.utils.StringUtils; | ||||
import com.gitblit.wicket.GitBlitWebSession; | import com.gitblit.wicket.GitBlitWebSession; | ||||
import com.gitblit.wicket.WicketUtils; | 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 { | public class MyDashboardPage extends DashboardPage { | ||||
add(repositoryTabs); | 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 | // active repository list | ||||
if (active.isEmpty()) { | if (active.isEmpty()) { | ||||
repositoryTabs.add(new Label("active").setVisible(false)); | repositoryTabs.add(new Label("active").setVisible(false)); | ||||
} else { | } 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 | // starred repository list | ||||
if (ArrayUtils.isEmpty(starred)) { | if (ArrayUtils.isEmpty(starred)) { | ||||
repositoryTabs.add(new Label("starred").setVisible(false)); | repositoryTabs.add(new Label("starred").setVisible(false)); | ||||
} else { | } 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 | // owned repository list | ||||
if (ArrayUtils.isEmpty(owned)) { | if (ArrayUtils.isEmpty(owned)) { | ||||
repositoryTabs.add(new Label("owned").setVisible(false)); | repositoryTabs.add(new Label("owned").setVisible(false)); | ||||
} else { | } 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); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
return MessageFormat.format(getString("gb.failedToReadMessage"), file); | 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 | |||||
} | |||||
} | } |
</tr> | </tr> | ||||
</table> | </table> | ||||
</wicket:fragment> | </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"> </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> | </body> | ||||
</html> | </html> |
import org.apache.wicket.PageParameters; | import org.apache.wicket.PageParameters; | ||||
import org.apache.wicket.markup.html.basic.Label; | import org.apache.wicket.markup.html.basic.Label; | ||||
import org.apache.wicket.markup.html.link.ExternalLink; | import org.apache.wicket.markup.html.link.ExternalLink; | ||||
import org.apache.wicket.markup.html.panel.Fragment; | |||||
import com.gitblit.GitBlit; | import com.gitblit.GitBlit; | ||||
import com.gitblit.Keys; | import com.gitblit.Keys; | ||||
import com.gitblit.wicket.PageRegistration.DropDownMenuItem; | import com.gitblit.wicket.PageRegistration.DropDownMenuItem; | ||||
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; | import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration; | ||||
import com.gitblit.wicket.WicketUtils; | import com.gitblit.wicket.WicketUtils; | ||||
import com.gitblit.wicket.panels.FilterableRepositoryList; | |||||
public class ProjectPage extends DashboardPage { | public class ProjectPage extends DashboardPage { | ||||
if (repositories.isEmpty()) { | if (repositories.isEmpty()) { | ||||
add(new Label("repositoryList").setVisible(false)); | add(new Label("repositoryList").setVisible(false)); | ||||
} else { | } else { | ||||
Fragment activeView = createNgList("repositoryList", "repositoryListFragment", "repositoryListCtrl", repositories); | |||||
add(activeView); | |||||
FilterableRepositoryList repoList = new FilterableRepositoryList("repositoryList", repositories); | |||||
repoList.setAllowCreate(user.canCreate(project.name + "/")); | |||||
add(repoList); | |||||
} | } | ||||
} | } | ||||
<!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> |
/* | |||||
* 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 | |||||
} | |||||
} |
<!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> |
/* | |||||
* 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 | |||||
} | |||||
} |
- [JCalendar](http://www.toedter.com/en/jcalendar) (LGPL 2.1) | - [JCalendar](http://www.toedter.com/en/jcalendar) (LGPL 2.1) | ||||
- [Commons-Compress](http://commons.apache.org/compress) (Apache 2.0) | - [Commons-Compress](http://commons.apache.org/compress) (Apache 2.0) | ||||
- [XZ for Java](http://tukaani.org/xz/java.html) (Public Domain) | - [XZ for Java](http://tukaani.org/xz/java.html) (Public Domain) | ||||
- [FreeMarker](http://www.freemarker.org) (modified BSD) | |||||
### Other Build Dependencies | ### Other Build Dependencies | ||||
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed) | - [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed) |