├── CNAME ├── .gitignore ├── images └── gallery-screenshot.png ├── Makefile ├── index.jade ├── _config.json ├── javascripts └── site.js ├── _posts ├── 2011-07-29-pop-0.0.9.md ├── 2011-08-02-pop-0.1.0.md ├── 2011-08-18-pop-0.1.1.md ├── 2011-07-26-pop-0.0.6.md ├── 2011-07-24-pop-0.0.4.md ├── 2011-07-23-pop-0.0.3.md ├── 2011-08-23-pop-ga.md ├── 2011-07-21-example-post-about-something.md ├── 2011-07-22-pop-0.0.2.md ├── 2011-07-28-pop-0.0.8.md ├── 2011-07-27-pop-0.0.7.md └── 2011-07-25-pop-0.0.5.md ├── tags.jade ├── _layouts ├── post.jade └── default.jade ├── stylesheets └── screen.styl └── doc └── index.html /CNAME: -------------------------------------------------------------------------------- 1 | popjs.com 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | _site/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /images/gallery-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/pop-pages/master/images/gallery-screenshot.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docs: 2 | @curl https://raw.github.com/alexyoung/pop/master/doc/index.html > doc/index.html 3 | 4 | .PHONY: docs 5 | -------------------------------------------------------------------------------- /index.jade: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Pop, A Static Site Builder 4 | paginate: true 5 | --- 6 | 7 | !{paginatedPosts()} 8 | !{paginate} 9 | -------------------------------------------------------------------------------- /_config.json: -------------------------------------------------------------------------------- 1 | { "url": "http://popjs.com/" 2 | , "title": "Pop Blog" 3 | , "permalink": "/:year/:month/:day/:title" 4 | , "paginate": 10 5 | , "exclude": ["\\.swp"] 6 | , "require": ["pop-disqus"] 7 | , "autoGenerate": [{"feed": "feed.xml"}, {"rss": "feed.rss"}] } 8 | -------------------------------------------------------------------------------- /javascripts/site.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('.tag-list a.tag').live('click', function() { 3 | $(this).parents('h2').next('.posts').toggle(); 4 | }); 5 | 6 | if (window.location.hash) { 7 | $(window.location.hash).next('.posts').show(); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /_posts/2011-07-29-pop-0.0.9.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.9 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop 0.0.9 is out: 10 | 11 | * Added centralised logging (mainly so I can turn off logs in tests) 12 | * Defaults post author to `LOGNAME` 13 | * Escapes post file names 14 | 15 | -------------------------------------------------------------------------------- /_posts/2011-08-02-pop-0.1.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.1.0 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | I've released Pop 0.1.0. This cements the basic functionality that I want to include in Pop for the near future. 10 | 11 | This release includes a tags page for the built-in site generator, and a helper called `postsForTag`. 12 | 13 | -------------------------------------------------------------------------------- /_posts/2011-08-18-pop-0.1.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.1.1 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop 0.1.1 has been released. This version improves the build process by making it use more events rather than counting down the number of files that have been processed. 10 | 11 | This should make the site build process more stable. 12 | 13 | -------------------------------------------------------------------------------- /_posts/2011-07-26-pop-0.0.6.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.6 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop 0.0.6 adds the following features and enhancements: 10 | 11 | * A new command-line option, `render`, makes Pop only render files that match a pattern 12 | * Character and word truncation helpers have been added 13 | * The Atom feed can now summarise posts based on paragraph length 14 | 15 | -------------------------------------------------------------------------------- /_posts/2011-07-24-pop-0.0.4.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.4 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop 0.0.4 is out: 10 | 11 | * Running `pop` will generate a site in the current directory and exit 12 | * Running `pop` on a path that doesn't look like a Pop site will print an error rather than a confusing stack trace 13 | * Pop server mode will watch all subdirectories for changes, so things like stylesheets (including Stylus files) will automatically regenerate the changed file 14 | -------------------------------------------------------------------------------- /_posts/2011-07-23-pop-0.0.3.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.3 4 | author: Pop 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop version 0.0.3 is out. 10 | 11 | * Atom feed helper takes less options (the variables are all in `config`) 12 | * Atom feed uses `config.perPage` 13 | * Post file names with extra dots will be parsed correctly 14 | * YAML front-matter will be extracted less greedily 15 | * YAML front-matter can now contain a `summary` property to make it easier to display post summaries in lists 16 | * Trailing slashes will be removed from `config.url` 17 | -------------------------------------------------------------------------------- /_posts/2011-08-23-pop-ga.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Google Analytics Plugin 4 | author: Pop 5 | tags: 6 | - plugins 7 | - contributions 8 | --- 9 | 10 | [pop-ga](https://github.com/shapeshed/pop-ga) by [George Ornbo](http://shapeshed.com/) is a Google Analytics plugin for pop. It can be installed using npm: 11 | 12 | npm install -g pop-ga 13 | 14 | And then used by updating your `_config.json`: 15 | 16 | , "require": ["pop-ga"] 17 | 18 | Next, call the plugin's helper with your Google Analytics ID: 19 | 20 | 21 | !{ga('UA-345678-90')} 22 | 23 | 24 | -------------------------------------------------------------------------------- /tags.jade: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | h1 Tags 6 | 7 | p Click a tag to view the associated posts. 8 | 9 | .tag-list 10 | - var tags = allTags(); 11 | - if (tags && tags.length > 0) 12 | - for (var i = 0, tag = tags[0], posts = postsForTag(tag); i < tags.length; i++, tag = tags[i], posts = postsForTag(tag)) 13 | h2(id=tag) 14 | a.tag(href="#") (#{posts.length}) #{tag} 15 | .posts 16 | - for (var j = 0, post = posts[j]; j < posts.length; j++, post = posts[j]) 17 | p 18 | a(href="#{post.url}") #{post.title} 19 | 20 | -------------------------------------------------------------------------------- /_posts/2011-07-21-example-post-about-something.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pop Released 3 | author: Pop 4 | layout: post 5 | tags: 6 | - releases 7 | --- 8 | 9 | Pop version 0.0.1 has been released: 10 | 11 | * GitHub: [alexyoung / pop](https://github.com/alexyoung/pop) 12 | 13 | This includes [stextile](https://github.com/alexyoung/stextile), which is a rough-and-ready [Textile](http://www.textism.com/tools/textile/) parser. Pop also supports [Markdown](http://daringfireball.net/projects/markdown/). 14 | 15 | Pop sites use [Jade](http://jade-lang.com/) and [Stylus](http://learnboost.github.com/stylus/). These are CSS selector-based shorthands for quickly writing HTML and CSS. 16 | 17 | -------------------------------------------------------------------------------- /_posts/2011-07-22-pop-0.0.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.2 4 | author: Pop 5 | tags: 6 | - releases 7 | - examples 8 | --- 9 | 10 | Pop version 0.0.2 has been released. This version adds additional default styles to the site generator (blockquotes, pagination), and adds post summary support. 11 | 12 | Writing YAML front-matter like this: 13 | 14 | --- 15 | layout: post 16 | title: Amazing Facts About Lions 17 | summary: Above all, lions are amazing creatures. 18 | author: Dave Grohl 19 | tags: 20 | - releases 21 | --- 22 | 23 | Post content here. 24 | 25 | This will make `post.summary` available to templates. It gets processed with markdown or textile, depending on the format of the post, and also applies helpers. I've added this to the `hNews()` helper when used to generate summaries. 26 | -------------------------------------------------------------------------------- /_posts/2011-07-28-pop-0.0.8.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.8 4 | author: Pop 5 | tags: 6 | - releases 7 | - examples 8 | --- 9 | 10 | I've just released Pop 0.0.8, which adds: 11 | 12 | * An option to `_config.json` for the port the local server uses 13 | * RSS feed helper 14 | 15 | ## RSS Feeds 16 | 17 | You can get RSS feeds "for free" by adding an `autoGenerate` option to `_config.json`: 18 | 19 | "autoGenerate": [{"feed": "feed.xml"}, {"rss": "feed.rss"}] 20 | 21 | The helper itself is fairly easy to use: 22 | 23 | /** 24 | * RSS Jade template. 25 | * 26 | * @param {String} Feed URL 27 | * @param {Integer} Number of paragraphs to summarise 28 | * @param {String} Optional description 29 | * 30 | * @return {String} 31 | */ 32 | rss: function(feed, summarise, description) { 33 | 34 | I've checked a few example feeds with the [W3C validation service](http://validator.w3.org/feed/). 35 | 36 | -------------------------------------------------------------------------------- /_layouts/post.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title #{post.title} 5 | link(href="/stylesheets/screen.css", media="screen", rel="stylesheet", type="text/css") 6 | link(rel="alternate", type="application/rss+xml", title="RSS 2.0", href="/feed.rss") 7 | link(rel="alternate", type="application/atom+xml", title="Atom Feed", href="/feed.xml") 8 | body 9 | header#header 10 | hgroup 11 | h1 12 | a(href="/") #{site.config.title} 13 | hgroup 14 | Fork me on GitHub 15 | nav#breadcrumb 16 | a.read-more(href="/") ← Back 17 | !{hNews(post)} 18 | !{disqus(post, 'popjs')} 19 | footer 20 | p.copyright Content © Alex R. Young 21 | -------------------------------------------------------------------------------- /_posts/2011-07-27-pop-0.0.7.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.7 4 | author: 5 | tags: 6 | - releases 7 | - tutorials 8 | --- 9 | 10 | Pop 0.0.7 is out: 11 | 12 | * Added generators 13 | * Plugins can add their own generators through `myPlugin.generators` 14 | * `SiteBuilder` should trigger the server properly 15 | 16 | ## Using Generators 17 | 18 | Generating a site with Pop generally works like this: 19 | 20 | pop new new_site 21 | 22 | Generators can now be used to generate different kinds of sites: 23 | 24 | pop new pop-gallery new_site 25 | 26 | This example requires [pop-gallery](https://github.com/alexyoung/pop-gallery). 27 | 28 | ## Writing Generators 29 | 30 | Basic usage is to make a CommonJS module with a `generator` property: 31 | 32 | module.exports = { 33 | generator: { 34 | run: function(helpers, pathName) { 35 | } 36 | } 37 | }; 38 | 39 | The parameters will be passed by Pop. The `helpers` value contains the internal cli_tools.js module, and pathName is the destination path for the site. 40 | 41 | A full example is available here: [pop-gallery](https://github.com/alexyoung/pop-gallery). 42 | 43 | -------------------------------------------------------------------------------- /_layouts/default.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang="en") 3 | head 4 | title #{site.config.title} 5 | link(href="/stylesheets/screen.css", media="screen", rel="stylesheet", type="text/css") 6 | link(rel="alternate", type="application/rss+xml", title="RSS 2.0", href="/feed.rss") 7 | link(rel="alternate", type="application/atom+xml", title="Atom Feed", href="/feed.xml") 8 | script(type="text/javascript", src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js") 9 | script(type="text/javascript", src="/javascripts/site.js") 10 | body 11 | header#header 12 | hgroup 13 | h1 14 | a(href="/") #{site.config.title} 15 | hgroup 16 | Fork me on GitHub 17 | nav.panel 18 | h2 About 19 | p Pop is a static site generator. 20 | h2 Links 21 | ul 22 | li 23 | a(href="/feed.xml") Feed 24 | li 25 | a(href="http://twitter.com/alex_young") Twitter 26 | li 27 | a(href="/doc/") API Documentation 28 | li 29 | a(href="http://popjs.com") Powered by Pop 30 | li 31 | a(href="https://github.com/alexyoung/pop") Pop Source Code 32 | li 33 | a(href="http://dailyjs.com") Learn JavaScript 34 | section.content 35 | !{content} 36 | footer 37 | p.copyright Content © Alex R. Young 38 | -------------------------------------------------------------------------------- /_posts/2011-07-25-pop-0.0.5.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Pop 0.0.5 4 | author: Pop 5 | tags: 6 | - releases 7 | - plugins 8 | - tutorials 9 | --- 10 | 11 | Pop 0.0.5 has been released. This version introduces a plugin architecture and post-filters. 12 | 13 | ## Post-Filters 14 | 15 | A post-filter can alter HTML after it has been generated by the template engine. The structure is the same as helpers and pre-filters. Add a file to `_lib/post-filters.js`: 16 | 17 | {% highlight javascript %} 18 | module.exports = { 19 | /** 20 | * Converts Alex's name to something more suitable. 21 | * 22 | * @param {String} The HTML to transform 23 | * @return {String} The transformed HTML 24 | */ 25 | myFilter: function(html) { 26 | return html.replace(/Alex/, 'Super Douche'); 27 | } 28 | }; 29 | {% endhighlight %} 30 | 31 | ## Using Plugins 32 | 33 | Plugins are bundles of helpers, pre-filters, and post-filters that are loaded using the config file. Here's an example, from this site: 34 | 35 | {% highlight javascript %} 36 | { "url": "http://popjs.com/" 37 | , "title": "Pop Blog" 38 | , "permalink": "/:year/:month/:day/:title" 39 | , "paginate": 10 40 | , "exclude": ["\\.swp"] 41 | , "require": ["pop-disqus"] 42 | , "autoGenerate": [{"feed": "feed.xml"}] } 43 | {% endhighlight %} 44 | 45 | The `require` property loads the plugin found in the CommonJS module `pop-disqus`. This plugin is available through npm. 46 | 47 | ## Writing Plugins 48 | 49 | Create a CommonJS module with a suitable `package.json` that exports an object with one or more of these properties: 50 | 51 | * `helpers` 52 | * `filters` 53 | * `postFilters` 54 | 55 | All the usual rules apply. Get started by looking at [pop-disqus](https://github.com/alexyoung/pop-disqus). Write tests, even if they're basic! 56 | 57 | -------------------------------------------------------------------------------- /stylesheets/screen.styl: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | light = #fff 3 | dark = #1a1a1f 4 | back = #ddd 5 | highlight = #d02413 6 | 7 | standard-font-size = 14px 8 | small-font-size = 12px 9 | header-font-size = 128.5% 10 | 11 | /* Functions */ 12 | header-gradient() 13 | background: #cfd2d7 14 | background: -moz-linear-gradient(top, #cfd2d7 0%, #a3a8af 100%) 15 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#cfd2d7), color-stop(100%,#a3a8af)) 16 | background: -webkit-linear-gradient(top, #cfd2d7 0%,#a3a8af 100%) 17 | background: -o-linear-gradient(top, #cfd2d7 0%,#a3a8af 100%) 18 | background: -ms-linear-gradient(top, #cfd2d7 0%,#a3a8af 100%) 19 | background: linear-gradient(top, #cfd2d7 0%,#a3a8af 100%) 20 | 21 | glass-gradient() 22 | background: #f6f8f9 23 | background: -moz-linear-gradient(top, #f6f8f9 0%, #e5ebee 50%, #d7dee3 51%, #f5f7f9 100%) 24 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f6f8f9), color-stop(50%,#e5ebee), color-stop(51%,#d7dee3), color-stop(100%,#f5f7f9)) 25 | background: -webkit-linear-gradient(top, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%) 26 | background: -o-linear-gradient(top, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%) 27 | background: -ms-linear-gradient(top, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%) 28 | background: linear-gradient(top, #f6f8f9 0%,#e5ebee 50%,#d7dee3 51%,#f5f7f9 100%) 29 | 30 | border-radius() 31 | -webkit-border-radius arguments 32 | -moz-border-radius arguments 33 | border-radius arguments 34 | 35 | border-radius-top() 36 | -webkit-border-top-left-radius: arguments 37 | -webkit-border-top-right-radius: arguments 38 | -moz-border-radius-topleft: arguments 39 | -moz-border-radius-topright: arguments 40 | border-top-left-radius: arguments 41 | border-top-right-radius: arguments 42 | 43 | shadow() 44 | -moz-box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) 45 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) 46 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) 47 | 48 | big-shadow() 49 | box-shadow: 0 0 15px #000 50 | 51 | lozenge() 52 | -webkit-border-radius: 10px 53 | -moz-border-radius: 10px 54 | border-radius: 10px 55 | background-color: #bbc4cc 56 | padding: 4px 8px 57 | text-decoration: none 58 | color: dark 59 | font-weight: bold 60 | font-size: small-font-size 61 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) 62 | 63 | reset() 64 | margin: 0 65 | padding: 0 66 | 67 | /* Styles */ 68 | body 69 | font-family: "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif 70 | font-size: standard-font-size 71 | margin: 0 auto 72 | padding-bottom: 2em 73 | word-spacing: 0.1em 74 | background-color: back 75 | max-width: 960px 76 | 77 | h1, h2, h3, h4, p 78 | reset() 79 | 80 | h1, h2, h3, h4 81 | font-size: header-font-size 82 | 83 | a 84 | color: highlight 85 | 86 | a:hover 87 | text-decoration: none 88 | 89 | blockquote 90 | reset() 91 | font-style: italic 92 | text-shadow: 0 1px 0 #fff 93 | color: #333 94 | 95 | #header 96 | margin: 10px 0 20px 0 97 | padding-bottom: 10px 98 | border-bottom: 3px solid #ced3d2 99 | 100 | #header h2 101 | color: #333 102 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) 103 | 104 | #header h1 105 | font-size: 50px 106 | text-shadow: rgba(0,0,0,0.3) 0 -1px 107 | 108 | #header h1 a 109 | color: #fff 110 | text-decoration: none 111 | 112 | article 113 | margin: 0 0 20px 0 114 | padding-bottom: 10px 115 | background-color: light 116 | border-radius 5px 117 | border-color: light 118 | shadow() 119 | 120 | article header 121 | margin-bottom: 10px 122 | 123 | article header h1 124 | border-top: 1px solid #bbb 125 | margin: 0 0 5px 0 126 | font-size: header-font-size 127 | 128 | article header h1 a 129 | text-decoration: none 130 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) 131 | 132 | article h1, article h2, article h3, article h4, article h5, article h6, article p, article blockquote, article pre 133 | padding: 10px 20px 134 | 135 | article time, article .author, article .tags 136 | display: inline 137 | margin: 0 0 0 5px 138 | font-size: standard-font-size 139 | lozenge() 140 | 141 | .read-more:hover 142 | box-shadow: 0 0 2px #000 143 | 144 | .tags a 145 | text-decoration: none 146 | color: dark 147 | 148 | .tag-list h2, p 149 | margin: 10px 0 150 | 151 | .tag-list .posts 152 | display: none 153 | 154 | article time 155 | margin-left: 20px 156 | 157 | .read-more 158 | lozenge() 159 | 160 | nav#breadcrumb 161 | margin: 10px 0 20px 0 162 | clear: both 163 | 164 | .content 165 | width: 740px 166 | float: left 167 | clear: right 168 | 169 | nav.panel 170 | width: 200px 171 | float: left 172 | clear: left 173 | padding-bottom: 10px 174 | background-color: light 175 | border-radius 5px 176 | border-color: light 177 | emboss() 178 | margin: 0 20px 40px 0 179 | 180 | nav.panel p 181 | margin: 10px 10px 20px 10px 182 | 183 | nav.panel ul 184 | margin: 10px 0 185 | padding: 0 0 0 25px 186 | 187 | nav.panel h2 188 | background-color: highlight 189 | color: #fff 190 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5) 191 | padding: 3px 5px 192 | 193 | .prev_next 194 | margin: 0 0 10px 0 195 | clear: both 196 | float: left 197 | 198 | .prev_next .page, .prev_next .next 199 | padding: 3px 5px 200 | margin: 5px 201 | text-shadow: 0 1px 0 #fff 202 | 203 | footer 204 | clear: both 205 | width: 100% 206 | margin: 40px 0 0 0 207 | padding-top: 20px 208 | border-top: 3px solid #ced3d2 209 | 210 | footer p 211 | font-size: small-font-size 212 | text-align: right 213 | text-shadow: 0 1px 0 #fff 214 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pop 4 | 5 | 99 | 109 | 110 | 111 | 114 | 118 | 127 | 128 | 129 | 136 | 141 | 142 | 143 | 150 | 161 | 162 | 163 | 170 | 197 | 198 | 199 | 206 | 217 | 218 | 219 | 226 | 238 | 239 | 240 | 247 | 252 | 253 | 254 | 261 | 266 | 267 | 268 | 275 | 280 | 281 | 282 | 289 | 294 | 295 | 296 | 303 | 317 | 318 | 319 | 326 | 331 | 332 | 333 | 340 | 367 | 368 | 369 | 376 | 398 | 399 | 406 | 438 | 439 | 440 | 444 | 451 | 452 | 456 | 462 | 463 | 464 | 471 | 484 | 485 | 486 | 493 | 510 | 511 | 512 | 519 | 553 | 554 | 555 | 562 | 568 | 569 | 570 | 577 | 587 | 588 | 589 | 596 | 607 | 608 | 609 | 616 | 624 | 625 | 628 | 636 | 637 | 638 | 646 | 649 | 650 | 655 | 658 | 659 | 663 | 669 | 670 | 671 | 678 | 712 | 713 | 714 | 721 | 756 | 757 | 761 | 771 | 772 | 773 | 780 | 807 | 808 | 809 | 816 | 827 | 828 | 829 | 836 | 882 | 883 | 884 | 891 | 942 | 943 | 944 | 951 | 975 | 976 | 977 | 984 | 995 | 996 | 997 | 1006 | 1013 | 1014 | 1015 | 1024 | 1049 | 1050 | 1051 | 1058 | 1063 | 1064 | 1065 | 1072 | 1077 | 1078 | 1079 | 1086 | 1091 | 1092 | 1093 | 1100 | 1106 | 1107 | 1108 | 1115 | 1120 | 1121 | 1122 | 1129 | 1135 | 1136 | 1137 | 1144 | 1149 | 1150 | 1151 | 1158 | 1168 | 1169 | 1182 | 1185 | 1186 | 1193 | 1204 | 1205 | 1206 | 1211 | 1222 | 1223 | 1224 | 1231 | 1247 | 1248 | 1252 | 1261 | 1262 | 1263 | 1270 | 1278 | 1279 | 1280 | 1287 | 1294 | 1295 | 1296 | 1303 | 1380 | 1381 | 1385 | 1391 | 1392 | 1393 | 1397 | 1423 | 1424 | 1425 | 1430 | 1459 | 1460 | 1464 | 1481 | 1482 | 1483 | 1490 | 1520 | 1521 | 1522 | 1526 | 1542 | 1543 | 1544 | 1551 | 1571 | 1572 | 1573 | 1580 | 1606 | 1607 | 1608 | 1615 | 1623 | 1624 | 1625 | 1631 | 1707 | 1708 | 1709 | 1717 | 1723 | 1724 | 1725 | 1733 | 1761 | 1762 | 1763 | 1770 | 1775 | 1776 | 1777 | 1785 | 1803 | 1804 | 1805 | 1812 | 1819 | 1820 | 1821 | 1828 | 1836 | 1837 | 1838 | 1845 | 1852 | 1853 | 1854 | 1861 | 1894 | 1895 | 1896 | 1903 | 1941 | 1942 | 1943 | 1950 | 1972 | 1973 | 1974 | 1981 | 1986 | 1987 | 1988 | 1992 | 2031 | 2032 | 2033 | 2040 | 2078 | 2079 | 2080 | 2087 | 2162 | 2163 | 2164 | 2171 | 2187 | 2188 | 2189 | 2196 | 2209 | 2210 | 2211 | 2218 | 2227 | 2228 | 2229 | 2236 | 2273 | 2274 | 2275 | 2282 | 2287 | 2288 | 2289 | 2296 | 2304 | 2305 |

