├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── contributing.md ├── docs ├── helpers.md └── intro.md ├── lib ├── helpers.coffee └── index.coffee ├── package.json └── test ├── fixtures ├── basic │ ├── app.coffee │ ├── index.jade │ ├── package.json │ └── posts │ │ ├── _layout.jade │ │ ├── foo.jade.erb │ │ ├── locals_test.jade │ │ ├── nested │ │ ├── double-nested │ │ │ ├── double-nested1.jade │ │ │ └── double-nested2.jade │ │ ├── nested1.jade │ │ └── nested2.jade │ │ └── other_test.jade ├── json_keys │ ├── app.coffee │ ├── index.jade │ ├── package.json │ └── posts │ │ ├── _layout.jade │ │ ├── nested │ │ └── test2.jade │ │ └── test.jade └── write_json │ ├── app.coffee │ ├── index.jade │ ├── package.json │ └── posts │ ├── _layout.jade │ └── test.jade ├── mocha.opts └── test.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | after_script: 5 | - npm run coveralls 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | License (MIT) 2 | ------------- 3 | 4 | Copyright (c) 2013 Jeff Escalante, Carrot Creative 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mv lib src 3 | coffee -o lib -c src 4 | 5 | unbuild: 6 | rm -rf lib 7 | mv src lib 8 | 9 | publish: 10 | make build 11 | npm publish . 12 | make unbuild 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Roots Dynamic Content 2 | ===================== 3 | 4 | [](http://badge.fury.io/js/dynamic-content) [](https://travis-ci.org/carrot/roots-dynamic-content) [](https://coveralls.io/r/carrot/roots-dynamic-content) [](https://gemnasium.com/carrot/roots-dynamic-content) 5 | 6 | Dynamic content functionality for roots 7 | 8 | > **Note:** This project is in early development, and versioning is a little different. [Read this](http://markup.im/#q4_cRZ1Q) for more details. 9 | 10 | ### Installation 11 | 12 | - make sure you are in your roots project directory 13 | - `npm i dynamic-content -S` 14 | - modify your `app.coffee` file to include the extension, as such 15 | 16 | ```coffee 17 | dynamic_content = require 'dynamic-content' 18 | 19 | module.exports = 20 | extensions: [dynamic_content()] 21 | 22 | # everything else... 23 | ``` 24 | 25 | ### Usage 26 | 27 | Please see the [documentation](docs) for an overview of the functionality. 28 | 29 | ### License & Contributing 30 | 31 | - Details on the license [can be found here](LICENSE.md) 32 | - Details on running tests and contributing [can be found here](contributing.md) 33 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to roots-dynamic-content 2 | 3 | Hello there! First of all, thanks for being interested in roots-dynamic-content and helping out. We all think you are awesome, and by contributing to open source projects, you are making the world a better place. That being said, there are a few ways to make the process of contributing code to roots-dynamic-content smoother, detailed below: 4 | 5 | ### Filing Issues 6 | 7 | If you are opening an issue about a bug, make sure that you include clear steps for how we can reproduce the problem. _If we can't reproduce it, we can't fix it_. If you are suggesting a feature, make sure your explanation is clear and detailed. 8 | 9 | ### Getting Set Up 10 | 11 | - Clone the project down 12 | - Make sure [nodejs](http://nodejs.org) has been installed and is above version `0.10.x` 13 | - Run `npm install` 14 | - Put in work 15 | 16 | ### Testing 17 | 18 | This project is constantly evolving, and to ensure that things are secure and working for everyone, we need to have tests. If you are adding a new feature, please make sure to add a test for it. The test suite for this project uses [mocha](http://visionmedia.github.io/mocha/) and [should](https://github.com/visionmedia/should.js/)/ 19 | 20 | To run the test suite, make sure you have installed mocha (`npm install mocha -g`), then you can use the `npm test` or simply `mocha` command to run the tests. 21 | 22 | ### Code Style 23 | 24 | To keep a consistant coding style in the project, we're using [Polar Mobile's guide](https://github.com/polarmobile/coffeescript-style-guide), with one difference begin that much of this project uses `under_scores` rather than `camelCase` for variable naming. For any inline documentation in the code, we're using [JSDoc](http://usejsdoc.org/). 25 | 26 | ### Commit Cleanliness 27 | 28 | It's ok if you start out with a bunch of experimentation and your commit log isn't totally clean, but before any pull requests are accepted, we like to have a nice clean commit log. That means [well-written and clear commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) and commits that each do something significant, rather than being typo or bug fixes. 29 | 30 | If you submit a pull request that doesn't have a clean commit log, we will ask you to clean it up before we accept. This means being familiar with rebasing - if you are not, [this guide](https://help.github.com/articles/interactive-rebase) by github should help you to get started. And if you are still confused, feel free to ask! 31 | -------------------------------------------------------------------------------- /docs/helpers.md: -------------------------------------------------------------------------------- 1 | Dynamic Content Helper Functions 2 | ================================ 3 | 4 | You can load a set of helper functions from the extension if you want to read dynamic content from a directory, file, or string outside of the Roots build process. This is useful if you need to load dynamic content data and migrate it to another data format. 5 | 6 | ### Getting Started 7 | 8 | Require the helpers module from the extension: 9 | 10 | ```coffee 11 | helpers = require('dynamic-content').Helpers 12 | ``` 13 | 14 | > **Note:** These helpers use a couple bits of logic from the main extension to parse dynamic content files. **However**, these helpers do not add additional features provided through roots such as the `_url` key, nor is any content below the front matter compiled. 15 | 16 | ### helpers.readdir 17 | 18 | Takes a directory path string argument to the target directory. Returns a promise for an array of dynamic content objects. 19 | 20 | ```coffee 21 | helpers.readdir('project/blog_posts') 22 | .then (res) -> console.log(res) 23 | ``` 24 | 25 | > **Note:** This function is non-recursive and does not nest content like roots would. 26 | 27 | ### helpers.readFile 28 | 29 | Takes a file path string argument. Returns a promise for an object representing the file's dynamic content. Returns `false` if the file is detected to not be dynamic content. 30 | 31 | ```coffee 32 | helpers.readFile('project/blog_posts/welcome.jade') 33 | .then (res) -> console.log(res) 34 | ``` 35 | 36 | ### helpers.read 37 | 38 | Takes a string argument. This does not return a promise, it returns an object for the dynamic content, or `false` if the string is not formatted as dynamic content. 39 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | Roots Dynamic Content 2 | ===================== 3 | 4 | Dynamic content is an officially maintained extension of roots that adds a large chunk of additional functionality, allowing users to manage static content in a dynamic and powerful fashion. 5 | 6 | ### Getting Started 7 | 8 | Let's say you have a little blog you want posts for. But you also want an index page that lists all your posts. With a normal static compiler, there is no way to achieve this. Dynamic content makes things like this possible. 9 | 10 | Following the blog example, let's walk through how to make this happen. First, make a folder called 'posts', and drop your first post in there - let's make it a jade file. Now, you can add some [front matter](#) to the top of the file to make it dynamic. For example: 11 | 12 | ```jade 13 | --- 14 | title: 'Hello world!' 15 | date: 1/2/2014 16 | --- 17 | 18 | :markdown 19 | This is my **first blog post** yaaaa! 20 | ``` 21 | 22 | Roots will handle files with front matter differently. First, the file's front matter and contents will be made available on a local called `site` that's exposed in every other non-dynamic template. It will be scoped under the folder that it's in. For example, to pull it in to a blog index: 23 | 24 | ```jade 25 | each post in site.posts 26 | a.post 27 | h2= post.title 28 | date= post.date 29 | != post.content 30 | ``` 31 | 32 | So a couple things here. First, we can see how all dynamic files are scoped onto `site` under their folder name, and that we have access to the front matter variables (`title` and `date`), as well as one special property, `content`, which is the compiled content of the file (minus the front matter). If you add another post to the `posts` folder, it will also show up in the index. 33 | 34 | There's nothing special about the `posts` name, you can make a folder with any other name, put dynamic content in it, and it will be put on `sites` so you can iterate through it anywhere else. 35 | 36 | ### Deep Nesting 37 | 38 | So let's say you want to get a little more granular with your blog posts, and you have a couple different categories that you want: `news`, `hires`, and `doge`. You can just make folders for each of these categories and sort your blog posts into them. So your folder structure might look like this: 39 | 40 | ``` 41 | example-project 42 | ˾ posts 43 | ˾ news 44 | ˾ news_post.jade 45 | ˾ hires 46 | ˾ hires_post.jade 47 | ˾ doge 48 | ˾ wow.jade 49 | ``` 50 | 51 | So now we can access these posts in two different ways. First, if you just want to grab **all posts nested under `posts`**, you can do something like this: 52 | 53 | ```jade 54 | each post in site.posts.all() 55 | a.post 56 | h2= post.title 57 | date= post.date 58 | .category= post._categories[-1] 59 | != post.content 60 | ``` 61 | 62 | Two new things here -- first, calling the `all()` function on any category will recursively grab all dynamic content nested under it and return a flattened array. Second, you can see that each post also gets a special property called `_categories`, which is an array of the folder names that it's nested under, in order. Here, we can list the post's category by just grabbing the last index of this array. 63 | 64 | Second, we can explicitly drill down to each category. For example, if you wanted to display each category in a different place, you could do something like this: 65 | 66 | ```jade 67 | #news 68 | each post in site.posts.news 69 | ... 70 | 71 | #hires 72 | each post in site.posts.hires 73 | ... 74 | 75 | #doge 76 | each post in site.posts.doge 77 | ... 78 | ``` 79 | 80 | If you are nesting any deeper, you also have an `all()` function on each of these categories. Everything is handled in a recursive fashion, so you can get as crazy as you want with this (although honestly I wouldn't recommend more than 2 levels deep). 81 | 82 | If you are sharp, you may have noticed that although we don't have "single post views" at the moment, the post contents (minus front matter) and still being written to public under their respective folders. To prevent these writes, we can add a special key to the front matter as such: 83 | 84 | ```jade 85 | --- 86 | title: 'Hello world!' 87 | date: 1/2/2014 88 | _render: false 89 | --- 90 | 91 | :markdown 92 | This is my **first blog post** yaaaa! 93 | ``` 94 | 95 | This will prevent the file from rendering and writing, since we don't need a single view here and are simply using the `post.content` property to render out the content in the index. 96 | 97 | ### Single Post Views 98 | 99 | Ok, so this is looking good so far, but what happens when you want to click into a blog post rather than just displaying it on the index? For this, we need a url and a layout. We can solve this quickly using jade layouts and the special `post._url` property. In the body of the file, rather than the single markdown line, we'll also include the `extends` and `block` directives, remove the `content` print on the index page, and add a link to the title pointing to `post._url`. Let's take a look, first at the modified post and then a sample layout for the post. 100 | 101 | ```jade 102 | --- 103 | title: 'Hello world!' 104 | date: 1/2/2014 105 | _content: false 106 | --- 107 | 108 | extends single_post_layout 109 | 110 | block content 111 | :markdown 112 | This is my **first blog post** yaaaa! 113 | ``` 114 | 115 | And updates to the index page: 116 | 117 | ```jade 118 | each post in site.posts.all() 119 | a.post 120 | h2: a(href=post._url)= post.title 121 | date= post.date 122 | ``` 123 | 124 | Also a sample layout for `single_post_layout.jade` 125 | 126 | ```jade 127 | body 128 | block content 129 | 130 | a(href='/') « back to index 131 | ``` 132 | Note that for the `extends` statement, the path is going to be relative. 133 | 134 | Also note that in the index page, we have removed `post.content`, as the content is going on it's own page, and would now also be surrounded by the layout. If you want a "preview" or "snippet" of the blog post, you should add that directly to the front matter. Taking a slice of the markup is usually a bad idea anyway, as you can slice in the middle of an open tag and it will mess up the rest of your page (for example, if you cut your snippet off in the middle of a bold tag before it closes, the rest of your page will be bold). For blog post snippets, you want to go with just plain text, which is better suited to the front matter than the body of the file. 135 | 136 | Now when we hit the url, we'll see the post rendered into a layout as it should be. You'll see the special property `post._url` as mentioned earlier, which simply prints a path to the post, including the deep-nesting if present. You might have also noticed the special `_content` key being set to false in the post file. This will just remove `post.content`, since we are no longer using it, and the full post contents with the full layout times a bunch of posts can be a very lengthy unused value. While you certainly can get away with not including this special key, if you are not planning on using `post.contents` on your index page, it's recommended that you just cut it for cleanliness and speed. 137 | 138 | ### Exporting Dynamic Content as JSON 139 | 140 | For some use cases, you may want to have your dynamic content available to be accessed by javascript in reaction to a user's action. For example, you might want to load your first five blog posts onto the index, but wait until the user hits "next page" to load in the rest. There are two ways you can do this. 141 | 142 | First, you can just stringify the `site` object into a script tag, and use it from there. It would look something like this, in jade: 143 | 144 | ```jade 145 | p here's my great page 146 | script!= "window.dynamic_content = " + JSON.stringify(site) 147 | ``` 148 | 149 | In other situations, this might not make the cut -- for example if you are trying to access other dynamic content from within a single piece of dynamic content, all of the other ones might not be finished rendering when that particular one renders, so you can't guarantee they will all be present. For use cases like this, you can simply have this extension export all dynamic content to a JSON file that you can then pull with javascript. To do this, just pass a `write` key to the extension's initialization with the value being a path you want to write the json to (written relative to the public folder). So, for example: 150 | 151 | ``` 152 | extensions: [ 153 | dynamic_content(write: 'content.json') 154 | ] 155 | ``` 156 | 157 | This would write all your dynamic content to `public/content.json` whenever the project compiles. 158 | 159 | It should also be noted that content is slightly reformatted when written as json to maintain nesting correctly. So if you have two folders nested inside of each other, each with a few items inside them, the json output might look like this: 160 | 161 | ```json 162 | { 163 | "posts": { 164 | "items": [ 165 | { 166 | "title": "test", 167 | "_url":"/posts/test.html", 168 | "content":"
wow
" 169 | }, { 170 | "title": "second test", 171 | "_url":"/posts/test2.html", 172 | "content":"amaze
" 173 | } 174 | ], 175 | "nested_posts": { 176 | "items": ["..."] 177 | } 178 | } 179 | } 180 | ``` 181 | 182 | So you can see here that each nesting level is accessed by name within the previous level, and the items for each level can be accessed through the `items` key, which is always an array of items at that level of nesting, if there are any present. 183 | 184 | There are two more features to writing json to be discussed. First, you can specify which folders you want to be written, even if they are deep nested, and second, you can write multiple json files for different folders. If you pass an object to the `write` key instead of a string, you can get multiple outputs by key, as such: 185 | 186 | ```coffee 187 | extensions: [dynamic(write: { 'posts.json': 'posts', 'press.json': 'press' })] 188 | ``` 189 | 190 | This config would write two different files, one for the `posts` folder as `posts.json` and one for the `press` folder as `press.json`. You can get even more specific than this, by drilling down to nested groups. For example: 191 | 192 | ```coffee 193 | extensions: [dynamic(write: { 'welcomes.json': 'posts/welcome' })] 194 | ``` 195 | 196 | This would write a `welcomes.json` file with just the contents of the `welcome` folder nested inside the `posts` folder. You can nest as deep as you need, just separate by a slash. 197 | 198 | ### For non-english text writer 199 | 200 | If you write non-english text content and save it by Notepad, it might not appear at all in your blog or website. There are some reasons. Firstly, Notepad always add BOM when you save as UTF-8. Secondly, Roots-dynamic-content can not handle UTF-8 with BOM. Therefore, you have to use another text editor(eg.Notepad2,mEditor,EmEditor,FooEditor etc...) to save as UTF-8 without BOM. 201 | -------------------------------------------------------------------------------- /lib/helpers.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | W = require 'when' 4 | nodefn = require 'when/node' 5 | yaml = require 'js-yaml' 6 | _ = require 'lodash' 7 | 8 | BR = "(?:\\\r\\\n|\\\n|\\\r)" # cross-platform newline 9 | LINEBREAK_REGEXP = new RegExp(BR) 10 | FRONTMATTER_REGEXP = new RegExp(///^---\s*#{BR}([\s\S]*?)#{BR}?---\s*#{BR}?///) 11 | 12 | ###* 13 | * Read the first three bytes of each file, if they are '---', assume 14 | * that we're working with dynamic content. 15 | * 16 | * @private 17 | * 18 | * @param {File} file - vinyl-wrapped file instance 19 | * @return {Boolean} promise returning true or false 20 | ### 21 | 22 | detect = (str) -> 23 | if str.split(LINEBREAK_REGEXP)[0] == '---' then true else false 24 | 25 | detect_file = (path) -> 26 | deferred = W.defer() 27 | res = false 28 | 29 | fs.createReadStream(path, encoding: 'utf-8', start: 0, end: 3) 30 | .on('error', deferred.reject) 31 | .on('end', -> deferred.resolve(res)) 32 | .on 'data', (data) -> if detect(data) then res = true 33 | 34 | return deferred.promise 35 | 36 | read = (str) -> 37 | if not detect(str) then return false 38 | front_matter_str = str.match(FRONTMATTER_REGEXP) 39 | data = yaml.safeLoad(front_matter_str[1]) 40 | data.content = str.replace(front_matter_str[0], '') 41 | return data 42 | 43 | readFile = (path) -> 44 | nodefn.call(fs.stat, path) 45 | .then (res) -> 46 | if res.isDirectory() then return false 47 | detect_file(path).then (res) -> 48 | if not res then return false 49 | nodefn.call(fs.readFile, path, 'utf8').then(read) 50 | 51 | readdir = (dir) -> 52 | nodefn.call(fs.readdir, dir) 53 | .then (paths) -> W.map(paths, (p) -> readFile(path.join(dir, p))) 54 | .then _.compact 55 | 56 | module.exports = 57 | read: read 58 | readFile: readFile 59 | readdir: readdir 60 | detect_file: detect_file 61 | -------------------------------------------------------------------------------- /lib/index.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | fs = require 'fs' 3 | _ = require 'lodash' 4 | helpers = require './helpers' 5 | 6 | module.exports = (opts = {}) -> 7 | 8 | class DynamicContent 9 | constructor: -> 10 | @category = 'dynamic' 11 | @all_content = [] 12 | 13 | fs: -> 14 | extract: true 15 | ordered: true 16 | detect: (f) -> helpers.detect_file(f.path) 17 | 18 | compile_hooks: -> 19 | before_pass: before_hook.bind(@) 20 | after_file: after_hook.bind(@) 21 | write: write_hook.bind(@) 22 | 23 | category_hooks: -> 24 | after: after_category.bind(@) 25 | 26 | ###* 27 | * For dynamic files before the last compile pass: 28 | * - remove the front matter, parse into an object 29 | * - add the object to the locals, nesting as deep as the folder it's in 30 | * - add an "all" utility function to each level 31 | * 32 | * @private 33 | * 34 | * @param {Object} ctx - roots context 35 | ### 36 | 37 | before_hook = (ctx) -> 38 | # if last pass 39 | if ctx.index is ctx.file.adapters.length 40 | f = ctx.file 41 | roots = f.roots 42 | 43 | data = helpers.read(ctx.content) 44 | front_matter = _.omit(data, 'content') 45 | ctx.content = data.content 46 | 47 | # get categories and per-compile locals, add or define site key 48 | folders = path.dirname(f.file.relative).split(path.sep) 49 | locals = f.compile_options.site ?= {} 50 | file_locals = f.file_options 51 | 52 | # add special keys for url and categories 53 | front_matter._categories = folders 54 | front_matter._url = roots.config.out(f.file, ctx.adapter.output) 55 | .replace(roots.config.output_path(), '') 56 | .replace(new RegExp('\\' + path.sep, 'g'), '/') 57 | 58 | # deep nested dynamic content 59 | # - make sure the backtraced path to a deep-nested folder exists 60 | # - push the front matter to the folder name array/object 61 | # - add special 'all' function to the array/object 62 | # - save pointer to the front matter obj under file-specific post local 63 | for f, i in folders 64 | locals[f] ?= [] 65 | locals = locals[f] 66 | if i is folders.length - 1 67 | locals.push(front_matter) 68 | @all_content.push(front_matter) 69 | locals.all = all_fn 70 | file_locals.post = locals[locals.length - 1] 71 | 72 | ###* 73 | * After a file in the category has been compiled, grabs the content and 74 | * adds it to the locals object unless _content key is false 75 | * 76 | * @private 77 | * 78 | * @param {Object} ctx - roots context 79 | * @return {Boolean} 80 | ### 81 | 82 | after_hook = (ctx) -> 83 | locals = ctx.file_options.post 84 | locals.content = ctx.content unless locals._content is false 85 | 86 | ###* 87 | * After the category has finished, if the user has chosen to have all 88 | * dynamic content written as json, make that happen. This can only happen 89 | * if a `write` option has been provided with an `output` key. Also can 90 | * optionally contain `flattened` and `keys` keys, which reformat or filter 91 | * the data in a certain way. 92 | * 93 | * @param {Object} ctx - roots context 94 | ### 95 | after_category = (ctx) -> 96 | opt = opts.write 97 | 98 | if opt 99 | content = reformat(@all_content) 100 | if typeof opt is 'string' then return write_json(ctx, opt, content) 101 | write_json(ctx, k, filter(content, v)) for k, v of opt 102 | 103 | ###* 104 | * If a dynamic file has `_render` set to false in the locals, don't write 105 | * the file. Otherwise write as usual. 106 | * 107 | * @param {Object} ctx - roots context 108 | * @return {Boolean} whether or not to write the file as usual 109 | ### 110 | 111 | write_hook = (ctx) -> 112 | ctx.file_options.post._render isnt false 113 | 114 | ###* 115 | * Returns an array of all the dynamic content object in the folder 116 | * it was called on, as well as every folder nested under it, flattened 117 | * into a single array. 118 | * 119 | * @private 120 | * 121 | * @return {Array} Array of dynamic content objects 122 | ### 123 | 124 | all_fn = -> 125 | values = [] 126 | recurse = (obj) -> 127 | for o in Object.keys(obj) 128 | if not isNaN(parseInt(o)) then values.push(obj[o]); continue 129 | recurse(obj[o]) 130 | recurse(this) 131 | values 132 | 133 | module.exports.Helpers = helpers 134 | 135 | # private 136 | 137 | reformat = (data) -> 138 | output = {} 139 | for item in data 140 | pointer = output 141 | for level in item._categories 142 | pointer[level] ?= {} 143 | pointer[level]['items'] ?= [] 144 | pointer = pointer[level] 145 | delete item._categories 146 | pointer.items.push(item) 147 | return output 148 | 149 | filter = (data, keys) -> 150 | keys = keys.split('/') 151 | pointer = data 152 | for key in keys 153 | pointer = pointer[key] 154 | return pointer 155 | 156 | write_json = (ctx, relative_path, content) -> 157 | destination = path.join(ctx.roots.config.output_path(), relative_path) 158 | fs.writeFileSync(destination, JSON.stringify(content)) 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamic-content", 3 | "version": "0.3.0", 4 | "author": "Carrot Creativeanother test
") 66 | 67 | it 'should skip content then _content: false', -> 68 | x = _.find(@json, {title: 'foo'}) 69 | y = y = _.find(@json, {title: 'nested 1'}) 70 | 71 | should.not.exist(x.content) 72 | should.exist(y.content) 73 | 74 | it 'should add _categories to posts', -> 75 | x = _.find(@json, {title: 'foo'}) 76 | y = _.find(@json, {title: 'nested 1'}) 77 | z = _.find(@json, {title: 'double-nested 1'}) 78 | 79 | x._categories.length.should.eql 1 80 | x._categories[0].should.eql 'posts' 81 | 82 | y._categories.length.should.eql 2 83 | y._categories[0].should.eql 'posts' 84 | y._categories[1].should.eql 'nested' 85 | 86 | z._categories.length.should.eql 3 87 | z._categories[0].should.eql 'posts' 88 | z._categories[1].should.eql 'nested' 89 | z._categories[2].should.eql 'double-nested' 90 | 91 | it 'should make all front matter available as locals', -> 92 | p = path.join(_path, @public, 'posts/locals_test.html') 93 | content = JSON.parse(fs.readFileSync(p, 'utf8')) 94 | content.post.wow.should.eql('amaze') 95 | 96 | describe 'write_json', -> 97 | 98 | before (done) -> compile_fixture.call(@, 'write_json', -> done()) 99 | 100 | it 'should write content to json file if specified', (done) -> 101 | p = path.join(@public, 'content.json') 102 | h.file.exists(p).should.be.ok 103 | h.file.contains_match(p, /another test/).should.be.ok 104 | h.file.contains_match(p, /_categories/).should.not.be.ok 105 | done() 106 | 107 | describe 'json_keys', -> 108 | 109 | before (done) -> compile_fixture.call(@, 'json_keys', -> done()) 110 | 111 | it 'should write content to json file if specified', (done) -> 112 | p = path.join(@public, 'content.json') 113 | h.file.exists(p).should.be.ok 114 | h.file.contains_match(p, /another test/).should.not.be.ok 115 | h.file.contains_match(p, /nested test/).should.be.ok 116 | done() 117 | 118 | describe 'helpers', -> 119 | describe 'readdir', -> 120 | it 'should read dynamic content files in the directory', (done) -> 121 | helpers.readdir(path.join(_path, 'basic', 'posts')).then (res) -> 122 | test = _.find res, (e) -> e.title == 'foo' 123 | test.foo.should.eql 'bar' 124 | test.content.should.eql 'extends _layout\n\nblock content\n p this is a test\n' 125 | done() 126 | 127 | describe 'readFile', -> 128 | it 'should read dynamic content from a single file', (done) -> 129 | helpers.readFile(path.join(_path, 'basic', 'posts', 'locals_test.jade')) 130 | .then (res) -> 131 | res.wow.should.eql 'amaze' 132 | done() 133 | 134 | describe 'read', -> 135 | it 'should read dynamic content from a string', -> 136 | test = "---\ntest: 'foo'\n---\nsweet content\n" 137 | res = helpers.read(test) 138 | res.test.should.eql 'foo' 139 | res.content.should.eql 'sweet content\n' 140 | 141 | it 'should read front matter from a string without content and no newline at the end with windows style line endings', -> 142 | test = "---\r\ntest: 'foo'\r\n---" 143 | res = helpers.read(test) 144 | res.test.should.eql 'foo' 145 | res.content.should.eql '' 146 | 147 | it 'should read dynamic content regardless of newline style', -> 148 | test = "---\ntest: 'foo'\n---\nsweet content\n" 149 | res = helpers.read(test) 150 | res.test.should.eql 'foo' 151 | res.content.should.eql 'sweet content\n' 152 | 153 | test = "---\r\ntest: 'foo'\r\n---\r\nsweet content\r\n" 154 | res = helpers.read(test) 155 | res.test.should.eql 'foo' 156 | res.content.should.eql 'sweet content\r\n' 157 | 158 | test = "---\rtest: 'foo'\r---\rsweet content\r" 159 | res = helpers.read(test) 160 | res.test.should.eql 'foo' 161 | res.content.should.eql 'sweet content\r' 162 | 163 | it "should return false if it's not formatted as dynamic content", -> 164 | test = "this ain't dynamic content doge" 165 | res = helpers.read(test).should.eql false 166 | --------------------------------------------------------------------------------