diff --git a/pom.xml b/pom.xml index ba4aa4edb..2494ffd04 100644 --- a/pom.xml +++ b/pom.xml @@ -426,6 +426,12 @@ ${thymeleaf.extras.version} true + + de.neuland-bfi + jade4j + 0.4.2 + true + org.slf4j diff --git a/src/main/assembly/assembly-example-project-jade.xml b/src/main/assembly/assembly-example-project-jade.xml new file mode 100644 index 000000000..71cc39a23 --- /dev/null +++ b/src/main/assembly/assembly-example-project-jade.xml @@ -0,0 +1,19 @@ + + base + false + + + zip + + + + ${project.build.directory}/example-project-jade/ + / + + .git + README.md + LICENSE + + + + \ No newline at end of file diff --git a/src/main/java/org/jbake/template/JadeTemplateEngine.java b/src/main/java/org/jbake/template/JadeTemplateEngine.java new file mode 100644 index 000000000..31ecb848b --- /dev/null +++ b/src/main/java/org/jbake/template/JadeTemplateEngine.java @@ -0,0 +1,155 @@ +package org.jbake.template; + +import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx; +import com.orientechnologies.orient.core.record.impl.ODocument; +import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery; + +import de.neuland.jade4j.JadeConfiguration; +import de.neuland.jade4j.exceptions.JadeCompilerException; +import de.neuland.jade4j.filter.CDATAFilter; +import de.neuland.jade4j.filter.CssFilter; +import de.neuland.jade4j.filter.Filter; +import de.neuland.jade4j.filter.JsFilter; +import de.neuland.jade4j.model.JadeModel; +import de.neuland.jade4j.template.FileTemplateLoader; +import de.neuland.jade4j.template.JadeTemplate; +import de.neuland.jade4j.template.TemplateLoader; + +import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.lang.StringEscapeUtils; +import org.jbake.app.ContentStore; +import org.jbake.app.DBUtil; +import org.jbake.app.DocumentList; +import org.jbake.model.DocumentTypes; + +import java.io.File; +import java.io.IOException; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Renders pages using the Jade template language. + * + * @author Aleksandar Vidakovic + */ +public class JadeTemplateEngine extends AbstractTemplateEngine { + private static final String FILTER_CDATA = "cdata"; + private static final String FILTER_STYLE = "css"; + private static final String FILTER_SCRIPT = "js"; + + private JadeConfiguration jadeConfiguration = new JadeConfiguration(); + + public JadeTemplateEngine(final CompositeConfiguration config, final ContentStore db, final File destination, final File templatesPath) { + super(config, db, destination, templatesPath); + + TemplateLoader loader = new FileTemplateLoader(templatesPath.getAbsolutePath() + "/", "UTF-8"); + jadeConfiguration.setTemplateLoader(loader); + jadeConfiguration.setFilter(FILTER_CDATA, new CDATAFilter()); + jadeConfiguration.setFilter(FILTER_SCRIPT, new JsFilter()); + jadeConfiguration.setFilter(FILTER_STYLE, new CssFilter()); + jadeConfiguration.getSharedVariables().put("formatter", new FormatHelper()); + } + + @Override + public void renderDocument(Map model, String templateName, Writer writer) throws RenderingException { + try { + JadeTemplate template = jadeConfiguration.getTemplate(templateName); + + renderTemplate(template, model, writer); + } catch (IOException e) { + throw new RenderingException(e); + } + } + + public void renderTemplate(JadeTemplate template, Map model, Writer writer) throws JadeCompilerException { + JadeModel jadeModel = wrap(jadeConfiguration.getSharedVariables()); + jadeModel.putAll(model); + template.process(jadeModel, writer); + } + + private JadeModel wrap(final Map model) { + return new JadeModel(model) { + + @Override + public Object get(final Object property) { + String key = property.toString(); + if ("db".equals(key)) { + return db; + } + if ("published_posts".equals(key)) { + List query = db.getPublishedPosts(); + return DocumentList.wrap(query.iterator()); + } + if ("published_pages".equals(key)) { + List query = db.getPublishedPages(); + return DocumentList.wrap(query.iterator()); + } + if ("published_content".equals(key)) { + List publishedContent = new ArrayList(); + String[] documentTypes = DocumentTypes.getDocumentTypes(); + for (String docType : documentTypes) { + List query = db.getPublishedContent(docType); + publishedContent.addAll(query); + } + return DocumentList.wrap(publishedContent.iterator()); + } + if ("all_content".equals(key)) { + List allContent = new ArrayList(); + String[] documentTypes = DocumentTypes.getDocumentTypes(); + for (String docType : documentTypes) { + List query = db.getAllContent(docType); + allContent.addAll(query); + } + return DocumentList.wrap(allContent.iterator()); + } + if ("alltags".equals(key)) { + List query = db.getAllTagsFromPublishedPosts(); + Set result = new HashSet(); + for (ODocument document : query) { + String[] tags = DBUtil.toStringArray(document.field("tags")); + Collections.addAll(result, tags); + } + return result; + } + String[] documentTypes = DocumentTypes.getDocumentTypes(); + for (String docType : documentTypes) { + if ((docType+"s").equals(key)) { + return DocumentList.wrap(db.getAllContent(docType).iterator()); + } + } + if ("tag_posts".equals(key)) { + String tag = model.get("tag").toString(); + // fetch the tag posts from db + List query = db.getPublishedPostsByTag(tag); + return DocumentList.wrap(query.iterator()); + } + + return super.get(property); + } + }; + } + + public static class FormatHelper { + private Map formatters = new HashMap(); + + public String format(Date date, String pattern) { + if(date!=null && pattern!=null) { + SimpleDateFormat df = formatters.get(pattern); + + if(df==null) { + df = new SimpleDateFormat(pattern); + formatters.put(pattern, df); + } + + return df.format(date); + } else { + return ""; + } + } + + public String escape(String s) { + return StringEscapeUtils.escapeHtml(s); + } + } +} diff --git a/src/main/resources/META-INF/org.jbake.parser.TemplateEngines.properties b/src/main/resources/META-INF/org.jbake.parser.TemplateEngines.properties index ba78df94c..934245673 100644 --- a/src/main/resources/META-INF/org.jbake.parser.TemplateEngines.properties +++ b/src/main/resources/META-INF/org.jbake.parser.TemplateEngines.properties @@ -2,4 +2,6 @@ org.jbake.template.FreemarkerTemplateEngine=ftl org.jbake.template.GroovyTemplateEngine=groovy,gsp,gxml org.jbake.template.GroovyMarkupTemplateEngine=tpl org.jbake.template.ThymeleafTemplateEngine=thyme -org.jbake.template.PebbleTemplateEngine=pebble,peb \ No newline at end of file +org.jbake.template.PebbleTemplateEngine=pebble,peb +org.jbake.template.ThymeleafTemplateEngine=thyme,html +org.jbake.template.JadeTemplateEngine=jade diff --git a/src/test/java/org/jbake/app/JadeRendererTest.java b/src/test/java/org/jbake/app/JadeRendererTest.java new file mode 100644 index 000000000..40a1de0e3 --- /dev/null +++ b/src/test/java/org/jbake/app/JadeRendererTest.java @@ -0,0 +1,208 @@ +package org.jbake.app; + +import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx; +import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.io.FileUtils; +import org.jbake.app.ConfigUtil.Keys; +import org.jbake.model.DocumentTypes; +import org.junit.*; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Iterator; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JadeRendererTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private File sourceFolder; + private File destinationFolder; + private File templateFolder; + private CompositeConfiguration config; + private ContentStore db; + + @Before + public void setup() throws Exception, IOException, URISyntaxException { + URL sourceUrl = this.getClass().getResource("/"); + + sourceFolder = new File(sourceUrl.getFile()); + if (!sourceFolder.exists()) { + throw new Exception("Cannot find sample data structure!"); + } + + destinationFolder = folder.getRoot(); + + templateFolder = new File(sourceFolder, "jadeTemplates"); + if (!templateFolder.exists()) { + throw new Exception("Cannot find template folder!"); + } + + config = ConfigUtil.load(new File(this.getClass().getResource("/").getFile())); + Iterator keys = config.getKeys(); + while (keys.hasNext()) { + String key = keys.next(); + if (key.startsWith("template") && key.endsWith(".file")) { + String old = (String)config.getProperty(key); + config.setProperty(key, old.substring(0, old.length()-4)+".jade"); + } + } + Assert.assertEquals(".html", config.getString(Keys.OUTPUT_EXTENSION)); + db = DBUtil.createDataStore("memory", "documents"+System.currentTimeMillis()); + } + + @After + public void cleanup() throws InterruptedException { + db.drop(); + db.close(); + } + + @Test + public void renderPost() throws Exception { + // setup + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Parser parser = new Parser(config, sourceFolder.getPath()); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + String filename = "second-post.html"; + + File sampleFile = new File(sourceFolder.getPath() + File.separator + "content" + File.separator + "blog" + File.separator + "2013" + File.separator + filename); + Map content = parser.processFile(sampleFile); + content.put("uri", "/" + filename); + renderer.render(content); + File outputFile = new File(destinationFolder, filename); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("

Second Post

") + .contains("

28") + .contains("2013

") + .contains("Lorem ipsum dolor sit amet") + .contains("
Published Posts
") + .contains("blog/2012/first-post.html"); + } + + @Test + public void renderPage() throws Exception { + // setup + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Parser parser = new Parser(config, sourceFolder.getPath()); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + // TODO: fix this + String filename = "about.html"; + + File sampleFile = new File(sourceFolder.getPath() + File.separator + "content" + File.separator + filename); + Map content = parser.processFile(sampleFile); + content.put("uri", "/" + filename); + renderer.render(content); + File outputFile = new File(destinationFolder, filename); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("

About

") + .contains("All about stuff!") + .contains("
Published Pages
") + .contains("/projects.html"); + } + + @Test + public void renderIndex() throws Exception { + //setup + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + //exec + renderer.renderIndex("index.html"); + + //validate + File outputFile = new File(destinationFolder, "index.html"); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("

First Post

") + .contains("

Second Post

"); + } + + @Test + public void renderFeed() throws Exception { + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + renderer.renderFeed("feed.xml"); + File outputFile = new File(destinationFolder, "feed.xml"); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("My corner of the Internet") + .contains("Second Post") + .contains("First Post"); + } + + @Test + public void renderArchive() throws Exception { + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + renderer.renderArchive("archive.html"); + File outputFile = new File(destinationFolder, "archive.html"); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("Second Post") + .contains("First Post"); + } + + @Test + public void renderTags() throws Exception { + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + renderer.renderTags(crawler.getTags(), "tags"); + + // verify + File outputFile = new File(destinationFolder + File.separator + "tags" + File.separator + "blog.html"); + Assert.assertTrue(outputFile.exists()); + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("Second Post") + .contains("First Post"); + } + + @Test + public void renderSitemap() throws Exception { + DocumentTypes.addDocumentType("paper"); + DBUtil.updateSchema(db); + + Crawler crawler = new Crawler(db, sourceFolder, config); + crawler.crawl(new File(sourceFolder.getPath() + File.separator + "content")); + Renderer renderer = new Renderer(db, destinationFolder, templateFolder, config); + renderer.renderSitemap("sitemap.xml"); + File outputFile = new File(destinationFolder, "sitemap.xml"); + Assert.assertTrue(outputFile.exists()); + + // verify + String output = FileUtils.readFileToString(outputFile); + assertThat(output) + .contains("blog/2013/second-post.html") + .contains("blog/2012/first-post.html") + .contains("papers/published-paper.html") + .doesNotContain("draft-paper.html"); + } +} diff --git a/src/test/resources/jadeTemplates/archive.jade b/src/test/resources/jadeTemplates/archive.jade new file mode 100644 index 000000000..95c404797 --- /dev/null +++ b/src/test/resources/jadeTemplates/archive.jade @@ -0,0 +1,15 @@ +extends layout.jade + +block content + .row-fluid.marketing + .span12 + h2 Archive + each post, index in posts + if last_month != formatter.format(post.date, 'MMMM yyyy') + h3 #{formatter.format(post.date, 'MMMM yyyy')} + h4 + | #{formatter.format(post.date, 'dd MMMM')} + |  -  + a(href='#{post.uri}') #{post.title} + - var last_month = formatter.format(post.date, 'MMMM yyyy') + hr diff --git a/src/test/resources/jadeTemplates/feed.jade b/src/test/resources/jadeTemplates/feed.jade new file mode 100644 index 000000000..125a1ab00 --- /dev/null +++ b/src/test/resources/jadeTemplates/feed.jade @@ -0,0 +1,17 @@ +rss(version='2.0', xmlns:atom='http://www.w3.org/2005/Atom') + channel + title JonathanBullock.com + link http://jonathanbullock.com/ + atom:link(href='http://jonathanbullock.com/feed.xml', rel='self', type='application/rss+xml') + description My corner of the Internet + language en-gb + pubdate #{formatter.format(published_date, 'EEE, d MMM yyyy HH:mm:ss Z')} + lastbuilddate #{formatter.format(published_date, 'EEE, d MMM yyyy HH:mm:ss Z')} + for post in posts + item + title #{post.title} + link http://jonathanbullock.com#{post.uri} + pubdate #{formatter.format(post.date, 'EEE, d MMM yyyy HH:mm:ss Z')} + guid(ispermalink='false') #{post.uri} + description + | #{formatter.escape(post.body)} diff --git a/src/test/resources/jadeTemplates/footer.jade b/src/test/resources/jadeTemplates/footer.jade new file mode 100644 index 000000000..44c3e1cea --- /dev/null +++ b/src/test/resources/jadeTemplates/footer.jade @@ -0,0 +1,11 @@ +.footer + p + | © Jonathan Bullock 2013 | Mixed with + a(href='http://twitter.github.com/bootstrap/') Bootstrap v2.3.1 + | | Baked with + a(href='http://jbake.org') JBake #{version} +// Le javascript +// ================================================== +// Placed at the end of the document so the pages load faster +script(src='/js/jquery-1.9.1.min.js') +script(src='/js/bootstrap.min.js') diff --git a/src/test/resources/jadeTemplates/header.jade b/src/test/resources/jadeTemplates/header.jade new file mode 100644 index 000000000..98aa8ba5c --- /dev/null +++ b/src/test/resources/jadeTemplates/header.jade @@ -0,0 +1,59 @@ +head + meta(charset='utf-8') + title Jonathan Bullock + meta(name='viewport', content='width=device-width, initial-scale=1.0') + meta(name='description', content='') + meta(name='author', content='Jonathan Bullock') + // Le styles + link(href='/css/bootstrap.min.css', rel='stylesheet') + style(type='text/css'). + body { + padding-top: 20px; + padding-bottom: 40px; + } + + /* Custom container */ + .container-narrow { + margin: 0 auto; + max-width: 700px; + } + + .container-narrow > hr { + margin: 30px 0; + } + + /* Main marketing message and sign up button */ + .jumbotron { + margin: 60px 0; + text-align: center; + } + + .jumbotron h1 { + font-size: 72px; + line-height: 1; + } + + .jumbotron .btn { + font-size: 21px; + padding: 14px 24px; + } + + /* Supporting marketing content */ + .marketing { + margin: 60px 0; + } + + .marketing p + h4 { + margin-top: 28px; + } + link(href='/css/bootstrap-responsive.min.css', rel='stylesheet') + // HTML5 shim, for IE6-8 support of HTML5 elements + //if lt IE 9 + script(src='/js/html5shiv.js') + // Fav and touch icons + // + + + + + diff --git a/src/test/resources/jadeTemplates/index.jade b/src/test/resources/jadeTemplates/index.jade new file mode 100644 index 000000000..7760b2d38 --- /dev/null +++ b/src/test/resources/jadeTemplates/index.jade @@ -0,0 +1,13 @@ +extends layout.jade + +block content + .row-fluid.marketing + .span12 + each post, index in posts + if index < 3 + h4 + a(href='#{post.uri}') #{post.title} + p #{formatter.format(post.date, 'dd MMMM yyyy')} - #{post.body.substring(0, 150)}... + + a(href='/archive.html') Archive + hr diff --git a/src/test/resources/jadeTemplates/layout.jade b/src/test/resources/jadeTemplates/layout.jade new file mode 100644 index 000000000..2e9942367 --- /dev/null +++ b/src/test/resources/jadeTemplates/layout.jade @@ -0,0 +1,20 @@ +html + include header + body + .container-narrow + .masthead + ul.nav.nav-pills.pull-right + li + a(href='/') Home + li + a(href='/about.html') About + li + a(href='/projects.html') Projects + li + a(href='/feed.xml') Subscribe + h3.muted Jonathan Bullock + hr + + block content + + include footer \ No newline at end of file diff --git a/src/test/resources/jadeTemplates/page.jade b/src/test/resources/jadeTemplates/page.jade new file mode 100644 index 000000000..6a7e93ed6 --- /dev/null +++ b/src/test/resources/jadeTemplates/page.jade @@ -0,0 +1,12 @@ +extends layout.jade + +block content + .row-fluid.marketing + .span12 + h4 #{content.title} + p #{content.body} + hr + h5 Published Pages + + for page in published_pages + a(href="#{config.site_host}/#{page.uri}") #{page.title} diff --git a/src/test/resources/jadeTemplates/post.jade b/src/test/resources/jadeTemplates/post.jade new file mode 100644 index 000000000..ceba3d5d0 --- /dev/null +++ b/src/test/resources/jadeTemplates/post.jade @@ -0,0 +1,13 @@ +extends layout.jade + +block content + .row-fluid.marketing + .span12 + h2 #{content.title} + p.post-date #{formatter.format(content.date,'dd MMMM yyyy')} + p #{content.body} + hr + h5 Published Posts + + for post in published_posts + a(href="#{config.site_host}/#{post.uri}") #{post.title} diff --git a/src/test/resources/jadeTemplates/sitemap.jade b/src/test/resources/jadeTemplates/sitemap.jade new file mode 100644 index 000000000..c7a178ec1 --- /dev/null +++ b/src/test/resources/jadeTemplates/sitemap.jade @@ -0,0 +1,5 @@ +urlset(xmlns='http://www.sitemaps.org/schemas/sitemap/0.9', xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance', xsi:schemalocation='http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd') + for content in published_content + url + loc #{config.site_host}#{content.uri} + lastmod #{formatter.format(content.date, 'yyyy-MM-dd')} diff --git a/src/test/resources/jadeTemplates/tags.jade b/src/test/resources/jadeTemplates/tags.jade new file mode 100644 index 000000000..d5b257245 --- /dev/null +++ b/src/test/resources/jadeTemplates/tags.jade @@ -0,0 +1,15 @@ +extends layout.jade + +block content + .row-fluid.marketing + .span12 + h2 Tags + each post, index in posts + if last_month != formatter.format(post.date, 'MMMM yyyy') + h3 #{formatter.format(post.date, 'MMMM yyyy')} + h4 + | #{formatter.format(post.date, 'dd MMMM')} + |  -  + a(href='#{post.uri}') #{post.title} + - var last_month = formatter.format(post.date, 'MMMM yyyy') + hr