Pop

Pop is a static site and blog generator for Node.

112 | 113 |

cli_tools

lib/cli_tools.js
115 |

Module dependencies. 116 |

117 |
119 |
var yamlish = require('yamlish')
 120 |   , path = require('path')
 121 |   , fs = require('fs')
 122 |   , log = require(__dirname + '/log')
 123 |   , generators = require(__dirname + '/generators');
 124 | 
 125 | module.exports = {
126 |
130 |

Adds zero padding to single digit numbers.

131 | 132 |

133 | 134 |
  • param: Integer A number to pad

  • return: String

135 |
137 |
datePad: function(num) {
 138 |     return num.toString().length === 1 ? '0' + num : num;
 139 |   },
140 |
144 |

Makes a post file name (not URL) based on the config's permanlink format.

145 | 146 |

147 | 148 |
  • param: String Permalink format

  • param: Date Date for the file name

  • param: String The title for the post

  • param: String The file format (md or textile)

  • return: String

149 |
151 |
getPostFileName: function(pf, date, title, format) {
 152 |     title = encodeURI(title.toLowerCase().replace(/\s+/g, '-'));
 153 |     return '_posts/' + pf.replace(':year', date.getFullYear())
 154 |              .replace(':month', this.datePad(date.getMonth() + 1))
 155 |              .replace(':day', this.datePad(date.getDate()))
 156 |              .replace(/^\//, '')
 157 |              .replace(/\//g, '-')
 158 |              .replace(':title', title + '.' + format);
 159 |   },
160 |
164 |

Generates a stubbed post and writes it based on a file name.

165 | 166 |

167 | 168 |
  • param: Config A config object

  • param: String Site title

  • param: Function callback

  • return: String

169 |
171 |
makePost: function(config, title, fn) {
 172 |     // TODO: Get format and author from command-line options
 173 |     var meta = {
 174 |           layout: 'post'
 175 |         , title: title
 176 |         , author: process.env.LOGNAME || ''
 177 |         , tags: ['tag_1', 'tag_2']
 178 |         }
 179 |       , url = ''
 180 |       , format = 'md'
 181 |       , date = new Date()
 182 |       , frontMatter = ''
 183 |       , fileName = this.getPostFileName(config.permalink, date, title, format);
 184 | 
 185 |     frontMatter = '---\n' + yamlish.encode(meta).replace(/^\s+/mg, '') + '\n---\n';
 186 | 
 187 |     fs.writeFile(path.join(config.root, fileName), frontMatter, function(err) {
 188 |       if (err) {
 189 |         log.error('Error writing file:', fileName);
 190 |         throw(err);
 191 |       }
 192 |       log.info('Post created:', fileName);
 193 |       fn();
 194 |     });
 195 |   },
196 |
200 |

Default config settings, used by site generator.

201 | 202 |

203 | 204 |
  • return: String

205 |
207 |
defaultConfig: function() {
 208 |     return '' 
 209 |     + '{  "url": "http://example.com"\n'
 210 |     + ' , "title": "Example"\n'
 211 |     + ' , "permalink": "/:year/:month/:day/:title"\n'
 212 |     + ' , "perPage": 10\n'
 213 |     + ' , "exclude": ["\\\\.swp"]\n'
 214 |     + ' , "autoGenerate": [{"feed": "feed.xml", "rss": "feed.rss"}] }\n'
 215 |   },
216 |
220 |

Default index file, used by site generator.

221 | 222 |

223 | 224 |
  • return: String

225 |
227 |
defaultIndex: function() {
 228 |     return ''
 229 |     + '---\n'
 230 |     + 'layout: default\n'
 231 |     + 'title: My Site\n'
 232 |     + 'paginate: true\n'
 233 |     + '---\n\n'
 234 |     + '!{paginatedPosts()}\n'
 235 |     + '!{paginate}\n';
 236 |   },
237 |
241 |

Default layout, used by site generator.

242 | 243 |

244 | 245 |
  • return: String

246 |
248 |
defaultLayout: function() {
 249 |     return fs.readFileSync(__dirname + '/assets/default.jade');
 250 |   },
251 |
255 |

Default post layout, used by site generator.

256 | 257 |

258 | 259 |
  • return: String

260 |
262 |
defaultPostLayout: function() {
 263 |     return fs.readFileSync(__dirname + '/assets/post.jade');
 264 |   },
265 |
269 |

Default client-side JavaScript.

270 | 271 |

272 | 273 |
  • return: String

274 |
276 |
defaultClientJavaScript: function() {
 277 |     return fs.readFileSync(__dirname + '/assets/site.js');
 278 |   },
279 |
283 |

Default tags page.

284 | 285 |

286 | 287 |
  • return: String

288 |
290 |
defaultTags: function() {
 291 |     return fs.readFileSync(__dirname + '/assets/tags.jade');
 292 |   },
293 |
297 |

Sample post, to get people started.

298 | 299 |

300 | 301 |
  • return: String

302 |
304 |
samplePost: function() {
 305 |     return ''
 306 |     + '---\n'
 307 |     + 'title: Example Post About Something\n'
 308 |     + 'author: Pop\n'
 309 |     + 'layout: post\n'
 310 |     + 'tags:\n'
 311 |     + '- tag1\n'
 312 |     + '- tag2\n'
 313 |     + '---\n'
 314 |     + 'Pop is a static site generator.  It can be used to make blogs.  I hope you enjoy it!\n';
 315 |   },
316 |
320 |

Built-in stylesheet.

321 | 322 |

323 | 324 |
  • return: String

325 |
327 |
defaultStylus: function() {
 328 |     return fs.readFileSync(__dirname + '/assets/screen.styl');
 329 |   },
330 |
334 |

Site generator. Will not create a site if pathName exists.

335 | 336 |

337 | 338 |
  • param: String Site path name

  • param: Function Callback to run when finished

339 |
341 |
makeSite: function(args, fn) {
 342 |     var pathName
 343 |       , generator;
 344 | 
 345 |     if (args.length === 1) {
 346 |       pathName = args[0];
 347 |       generator = 'default';
 348 |     } else {
 349 |       pathName = args[1];
 350 |       generator = args[0];
 351 |     }
 352 | 
 353 |     if (generators.hasOwnProperty(generator)) {
 354 |       generators[generator].run(this, pathName, fn);
 355 |     } else {
 356 |       try {
 357 |         var pluginGenerator = require(generator);
 358 |       } catch (e) {
 359 |         log.error('Error: Unable to find the', generator, 'generator');
 360 |         return;
 361 |       }
 362 | 
 363 |       pluginGenerator.generator.run(this, pathName, fn);
 364 |     }
 365 |   },
366 |
370 |

Renders files that match pattern.

371 | 372 |

373 | 374 |
  • param: Object Site config

  • param: String File name pattern

  • param: Function Callback

375 |
377 |
renderFile: function(pop, config, pattern) {
 378 |     var fileMap = new pop.FileMap(config)
 379 |       , siteBuilder = new pop.SiteBuilder(config);
 380 |     
 381 |     fileMap.on('ready', function() {
 382 |       if (fileMap.files.length === 0) {
 383 |         fn();
 384 |       } else {
 385 |         siteBuilder.fileMap = fileMap;
 386 |         siteBuilder.build();
 387 |         siteBuilder.on('ready', function() {
 388 |           log.info('%d files rendered.', fileMap.files.length);
 389 |         });
 390 |       }
 391 |     });
 392 | 
 393 |     fileMap.search(pattern);
 394 |   }
 395 | };
 396 | 
397 |

config

lib/config.js
400 |

Config file reader.

401 | 402 |

403 | 404 |
  • param: String Config file name

  • return: Object Parsed JavaScript object

405 |
407 |
function readConfigFile(file) {
 408 |   var defaults = {
 409 |     perPage: 20
 410 |   , port: 4000
 411 |   , output: '_site/'
 412 |   };
 413 | 
 414 |   function applyDefaults(config) {
 415 |     for (var key in defaults) {
 416 |       config[key] = config[key] || defaults[key];
 417 |     }
 418 | 
 419 |     if (config.url) config.url = config.url.replace(/\/$/, '');
 420 | 
 421 |     return config;
 422 |   }
 423 | 
 424 |   try {
 425 |     var data = fs.readFileSync(file).toString();
 426 |     return applyDefaults(JSON.parse(data));
 427 |   } catch(exception) {
 428 |     if (exception.code === 'EBADF') {
 429 |       log.error('No _config.json file in this directory.  Is this a Pop site?');
 430 |       process.exit(1);
 431 |     } else {
 432 |       log.info('Error reading config:', exception.message);
 433 |       throw(exception);
 434 |     }
 435 |   }
 436 | }
437 |
441 |

Module dependencies and additional config variables. 442 |

443 |
445 |
var fs = require(__dirname + '/graceful')
 446 |   , log = require(__dirname + '/log');
 447 | 
 448 | module.exports = readConfigFile;
 449 | 
450 |

file_map

lib/file_map.js
453 |

Module dependencies. 454 |

455 |
457 |
var fs = require(__dirname + '/graceful')
 458 |   , path = require('path')
 459 |   , log = require(__dirname + '/pop').log  
 460 |   , EventEmitter = require('events').EventEmitter;
461 |
465 |

Initialize FileMap with a config object.

466 | 467 |

468 | 469 |
  • param: Object options

  • api: public

470 |
472 |
function FileMap(config) {
 473 |   this.config = config || {};
 474 |   this.config.exclude = config &amp;&amp; config.exclude ? config.exclude : [];
 475 |   this.config.exclude.push('/_site');
 476 |   this.ignoreDotFiles = true;
 477 |   this.root = config.root;
 478 |   this.files = [];
 479 |   this.events = new EventEmitter();
 480 |   this.filesLeft = 0;
 481 |   this.dirsLeft = 1;
 482 | }
483 |
487 |

Determines file type based on file extension.

488 | 489 |

490 | 491 |
  • param: String File name

  • return: String Internal file type used by SiteBuilder

492 |
494 |
FileMap.prototype.fileType = function(fileName) {
 495 |   var extension = path.extname(fileName).substring(1);
 496 | 
 497 |   if (fileName.match(/\/_posts\//)) {
 498 |     return 'post ' + extension;
 499 |   } else if (fileName.match(/\/_layouts\//)) {
 500 |     return 'layout ' + extension;
 501 |   } else if (fileName.match(/\/_includes\//)) {
 502 |     return 'include ' + extension;
 503 |   } else if (['jade', 'ejs', 'styl'].indexOf(extension) !== -1) {
 504 |     return 'file ' + extension;
 505 |   } else {
 506 |     return 'file';
 507 |   }
 508 | };
509 |
513 |

Recursively iterates from an initial path.

514 | 515 |

516 | 517 |
  • param: String Start path name

518 |
520 |
FileMap.prototype.walk = function(dir) {
 521 |   if (!dir) dir = this.root;
 522 | 
 523 |   var self = this;
 524 | 
 525 |   fs.readdir(dir, function(err, files) {
 526 |     self.dirsLeft--;
 527 |     if (!files) return;
 528 |     files.forEach(function(file) {
 529 |       file = path.join(dir, file);
 530 |       self.filesLeft++;
 531 |       fs.stat(file, function(err, stats) {
 532 |         if (err) log.error('Error:', err);
 533 |         if (!stats) return;
 534 |         if (stats.isDirectory(file)) {
 535 |           self.filesLeft--;
 536 |           self.dirsLeft++;
 537 |           self.walk(file);
 538 |           self.addFile(file, 'dir');
 539 |         } else {
 540 |           self.filesLeft--;
 541 |           self.addFile(file, self.fileType(file));
 542 |           if (self.filesLeft === 0 &amp;&amp; self.dirsLeft === 0) {
 543 |             process.nextTick(function() {
 544 |               self.events.emit('ready');
 545 |             });
 546 |           }
 547 |         }
 548 |       });
 549 |     });
 550 |   });
 551 | };
552 |
556 |

Searches for files that match pattern.

557 | 558 |

559 | 560 |
  • param: String A pattern to search for

561 |
563 |
FileMap.prototype.search = function(pattern) {
 564 |   this.searchPattern = pattern;
 565 |   this.walk();
 566 | };
567 |
571 |

Checks to see if a file name matches the excluded patterns.

572 | 573 |

574 | 575 |
  • param: String File name

  • return: Boolean Should the file be excluded?

576 |
578 |
FileMap.prototype.isExcludedFile = function(file) {
 579 |   if (this.ignoreDotFiles)
 580 |     if (file.match(/\/\./)) return true;
 581 | 
 582 |   return this.config.exclude.some(function(pattern) {
 583 |     return file.match(pattern);
 584 |   });
 585 | };
586 |
590 |

Determines file type based on file extension.

591 | 592 |

593 | 594 |
  • param: String File name

  • param: String File type

595 |
597 |
FileMap.prototype.addFile = function(file, type) {
 598 |   if (this.isExcludedFile(file)) return;
 599 |   if (this.searchPattern &amp;&amp; !file.match(this.searchPattern)) return;
 600 | 
 601 |   this.files.push({
 602 |     name: file,
 603 |     type: type,
 604 |   });
 605 | };
606 |
610 |

Bind an event to the internal EventEmitter.

611 | 612 |

613 | 614 |
  • param: String Event name

  • param: Function Handler

615 |
617 |
FileMap.prototype.on = function(eventName, fn) {
 618 |   this.events.on(eventName, fn);
 619 | };
 620 | 
 621 | module.exports = FileMap;
 622 | 
623 |

filters

lib/filters.js
626 |

module.exports = {

627 |
629 |
*
 630 |    * Replaces liquid tag highlight directives with prettyprint HTML tags.
 631 |    *
 632 |    * @param {String} The text for a post
 633 |    * @return {String}
 634 |    
635 |
639 |

highlight: function(data) { 640 | data = data.replace(/{% highlight ([^ ]*) %}/g, '<pre class="prettyprint lang-$1">'); 641 | data = data.replace(/{% endhighlight %}/g, '</pre>'); 642 | return data; 643 | } 644 | };

645 |
647 | 648 |

generators

lib/generators.js
651 |

module.exports = { 652 | 'default': require(__dirname + '/generators/default') 653 | };

654 |
656 | 657 |

graceful

lib/graceful.js
660 |

Module dependencies. 661 |

662 |
664 |
var fs = require('fs')
 665 |   , path = require('path')
 666 |   , defaultTimeout = 0
 667 |   , timeout = defaultTimeout;
668 |
672 |

Offers functionality similar to mkdir -p, but is async.

673 | 674 |

675 | 676 |
  • param: String Path name

  • param: Number File creation mode

  • param: Function Callback

  • param: Integer Path depth counter

677 |
679 |
function mkdir_p(dir, mode, callback, position) {
 680 |   mode = mode || process.umask();
 681 |   position = position || 0;
 682 |   parts = path.normalize(dir).split('/');
 683 | 
 684 |   if (position &gt;= parts.length) {
 685 |     if (callback) {
 686 |       return callback();
 687 |     } else {
 688 |       return true;
 689 |     }
 690 |   }
 691 | 
 692 |   var directory = parts.slice(0, position + 1).join('/') || '/';
 693 |   fs.stat(directory, function(err) {    
 694 |     if (err === null) {
 695 |       mkdir_p(dir, mode, callback, position + 1);
 696 |     } else {
 697 |       fs.mkdir(directory, mode, function(err) {
 698 |         if (err &amp;&amp; err.errno != 17) {
 699 |           if (callback) {
 700 |             return callback(err);
 701 |           } else {
 702 |             throw err;
 703 |           }
 704 |         } else {
 705 |           mkdir_p(dir, mode, callback, position + 1);
 706 |         }
 707 |       });
 708 |     }
 709 |   });
 710 | }
711 |
715 |

Polymorphic approach to fs.mkdir()

716 | 717 |

718 | 719 |
  • param: String Path name

  • param: Number File creation mode

  • param: Function Callback

720 |
722 |
fs.mkdir_p = function(dir, mode, callback) {
 723 |   mkdir_p(dir, mode, callback || process.noop);
 724 | }
 725 | 
 726 | // Graceful patching wraps async fs methods
 727 | Object.keys(fs)
 728 |   .forEach(function(i) {
 729 |     exports[i] = (typeof fs[i] !== 'function') ? fs[i]
 730 |                : (i.match(/^[A-Z]|^create|Sync$/)) ? function() {
 731 |                    return fs[i].apply(fs, arguments);
 732 |                  }
 733 |                : graceful(fs[i]);
 734 |   });
 735 | 
 736 | function graceful(fn) { return function GRACEFUL() {
 737 |   var args = Array.prototype.slice.call(arguments)
 738 |     , cb_ = args.pop();
 739 |   
 740 |   args.push(cb);
 741 | 
 742 |   function cb(er) {
 743 |     if (er &amp;&amp; er.message.match(/^EMFILE, Too many open files/)) {
 744 |       setTimeout(function() {
 745 |         GRACEFUL.apply(fs, args)
 746 |       }, timeout++);
 747 |       return;
 748 |     }
 749 |     timeout = defaultTimeout;
 750 |     cb_.apply(null, arguments);
 751 |   }
 752 |   fn.apply(fs, args)
 753 | }};
 754 | 
755 |

helpers

lib/helpers.js
758 |

Module dependencies and local variables. 759 |

760 |
762 |
var jade = require('jade')
 763 |   , fs = require('fs')
 764 |   , path = require('path')
 765 |   , cache = {}
 766 |   , _date = require('underscore.date')
 767 |   , helpers;
 768 | 
 769 | helpers = {
770 |
774 |

Pagination links.

775 | 776 |

777 | 778 |
  • param: Object Paginator object

  • return: String

779 |
781 |
paginate: function(paginator) {
 782 |     var template = '';
 783 |     template += '.pages\n';
 784 |     template += '  - if (paginator.previousPage)\n';
 785 |     template += '    span.prev_next\n';
 786 |     template += '      - if (paginator.previousPage === 1)\n';
 787 |     template += '        span ←\n';
 788 |     template += '        a.previous(href="/") Previous\n';
 789 |     template += '      - else\n';
 790 |     template += '        span ←\n';
 791 |     template += '        a.previous(href="/page" + paginator.previousPage + "/") Previous\n';
 792 |     template += '  - if (paginator.pages > 1)\n';
 793 |     template += '    span.prev_next\n';
 794 |     template += '      - for (var i = 1; i <= paginator.pages; i++)\n';
 795 |     template += '        - if (i === paginator.page)\n';
 796 |     template += '          strong.page #{i}\n';
 797 |     template += '        - else if (i !== 1)\n';
 798 |     template += '          a.page(href="/page" + i + "/") #{i}\n';
 799 |     template += '        - else\n';
 800 |     template += '          a.page(href="/") 1\n';
 801 |     template += '      - if (paginator.nextPage <= paginator.pages)\n';
 802 |     template += '        a.next(href="/page" + paginator.nextPage + "/") Next\n';
 803 |     template += '        span →\n';
 804 |     return jade.render(template, { locals: { paginator: paginator } });
 805 |   },
806 |
810 |

Generates paginated blog posts, suitable for use on an index page.

811 | 812 |

813 | 814 |
  • return: String

815 |
817 |
paginatedPosts: function() {
 818 |     var template
 819 |       , site = this;
 820 | 
 821 |     template = ''
 822 |       + '- for (var i = 0; i < paginator.items.length; i++)\n'
 823 |       + '  !{hNews(paginator.items[i], true)}\n';
 824 |     return jade.render(template, { locals: site.applyHelpers({ paginator: site.paginator }) });
 825 |   },
826 |
830 |

Atom Jade template.

831 | 832 |

833 | 834 |
  • param: String Feed URL

  • param: Integer Number of paragraphs to summarise

  • return: String

835 |
837 |
atom: function(feed, summarise) {
 838 |     var template = ''
 839 |       , url = this.config.url
 840 |       , title = this.config.title
 841 |       , perPage = this.config.perPage
 842 |       , posts = this.posts.slice(-perPage).reverse()
 843 |       , site = this;
 844 | 
 845 |     summarise = typeof summarise === 'boolean' &amp;&amp; summarise ? 3 : summarise;
 846 |     perPage = site.posts.length &lt; perPage ? site.posts.length : perPage;
 847 | 
 848 |     template += '!!!xml\n';
 849 |     template += 'feed(xmlns="http://www.w3.org/2005/Atom")\n';
 850 |     template += '  title #{title}\n';
 851 |     template += '  link(href=feed, rel="self")\n';
 852 |     template += '  link(href=url)\n';
 853 | 
 854 |     if (posts.length &gt; 0)
 855 |       template += '  updated #{dx(posts[0].date)}\n';
 856 | 
 857 |     template += '  id #{url}\n';
 858 |     template += '  author\n';
 859 |     template += '    name #{title}\n';
 860 |     template += '  - for (var i = 0, post = posts[i]; i < ' + perPage + '; i++, post = posts[i])\n';
 861 |     template += '    entry\n';
 862 |     template += '      title #{post.title}\n';
 863 |     template += '      link(href=url + post.url)\n';
 864 |     template += '      updated #{dx(post.date)}\n';
 865 |     template += '      id #{url.replace(/\\/$/, "")}#{post.url}\n';
 866 | 
 867 |     if (summarise)
 868 |       template += '      content(type="html") !{h(truncateParagraphs(post.content, summarise, ""))}\n';
 869 |     else
 870 |       template += '      content(type="html") !{h(post.content)}\n';
 871 | 
 872 |     return jade.render(template, { locals: site.applyHelpers({
 873 |         paginator: site.paginator
 874 |       , posts: posts
 875 |       , title: title
 876 |       , url: url
 877 |       , feed: feed
 878 |       , summarise: summarise
 879 |     })});
 880 |   },
881 |
885 |

RSS Jade template.

886 | 887 |

888 | 889 |
  • param: String Feed URL

  • param: Integer Number of paragraphs to summarise

  • param: String Optional description

  • return: String

890 |
892 |
rss: function(feed, summarise, description) {
 893 |     var template = ''
 894 |       , url = this.config.url
 895 |       , title = this.config.title
 896 |       , perPage = this.config.perPage
 897 |       , posts = this.posts.slice(-perPage).reverse()
 898 |       , site = this;
 899 | 
 900 |     description = description || title;
 901 |     summarise = typeof summarise === 'boolean' &amp;&amp; summarise ? 3 : summarise;
 902 |     perPage = site.posts.length &lt; perPage ? site.posts.length : perPage;
 903 | 
 904 |     template += '!!!xml\n';
 905 |     template += 'rss(version="2.0")\n';
 906 |     template += '  channel\n';
 907 |     template += '    title #{title}\n';
 908 |     template += '    link #{url}\n';
 909 |     template += '    description #{description}\n';
 910 | 
 911 |     // TODO: Site description, language, managingEditor, webMaster
 912 | 
 913 |     if (posts.length &gt; 0) {
 914 |       template += '    pubDate #{d822(posts[0].date)}\n';
 915 |       template += '    lastBuildDate #{d822(posts[0].date)}\n';
 916 |     }
 917 | 
 918 |     template += '    generator Pop\n';
 919 |     template += '    - for (var i = 0, post = posts[i]; i < ' + perPage + '; i++, post = posts[i])\n';
 920 |     template += '      item\n';
 921 |     template += '        title #{post.title}\n';
 922 |     template += '        link #{url + post.url}\n';
 923 |     template += '        pubDate #{d822(post.date)}\n';
 924 |     template += '        guid #{url.replace(/\\/$/, "")}#{post.url}\n';
 925 | 
 926 |     if (summarise)
 927 |       template += '        description !{h(truncateParagraphs(post.content, summarise, ""))}\n';
 928 |     else
 929 |       template += '        description !{h(post.content)}\n';
 930 | 
 931 |     return jade.render(template, { locals: site.applyHelpers({
 932 |         paginator: site.paginator
 933 |       , posts: posts
 934 |       , title: title
 935 |       , url: url
 936 |       , feed: feed
 937 |       , description: description
 938 |       , summarise: summarise
 939 |     })});
 940 |   },
941 |
945 |

Returns unique sorted tags for every post.

946 | 947 |

948 | 949 |
  • return: Array

950 |
952 |
allTags: function() {
 953 |     var allTags = [];
 954 | 
 955 |     for (var key in this.posts) {
 956 |       if (this.posts[key].tags) {
 957 |         for (var i = 0; i &lt; this.posts[key].tags.length; i++) {
 958 |           var tag = this.posts[key].tags[i];
 959 |           if (allTags.indexOf(tag) === -1) allTags.push(tag);
 960 |         }
 961 |       }
 962 |     }
 963 | 
 964 |     allTags.sort(function(a, b) {
 965 |       a = a.toLowerCase();
 966 |       b = b.toLowerCase();
 967 |       if (a &lt; b) return -1;
 968 |       if (a &gt; b) return 1;
 969 |       return 0;
 970 |     });
 971 | 
 972 |     return allTags;
 973 |   },
974 |
978 |

Get a set of posts for a tag.

979 | 980 |

981 | 982 |
  • param: String Tag name

  • return: Array

983 |
985 |
postsForTag: function(tag) {
 986 |     var posts = [];
 987 |     for (var key in this.posts) {
 988 |       if (this.posts[key].tags &amp;&amp; this.posts[key].tags.indexOf(tag) !== -1) {
 989 |         posts.push(this.posts[key]);
 990 |       }
 991 |     }
 992 |     return posts;
 993 |   },
994 |
998 |

Display a list of tags.

999 | 1000 |

TODO Link options

1001 | 1002 |

1003 | 1004 |
  • param: Array Tag names

  • return: String

1005 |
1007 |
tags: function(tags) {
1008 |     return tags.map(function(tag) {
1009 |       return '<a href="/tags.html#' + escape(tag) + '">' + tag + '</a>';
1010 |     }).join(', ');
1011 |   },
1012 |
1016 |

Renders a post using the hNews microformat, based on:

1017 | 1018 |

http://www.readability.com/publishers/guidelines/#view-exampleGuidelines

1019 | 1020 |

1021 | 1022 |
  • param: Object A post object

  • return: String Post in the hNews format

1023 |
1025 |
hNews: function(post, summary) {
1026 |     var template = '';
1027 |     template += 'article.hentry\n';
1028 |     template += '  header\n';
1029 |     template += '    h1.entry-title\n';
1030 |     template += '      a(href=post.url) !{post.title}\n';
1031 |     template += '    time.updated(datetime=dx(post.date), pubdate) #{ds(post.date)}\n';
1032 |     if (post.author)
1033 |       template += '    p.byline.author.vcard by <span class="fn">#{post.author}</span>\n';
1034 | 
1035 |     if (post.tags) template += '    div.tags !{tags(post.tags)}\n';
1036 | 
1037 |     if (summary) {
1038 |       if (post.summary) {
1039 |         template += '  !{post.summary + "<p><a class=\\"read-more\\" href=\\"' + post.url + '\\">Read More →</a></p>"}\n';
1040 |       } else {
1041 |         template += '  !{truncateParagraphs(post.content, 2, "<p><a class=\\"read-more\\" href=\\"' + post.url + '\\">Read More →</a></p>")}\n';
1042 |       }
1043 |     } else {
1044 |       template += '  !{post.content}\n';
1045 |     }
1046 |     return jade.render(template, { locals: this.applyHelpers({ post: post }) });
1047 |   },
1048 |
1052 |

Formats a date with date formatting rules according to underscore.date's rules.

1053 | 1054 |

1055 | 1056 |
  • param: Date Date to format

  • param: String Date format

  • return: String

1057 |
1059 |
df: function(date, format) {
1060 |     return _date(date).format(format);
1061 |   },
1062 |
1066 |

Short date (01 January 2001).

1067 | 1068 |

1069 | 1070 |
  • param: Date Date to format

  • return: String

1071 |
1073 |
ds: function(date) {
1074 |     return helpers.df(date, 'DD MMMM YYYY');
1075 |   },
1076 |
1080 |

Atom date formatting.

1081 | 1082 |

1083 | 1084 |
  • param: Date Date to format

  • return: String

1085 |
1087 |
dx: function(date) {
1088 |     return helpers.df(date, 'YYYY-MM-DDTHH:MM:ssZ');
1089 |   },
1090 |
1094 |

RFC-822 dates.

1095 | 1096 |

1097 | 1098 |
  • param: Date Date to format

  • return: String

1099 |
1101 |
d822: function(date) {
1102 |     // FIXME: why is _date always setting the timezone to local?
1103 |     return helpers.df(date, 'ddd, DD MMM YYYY HH:MM:ss') + ' GMT';
1104 |   },
1105 |
1109 |

Escapes brackets and ampersands.

1110 | 1111 |

1112 | 1113 |
  • param: String Text to escape

  • return: String

1114 |
1116 |
h: function(text) {
1117 |     return text &amp;&amp; text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
1118 |   },
1119 |
1123 |

Truncates HTML based on paragraph counts.

1124 | 1125 |

1126 | 1127 |
  • param: String Text to truncate

  • param: Integer Number of paragraphs

  • param: String Text to append when truncated

  • return: String

1128 |
1130 |
truncateParagraphs: function(text, length, moreText) {
1131 |     var t = text.split('</p>');
1132 |     return t.length &lt; length ? text : t.slice(0, length).join('</p>') + '</p>' + moreText;
1133 |   },
1134 |
1138 |

Truncates based on characters (not HTML safe, use with pre-formatted text).

1139 | 1140 |

1141 | 1142 |
  • param: String Text to truncate

  • param: Integer Length

  • param: String Text to append when truncated

  • return: String

1143 |
1145 |
truncate: function(text, length, moreText) {
1146 |     return text.length &gt; length ? text.slice(0, length).trim() + moreText : text;
1147 |   },
1148 |
1152 |

Truncates based on words (not HTML safe, use with pre-formatted text).

1153 | 1154 |

1155 | 1156 |
  • param: String Text to truncate

  • param: Integer Number of words

  • param: String Text to append when truncated

  • return: String

1157 |
1159 |
truncateWords: function(text, length, moreText) {
1160 |     var t = text.split(/\s/);
1161 |     return t.length &gt; length ? t.slice(0, length).join(' ') + moreText : text;
1162 |   }
1163 | };
1164 | 
1165 | module.exports = helpers;
1166 | 
1167 |

log

lib/log.js
1170 |

module.exports = { 1171 | enabled: true,

1172 | 1173 |

info: function() { 1174 | if (this.enabled) console.log.apply(this, arguments); 1175 | },

1176 | 1177 |

error: function() { 1178 | if (this.enabled) console.error.apply(this, arguments); 1179 | } 1180 | };

1181 |
1183 | 1184 |

paginator

lib/paginator.js
1187 |

Initialize Paginator with the number of items per-page and a list of items.

1188 | 1189 |

1190 | 1191 |
  • param: Integer Number of items per-page

  • param: Object A list of items (generally posts)

  • api: public

1192 |
1194 |
function Paginator(perPage, items) {
1195 |   this.allItems = this.sort(items);
1196 |   this.perPage = perPage;
1197 |   this.items = this.allItems.slice(0, perPage);
1198 |   this.previousPage = 0;
1199 |   this.nextPage = 2;
1200 |   this.page = 1;
1201 |   this.pages = Math.round(this.allItems.length / this.perPage) + 1;
1202 | }
1203 |
1207 |

Moves to the next page.

1208 | 1209 |

1210 |
1212 |
Paginator.prototype.advancePage = function() {
1213 |   this.page++;
1214 |   this.previousPage = this.page - 1;
1215 |   this.nextPage = this.page + 1;
1216 | 
1217 |   var start = (this.page - 1) * this.perPage
1218 |     , end = this.page * this.perPage;
1219 |   this.items = this.allItems.slice(start, end); 
1220 | };
1221 |
1225 |

Sort items according to date.

1226 | 1227 |

1228 | 1229 |
  • param: Array Array of items

  • return: Integer -1, 1, 0 according to the date comparison

1230 |
1232 |
Paginator.prototype.sort = function(items) {
1233 |   return items.sort(function(a, b) {
1234 |     a = a.date.valueOf();
1235 |     b = b.date.valueOf();
1236 |     if (a &gt; b)
1237 |       return -1;
1238 |     else if (a &lt; b)
1239 |       return 1;
1240 |     return 0;
1241 |   });
1242 | };
1243 | 
1244 | module.exports = Paginator;
1245 | 
1246 |

pop

lib/pop.js
1249 |

Module dependencies and local variables. 1250 |

1251 |
1253 |
var path = require('path')
1254 |   , fs = require(__dirname + '/../lib/graceful')
1255 |   , FileMap = require(__dirname + '/file_map')
1256 |   , SiteBuilder = require(__dirname + '/site_builder')
1257 |   , cliTools = require(__dirname + '/cli_tools')
1258 |   , readConfig = require(__dirname + '/config')
1259 |   , log = require(__dirname + '/log');
1260 |
1264 |

Loads the config script and sets the local variable.

1265 | 1266 |

1267 | 1268 |
  • return: Object Config object

1269 |
1271 |
function loadConfig() {
1272 |   var root = process.cwd()
1273 |     , config = readConfig(path.join(root, '_config.json'));
1274 |   config.root = root;
1275 |   return config;
1276 | }
1277 |
1281 |

Loads configuration then runs generateSite.

1282 | 1283 |

1284 | 1285 |
  • params: Boolean Use a HTTP sever?

1286 |
1288 |
function loadConfigAndGenerateSite(useServer, port) {
1289 |   var config = loadConfig();
1290 |   if (port) config.port = port;
1291 |   generateSite(config, useServer);
1292 | }
1293 |
1297 |

Runs FileMap and SiteBuilder based on the config.

1298 | 1299 |

1300 | 1301 |
  • params: Object Configuration options

  • params: Boolean Use a HTTP sever?

  • return: SiteBuilder A SiteBuilder instance

1302 |
1304 |
function generateSite(config, useServer) {
1305 |   var fileMap = new FileMap(config)
1306 |     , siteBuilder = new SiteBuilder(config)
1307 |     , server = require(__dirname + '/server')(siteBuilder);
1308 | 
1309 |   fileMap.walk();
1310 |   fileMap.on('ready', function() {
1311 |     siteBuilder.fileMap = fileMap;
1312 |     siteBuilder.build();
1313 |   });
1314 | 
1315 |   siteBuilder.once('ready', function() {
1316 |     if (useServer) {
1317 |       server.run();
1318 |       server.watch();
1319 |     }
1320 |   });
1321 | 
1322 |   return siteBuilder;
1323 | }
1324 | 
1325 | function build() {
1326 |   var args = process.argv.slice(2)
1327 |     , usage;
1328 |   if (args.length === 0) return loadConfigAndGenerateSite();
1329 | 
1330 |   usage  = 'pop is a static site builder.\n\n';
1331 |   usage += 'Usage: pop [command] [options]\n';
1332 |   usage += 'new    path           Generates a new site at path/\n';
1333 |   usage += 'post   "Post Title"   Writes a new post file\n'; 
1334 |   usage += 'render pattern        Renders files that match "pattern"\n'; 
1335 |   usage += 'server [port]          Create a server on port (default: 4000) for _site/\n\n';
1336 |   usage += '-v, --version         Display version and exit\n';
1337 |   usage += '-h, --help            Shows this message\n';
1338 | 
1339 |   while (args.length) {
1340 |     arg = args.shift();
1341 |     switch (arg) {
1342 |       case 'server':
1343 |         loadConfigAndGenerateSite(true, args.shift());
1344 |       break;
1345 |       case 'post':
1346 |         return cliTools.makePost(loadConfig(), args.shift(), function() { process.exit(0); });
1347 |       break;
1348 |       case 'new':
1349 |         return cliTools.makeSite(args, function() { process.exit(0); });
1350 |       break;
1351 |       case 'render':
1352 |         return cliTools.renderFile(module.exports, loadConfig(), args.shift());
1353 |       break;
1354 |       case '-v':
1355 |       case '--version':
1356 |         var version = JSON.parse(fs.readFileSync(__dirname + '/../package.json')).version;
1357 |         log.info('pop version:', version);
1358 |         process.exit(0);
1359 |       break;
1360 |       case '-h':
1361 |       case '--help':
1362 |         log.info(usage);
1363 |         process.exit(1);
1364 |       default:
1365 |         loadConfigAndGenerateSite();
1366 |     }
1367 |   }
1368 | }
1369 | 
1370 | module.exports = {
1371 |   build: build
1372 | , SiteBuilder: SiteBuilder
1373 | , FileMap: FileMap
1374 | , generateSite: generateSite
1375 | , cliTools: cliTools
1376 | , log: log
1377 | };
1378 | 
1379 |

server

lib/server.js
1382 |

Module dependencies and local variables. 1383 |

1384 |
1386 |
var path = require('path')
1387 |   , watch = require('nodewatch')
1388 |   , siteBuilder
1389 |   , log = require(__dirname + '/log');
1390 |
1394 |

Instantiates and runs the Express server. 1395 |

1396 |
1398 |
function server() {
1399 |   // TODO: Show require express error
1400 |   var express = require('express'),
1401 |       app = express.createServer();
1402 | 
1403 |   app.configure(function() {
1404 |     app.use(express.static(siteBuilder.outputRoot));
1405 |     app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
1406 |   });
1407 | 
1408 |   // Map missing trailing slashes for posts
1409 |   app.get('*', function(req, res) {
1410 |     var postPath = siteBuilder.outputRoot + req.url + '/';
1411 |     // TODO: Security
1412 |     if (req.url.match(/[^/]$/) &amp;&amp; path.existsSync(postPath)) {
1413 |       res.redirect(req.url + '/');
1414 |     } else {
1415 |       res.send('404');
1416 |     }
1417 |   });
1418 | 
1419 |   app.listen(siteBuilder.config.port);
1420 |   log.info('Listening on port', siteBuilder.config.port);
1421 | }
1422 |
1426 |

Watches for file changes and regenerates files as required. 1427 | ## TODO Work in progress 1428 |

1429 |
1431 |
function watchChanges() {
1432 |   function buildChange(file) {
1433 |     log.info('File changed:', file);
1434 |     try {
1435 |       siteBuilder.buildChange(file);
1436 |     } catch (e) {
1437 |       log.error('Error building site:', e);
1438 |     }
1439 |   }
1440 | 
1441 |   // TODO: What happens when files/dirs are added?
1442 |   siteBuilder.fileMap.files.forEach(function(file) {
1443 |     if (file.type === 'dir') {
1444 |       watch.add(file.name).onChange(buildChange);
1445 |     }
1446 |   });
1447 | }
1448 | 
1449 | module.exports = function(s) {
1450 |   siteBuilder = s;
1451 |   return {
1452 |     run: server
1453 |   , watch: watchChanges
1454 |   };
1455 | };
1456 | 
1457 | 
1458 |

site_builder

lib/site_builder.js
1461 |

Module dependencies. 1462 |

1463 |
1465 |
var textile = require('stextile')
1466 |   , fs = require('./graceful')
1467 |   , path = require('path')
1468 |   , jade = require('jade')
1469 |   , stylus = require('stylus')
1470 |   , yamlish = require('yamlish')
1471 |   , markdown = require('markdown-js')
1472 |   , Paginator = require('./paginator')
1473 |   , FileMap = require('./file_map.js').FileMap
1474 |   , EventEmitter = require('events').EventEmitter
1475 |   , filters = require('./filters')
1476 |   , helpers = require('./helpers')
1477 |   , userHelpers = {}
1478 |   , userFilters = {}
1479 |   , userPostFilters = {};
1480 |
1484 |

Initialize SiteBuilder with a config object and FileMap.

1485 | 1486 |

1487 | 1488 |
  • param: Object Config options

  • param: FileMap A FileMap object

  • api: public

1489 |
1491 |
function SiteBuilder(config, fileMap) {
1492 |   this.config = config;
1493 |   this.root = config.root;
1494 | 
1495 |   var helperFile = path.join(this.root, '_lib', 'helpers.js');
1496 |   if (path.existsSync(helperFile)) {
1497 |     userHelpers = require(helperFile);
1498 |   }
1499 | 
1500 |   var filterFile = path.join(this.root, '_lib', 'filters.js');
1501 |   if (path.existsSync(filterFile)) {
1502 |     userFilters = require(filterFile);
1503 |   }
1504 | 
1505 |   var postFilterFile = path.join(this.root, '_lib', 'post-filters.js');
1506 |   if (path.existsSync(postFilterFile)) {
1507 |     userPostFilters = require(postFilterFile);
1508 |   }
1509 | 
1510 |   this.outputRoot = config.output;
1511 |   this.fileMap = fileMap;
1512 |   this.posts = [];
1513 |   this.helpers = helpers;
1514 |   this.events = new EventEmitter();
1515 |   this.includes = {};
1516 | 
1517 |   this.loadPlugins();
1518 | }
1519 |
1523 |

Loads Pop plugins based on config.require. 1524 |

1525 |
1527 |
SiteBuilder.prototype.loadPlugins = function() {
1528 |   if (!this.config.require) return;
1529 |   var self = this;
1530 | 
1531 |   this.config.require.forEach(function(name) {
1532 |     try {
1533 |       var plugin = require(name);
1534 |       self.loadPlugin(name, plugin);
1535 |     } catch (e) {
1536 |       console.error('Unable to load plugin:', name, '-', e.message);
1537 |       throw(e);
1538 |     }
1539 |   });
1540 | };
1541 |
1545 |

Applies helpers and "user helpers" to an object so it can be easily passed to Jade.

1546 | 1547 |

1548 | 1549 |
  • param: String The name of the plugin

  • param: Object The plugin's module. Properties loaded: helpers, filters, postFilters

1550 |
1552 |
SiteBuilder.prototype.loadPlugin = function(name, plugin) {
1553 |   if (!plugin) return;
1554 | 
1555 |   for (key in plugin.helpers) {
1556 |     if (plugin.helpers.hasOwnProperty(key))
1557 |       userHelpers[key] = this.bind(plugin.helpers[key]);
1558 |   }
1559 | 
1560 |   for (key in plugin.filters) {
1561 |     if (plugin.filters.hasOwnProperty(key))
1562 |       userFilters[key] = this.bind(plugin.filters[key]);
1563 |   }
1564 | 
1565 |   for (key in plugin.postFilters) {
1566 |     if (plugin.postFilters.hasOwnProperty(key))
1567 |       userPostFilters[key] = this.bind(plugin.postFilters[key]);
1568 |   }
1569 | };
1570 |
1574 |

Applies helpers and "user helpers" to an object so it can be easily passed to Jade.

1575 | 1576 |

1577 | 1578 |
  • param: Object An object to merge with

  • return: Object The mutated object

1579 |
1581 |
SiteBuilder.prototype.applyHelpers = function(obj) {
1582 |   var self = this
1583 |     , key;
1584 | 
1585 |   for (key in this.helpers) {
1586 |     obj[key] = this.bind(this.helpers[key]);
1587 |   }
1588 | 
1589 |   for (key in userHelpers) {
1590 |     obj[key] = this.bind(userHelpers[key]);
1591 |   }
1592 | 
1593 |   obj.include = function(template) {
1594 |     return self.includes[template];
1595 |   };
1596 | 
1597 |   // TODO: Only do this once
1598 |   if (obj.paginate &amp;&amp; obj.paginator)
1599 |     obj.paginate = obj.paginate(obj.paginator);
1600 | 
1601 |   obj.site = self;
1602 | 
1603 |   return obj;
1604 | };
1605 |
1609 |

Binds methods to this SiteBuilder.

1610 | 1611 |

1612 | 1613 |
  • param: Function The function to bind

  • return: Function The bound function

1614 |
1616 |
SiteBuilder.prototype.bind = function(fn) {
1617 |   var self = this;
1618 |   return function() {
1619 |     return fn.apply(self, arguments);
1620 |   };
1621 | };
1622 |
1626 |

Builds the site. This is asynchronous, so various counters 1627 | and events are used to track progress.

1628 | 1629 |

1630 |
1632 |
SiteBuilder.prototype.build = function() {
1633 |   var self = this;
1634 | 
1635 |   function build() {
1636 |     var posts = self.findPosts()
1637 |       , otherFiles = self.otherRenderedFiles()
1638 |       , staticFiles = self.staticFiles()
1639 |       , autoGen = self.autoGenerate();
1640 | 
1641 |     function renderPost() {
1642 |       if (posts.length) {
1643 |         var file = posts.pop();
1644 |         self.renderPost(file, function() {
1645 |           self.events.emit('render post');
1646 |         });
1647 |       } else {
1648 |         self.events.emit('render autoGen');
1649 |       }
1650 |     }
1651 | 
1652 |     function renderAutoGen() {
1653 |       if (autoGen.length) {
1654 |         var file = autoGen.pop();
1655 |         self.autoGenerateFile(file, function() {
1656 |           self.events.emit('render autoGen');
1657 |         });
1658 |       } else {
1659 |         self.events.emit('check finished');
1660 |       }
1661 |     }
1662 | 
1663 |     function renderOtherFile() {
1664 |       if (otherFiles.length) {
1665 |         var file = otherFiles.pop();
1666 |         self.renderFile(file, function() {
1667 |           self.events.emit('render otherFile');
1668 |         });
1669 |       } else {
1670 |         self.events.emit('check finished');
1671 |       }
1672 |     }
1673 | 
1674 |     function renderStaticFile() {
1675 |       if (staticFiles.length) {
1676 |         var file = staticFiles.pop();
1677 |         self.copyStatic(file, function() {
1678 |           self.events.emit('render staticFile');
1679 |         });
1680 |       } else {
1681 |         self.events.emit('check finished');
1682 |       }
1683 |     }
1684 | 
1685 |     function checkFinished() {
1686 |       var filesLeft = posts.length + otherFiles.length + staticFiles.length + autoGen.length;
1687 |       if (filesLeft === 0) {
1688 |         self.events.emit('ready');
1689 |       }
1690 |     }
1691 | 
1692 |     self.events.on('render post', renderPost);
1693 |     self.events.on('render autoGen', renderAutoGen);
1694 |     self.events.on('render otherFile', renderOtherFile);
1695 |     self.events.on('render staticFile', renderStaticFile);
1696 |     self.events.on('check finished', checkFinished);
1697 | 
1698 |     self.events.emit('render post');
1699 |     self.events.emit('render otherFile');
1700 |     self.events.emit('render staticFile');
1701 |   }
1702 | 
1703 |   this.events.once('cached includes', build);
1704 |   this.cacheIncludes();
1705 | };
1706 |
1710 |

Returns any configured built-in pages, 1711 | or an empty array.

1712 | 1713 |

1714 | 1715 |
  • return: Array

1716 |
1718 |
SiteBuilder.prototype.autoGenerate = function() {
1719 |   if (!this.config.autoGenerate) return [];
1720 |   return this.config.autoGenerate;
1721 | };
1722 |
1726 |

Generates a built-in page. Only atom feeds and RSS 1727 | are currently available.

1728 | 1729 |

1730 | 1731 |
  • param: String Page/file details from the config object

  • param: Function A callback function to run on completion

1732 |
1734 |
SiteBuilder.prototype.autoGenerateFile = function(file, fn) {
1735 |   // TODO: Allow this to be easily extended
1736 |   var self = this;
1737 | 
1738 |   if (file.feed) {
1739 |     if (!this.config.url || !this.config.title) {
1740 |       console.error('Error: Built-in feed generation requires config values for: url and title.'); 
1741 |     } else {
1742 |       var layoutData = &quot;!{atom('" + this.config.url + '/' + file.feed + "')}&quot;
1743 |         , html = jade.render(layoutData, { locals: self.applyHelpers({ }) });
1744 |       this.write(this.outFileName(file.feed), html);
1745 |       fn();
1746 |     }
1747 |   }
1748 |   
1749 |   if (file.rss) {
1750 |     if (!this.config.url || !this.config.title) {
1751 |       console.error('Error: Built-in feed generation requires config values for: url and title.'); 
1752 |     } else {
1753 |       var layoutData = &quot;!{rss('" + this.config.url + '/' + file.rss + "')}&quot;
1754 |         , html = jade.render(layoutData, { locals: self.applyHelpers({ }) });
1755 |       this.write(this.outFileName(file.rss), html);
1756 |       fn();
1757 |     }
1758 |   }
1759 | };
1760 |
1764 |

Determines if a file needs Jade or Stylus rendering.

1765 | 1766 |

1767 | 1768 |
  • param: Object An instance that has a type property

  • return: Boolean

1769 |
1771 |
SiteBuilder.prototype.isRenderedFile = function(file) {
1772 |   return file.type === 'file jade' || file.type === 'file styl';
1773 | };
1774 |
1778 |

Builds a single file. 1779 | ## TODO Work in progress.

1780 | 1781 |

1782 | 1783 |
  • param: String File name to build

1784 |
1786 |
SiteBuilder.prototype.buildChange = function(file) {
1787 |   if (this.fileMap.isExcludedFile(file)) return;
1788 | 
1789 |   file = {
1790 |     type: this.fileMap.fileType(file)
1791 |   , name: file
1792 |   };
1793 | 
1794 |   if (file.type.indexOf('post') !== -1) {
1795 |     this.renderPost(file);
1796 |   } else if (this.isRenderedFile(file)) {
1797 |     this.renderFile(file);
1798 |   } else if (file.type === 'file') {
1799 |     this.copyStatic(file);
1800 |   }
1801 | };
1802 |
1806 |

Iterates over the files in the FileMap object to find posts.

1807 | 1808 |

1809 | 1810 |
  • return: Array A list of posts

1811 |
1813 |
SiteBuilder.prototype.findPosts = function() {
1814 |   return this.fileMap.files.filter(function(file) {
1815 |     return file.type.indexOf('post') !== -1;
1816 |   });
1817 | };
1818 |
1822 |

Iterates over the files in the FileMap object to find "other" rendered files.

1823 | 1824 |

1825 | 1826 |
  • return: Array A list of files

1827 |
1829 |
SiteBuilder.prototype.otherRenderedFiles = function() {
1830 |   var self = this;
1831 |   return this.fileMap.files.filter(function(file) {
1832 |     return self.isRenderedFile(file);
1833 |   });
1834 | };
1835 |
1839 |

Iterates over the files in the FileMap object to find static files that require copying.

1840 | 1841 |

1842 | 1843 |
  • return: Array A list of files

1844 |
1846 |
SiteBuilder.prototype.staticFiles = function() {
1847 |   return this.fileMap.files.filter(function(file) {
1848 |     return file.type === 'file';
1849 |   });
1850 | };
1851 |
1855 |

Copies a static file.

1856 | 1857 |

1858 | 1859 |
  • param: String The file name

  • param: Function A callback to run on completion

1860 |
1862 |
SiteBuilder.prototype.copyStatic = function(file, fn) {
1863 |   var outDir = this.outFileName(path.dirname(file.name.replace(this.root, '')))
1864 |     , fileName = path.join(outDir, path.basename(file.name))
1865 |     , self = this;
1866 | 
1867 |   // TODO: Use a configurable ignore list
1868 |   if (path.basename(file.name).match(/^_/)) return fn.apply(self);
1869 | 
1870 |   fs.mkdir_p(outDir, 0777, function(err) {
1871 |     if (err) {
1872 |       console.error('Error creating directory:', outDir);
1873 |       throw(err);
1874 |     }
1875 | 
1876 |     fs.readFile(file.name, function(err, data) {
1877 |       if (err) {
1878 |         console.error('Error reading file:', file.name);
1879 |         throw(err);
1880 |       }
1881 | 
1882 |       fs.writeFile(fileName, data, function(err) {
1883 |         if (err) {
1884 |           console.error('Error writing file:', fileName);
1885 |           throw(err);
1886 |         }
1887 | 
1888 |         if (fn) fn.apply(self);
1889 |       });
1890 |     });
1891 |   });
1892 | };
1893 |
1897 |

Parse YAML meta data for both files and posts.

1898 | 1899 |

1900 | 1901 |
  • param: String File name (used by logging on errors)

  • param: String Data to parse

  • return: Array Parsed YAML

1902 |
1904 |
SiteBuilder.prototype.parseMeta = function(file, data) {
1905 |   function clean(yaml) {
1906 |     for (var key in yaml) {
1907 |       if (typeof yaml[key] === 'string') {
1908 |         var m = yaml[key].match(/^"([^"]*)"$/);
1909 |         if (m) yaml[key] = m[1];
1910 |       }
1911 |     }
1912 |     return yaml;
1913 |   }
1914 | 
1915 |   // FIXME: This shouldn't be used, my articles are badly formatted
1916 |   function fix(text) {
1917 |     if (!text) return;
1918 |     return text.split('\n').map(function(line) {
1919 |       if (line.match(/^- /))
1920 |         line = '  ' + line;
1921 |       return line;
1922 |     }).join('\n');
1923 |   }
1924 | 
1925 |   var dataChunks = data.split('---')
1926 |     , parsedYAML = [];
1927 | 
1928 |   try {
1929 |     if (dataChunks[1]) {
1930 |       // TODO: Improve YAML extraction, add JSON alternative
1931 |       parsedYAML = clean(yamlish.decode(fix(dataChunks[1] || data)));
1932 |       return [parsedYAML, ((dataChunks || []).slice(2).join('---')).trim()];
1933 |     } else {
1934 |       return ['', data];
1935 |     }
1936 |   } catch (e) {
1937 |     console.error(&quot;Couldn't parse YAML in:", file, ':', e);
1938 |   }
1939 | };
1940 |
1944 |

Writes a file, making directories recursively when required.

1945 | 1946 |

1947 | 1948 |
  • param: String File name to write to

  • param: String Contents of the file

1949 |
1951 |
SiteBuilder.prototype.write = function(fileName, content) {
1952 |   if (!content) return console.error('No content for:', fileName);
1953 | 
1954 |   // Apply the post-filters before writing
1955 |   content = this.applyPostFilters(content);
1956 | 
1957 |   fs.mkdir_p(path.dirname(fileName), 0777, function(err) {
1958 |     if (err) {
1959 |       console.error('Error creating directory:', path.dirname(fileName));
1960 |       throw(err);
1961 |     }
1962 | 
1963 |     fs.writeFile(fileName, content, function(err) {
1964 |       if (err) {
1965 |         console.error('Error writing file:', fileName);
1966 |         throw(err);
1967 |       }
1968 |     });
1969 |   });
1970 | };
1971 |
1975 |

Returns a full path name.

1976 | 1977 |

1978 | 1979 |
  • param: String Relative path name, i.e., _posts/

  • param: String File name

  • return: String

1980 |
1982 |
SiteBuilder.prototype.outFileName = function(subDir, name) {
1983 |   return path.join(this.outputRoot, subDir, name);
1984 | };
1985 |
1989 |

Caches templates inside _includes/ 1990 |

1991 |
1993 |
SiteBuilder.prototype.cacheIncludes = function() {
1994 |   var self = this;
1995 | 
1996 |   function done() {
1997 |     self.events.emit('cached includes');
1998 |   }
1999 | 
2000 |   path.exists(path.join(this.root, '_includes'), function(exists) {
2001 |     if (!exists) {
2002 |       done();
2003 |     } else {
2004 |       fs.readdir(path.join(self.root, '_includes'), function(err, files) {
2005 |         if (!files) return;
2006 |         var file;
2007 | 
2008 |         if (files.length === 0) done();
2009 | 
2010 |         for (var i = 0; i &lt; files.length; i++) {
2011 |           file = files[i];
2012 |           // TODO: Configurable templates
2013 |           if (path.extname(file) !== '.jade') {
2014 |             if (i === files.length) return self.events.emit('cached includes');
2015 |           } else {
2016 |             fs.readFile(path.join(self.root, '_includes', file), 'utf8', function(err, data) {
2017 |               // TODO: This won't cope with _include/file/file, but people will expect this
2018 |               var html = jade.render(data, { locals: self.applyHelpers({}) })
2019 |                 , name = path.basename(file).replace(path.extname(file), '');
2020 |               self.includes[name] = html;
2021 | 
2022 |               if (i === files.length) done();
2023 |             });
2024 |           }
2025 |         }
2026 |       });
2027 |     }
2028 |   });
2029 | };
2030 |
2034 |

Renders a post using a template. Called by renderPost.

2035 | 2036 |

2037 | 2038 |
  • param: String Template file name

  • param: Object A post object

  • param: String The post's content

2039 |
2041 |
SiteBuilder.prototype.renderTemplate = function(templateFile, post, content) {
2042 |   var self = this;
2043 |   templateFile = path.join(this.root, '_layouts', templateFile + '.jade');
2044 | 
2045 |   fs.readFile(templateFile, 'utf8', function(err, data) {
2046 |     if (err) {
2047 |       console.error('Error in: ' + templateFile);
2048 |       console.error(err);
2049 |       console.error(err.message);
2050 |       throw(err);
2051 |     }
2052 | 
2053 |     try {
2054 |       var html = jade.render(data, { locals: self.applyHelpers({ post: post, content: content }) })
2055 |         , fileName = self.outFileName(post.fileName, 'index.html')
2056 |         , dirName = path.dirname(fileName);
2057 |     } catch (e) {
2058 |       console.error('Error rendering:', templateFile);
2059 |       throw(e);
2060 |     }
2061 | 
2062 |     path.exists(dirName, function(exists) {
2063 |       if (exists) {
2064 |         self.write(fileName, html);
2065 |       } else {
2066 |         fs.mkdir_p(dirName, 0777, function(err) {
2067 |           if (err) {
2068 |             console.error('Error making directory:', dirName);
2069 |             throw(err);
2070 |           }
2071 |           self.write(fileName, html);
2072 |         });
2073 |       }
2074 |     });
2075 |   });
2076 | };
2077 |
2081 |

Renders a generic Jade file and will supply pagination if required.

2082 | 2083 |

2084 | 2085 |
  • param: String File name

  • param: Function Callback to run on completion

2086 |
2088 |
SiteBuilder.prototype.renderFile = function(file, fn) {
2089 |   var self = this;
2090 | 
2091 |   if (file.type === 'file styl') {
2092 |     var outFileName = self.outFileName(
2093 |       path.dirname(file.name.replace(self.root, '')),
2094 |       path.basename(file.name).replace(path.extname(file.name), '.css')
2095 |     );
2096 | 
2097 |     return fs.readFile(file.name, 'utf8', function(err, fileData) {
2098 |       stylus.render(fileData, { filename: path.basename(outFileName) }, function(err, css) {
2099 |         if (err) throw err;
2100 |         self.write(outFileName, css);
2101 |         if (fn) fn.apply(self);
2102 |       });
2103 |     });
2104 |   }
2105 | 
2106 |   function render(fileData, meta, layoutData, dirName) {
2107 |     // Use .html for file extensions unless the file has the format file.ext.jade
2108 |     var ext = path.basename(file.name).match('\\.[^.]*\\' + path.extname(file.name) + '$') ? '' : '.html';
2109 |     dirName = dirName || '';
2110 | 
2111 |     var outFileName = self.outFileName(
2112 |       path.dirname(file.name.replace(self.root, '')) + dirName,
2113 |       path.basename(file.name).replace(path.extname(file.name), ext)
2114 |     );
2115 | 
2116 |     var fileContent = jade.render(fileData, {
2117 |       locals: self.applyHelpers({ paginator: self.paginator, page: meta }),
2118 |     });
2119 | 
2120 |     if (!layoutData) {
2121 |       self.write(outFileName, fileContent);
2122 |     } else {
2123 |       var html = jade.render(layoutData, { locals: self.applyHelpers({ content: fileContent }) });
2124 |       self.write(outFileName, html);
2125 |     }
2126 | 
2127 |     if (fn) fn.apply(self);
2128 |   }
2129 | 
2130 |   fs.readFile(file.name, 'utf8', function(err, fileData) {
2131 |     var meta = self.parseMeta(file.name, fileData)
2132 |       , fileContent = '';
2133 | 
2134 |     fileData = meta[1];
2135 |     meta = meta[0];
2136 | 
2137 |     if (!meta.layout) {
2138 |       self.paginator = new Paginator(self.config.perPage, self.posts);
2139 |       render(fileData, meta);
2140 |     } else {
2141 |       fs.readFile(path.join(self.root, '_layouts', meta.layout + '.jade'), function(err, layoutData) {
2142 |         if (err) {
2143 |           console.error('Unable to read layout:', meta.layout + '.jade');
2144 |           throw(err);
2145 |         }
2146 | 
2147 |         // TODO: Per page config
2148 |         self.paginator = new Paginator(self.config.perPage, self.posts)
2149 |         render(fileData, meta, layoutData);
2150 | 
2151 |         if (meta.paginate) {
2152 |           while (self.paginator.items.length) {
2153 |             self.paginator.advancePage();
2154 |             render(fileData, meta, layoutData, '/page' + self.paginator.page + '/');
2155 |           }
2156 |         }
2157 |       });
2158 |     }
2159 |   });
2160 | };
2161 |
2165 |

Parses a file name according to the permalink format.

2166 | 2167 |

2168 | 2169 |
  • param: String A post file name

  • return: Object An object containing the parsed file name

2170 |
2172 |
SiteBuilder.prototype.parseFileName = function(fileName) {
2173 |   var format = this.config.permalink
2174 |     , parts = fileName.match(/(\d+)-(\d+)-(\d+)-(.*)/)
2175 |     , year = parts[1]
2176 |     , month = parts[2]
2177 |     , day = parts[3]
2178 |     , title = parts[4].replace(/\.(textile|md)/, '');
2179 | 
2180 |   return { date: new Date(Date.UTC(year, month - 1, day)),
2181 |            fileName: format.replace(':year', year).
2182 |                        replace(':month', month).
2183 |                        replace(':day', day).
2184 |                        replace(':title', title) };
2185 | };
2186 |
2190 |

Applies internal and user-supplied content pre-filters.

2191 | 2192 |

2193 | 2194 |
  • param: String Text to transform using filters

  • return: String The transformed text

2195 |
2197 |
SiteBuilder.prototype.applyFilters = function(text) {
2198 |   for (var key in filters) {
2199 |     text = filters[key](text);
2200 |   }
2201 | 
2202 |   for (key in userFilters) {
2203 |     text = userFilters[key].apply(this, [text]);
2204 |   }
2205 | 
2206 |   return text;
2207 | };
2208 |
2212 |

Applies user-supplied content post-filters. These are run after HTML is generated.

2213 | 2214 |

2215 | 2216 |
  • param: String Text to transform using filters

  • return: String The transformed text

2217 |
2219 |
SiteBuilder.prototype.applyPostFilters = function(text) {
2220 |   for (key in userPostFilters) {
2221 |     text = userPostFilters[key].apply(this, [text]);
2222 |   }
2223 | 
2224 |   return text;
2225 | };
2226 |
2230 |

Renders a post.

2231 | 2232 |

2233 | 2234 |
  • param: String Post file name

  • param: Function A callback to run on completion

2235 |
2237 |
SiteBuilder.prototype.renderPost = function(file, fn) {
2238 |   var self = this
2239 |     , formatter;
2240 | 
2241 |   if (file.type.indexOf('textile') !== -1) {
2242 |     formatter = textile;
2243 |   } else if (file.type.indexOf('md') !== -1) {
2244 |     formatter = markdown.makeHtml;
2245 |   }
2246 | 
2247 |   fs.readFile(file.name, 'utf8', function(err, data) {
2248 |     var meta = self.parseMeta(file.name, data);
2249 |     data = meta[1];
2250 |     meta = meta[0];
2251 | 
2252 |     // Categories and tags are synonyms
2253 |     if (!meta.tags) meta.tags = meta.categories ? meta.categories : [];
2254 | 
2255 |     if (data &amp;&amp; meta) {
2256 |       var fileDetails = self.parseFileName(file.name);
2257 |       meta.fileName = fileDetails.fileName;
2258 |       meta.url = fileDetails.fileName;
2259 |       meta.date = fileDetails.date;
2260 |       meta.content = formatter(self.applyFilters(data));
2261 | 
2262 |       if (meta.summary)
2263 |         meta.summary = formatter(self.applyFilters(meta.summary));
2264 | 
2265 |       self.renderTemplate(meta.layout, meta, meta.content);
2266 |       self.posts.push(meta);
2267 |     }
2268 | 
2269 |     if (fn) fn.apply(self);
2270 |   });
2271 | };
2272 |
2276 |

Adds a listener to the internal EventEmitter object.

2277 | 2278 |

2279 | 2280 |
  • param: String The event name

  • param: Function The handler

2281 |
2283 |
SiteBuilder.prototype.on = function(eventName, fn) {
2284 |   this.events.on(eventName, fn);
2285 | };
2286 |
2290 |

Adds a listener to the internal EventEmitter object.

2291 | 2292 |

2293 | 2294 |
  • param: String The event name

  • param: Function The handler

2295 |
2297 |
SiteBuilder.prototype.once = function(eventName, fn) {
2298 |   this.events.once(eventName, fn);
2299 | };
2300 | 
2301 | module.exports = SiteBuilder;
2302 | 
2303 |
--------------------------------------------------------------------------------