├── .gitignore ├── .jshintrc ├── .travis.yml ├── appveyor.yml ├── example-bench.js ├── example-simple.js ├── example-stream.js ├── example.js ├── gulpfile.js ├── index.js ├── lib ├── Compiler.js ├── cache.js ├── config.js ├── core │ ├── path.js │ └── util.js ├── crossbow.js ├── errors.js ├── file-helper.js ├── file.js ├── filter.js ├── hooks.js ├── layouts.js ├── logger.js ├── paginator.js ├── plugins │ ├── code-fences.js │ ├── handlebars.js │ ├── handlebars │ │ ├── $_.js │ │ ├── $md.js │ │ ├── clean-dump.js │ │ ├── compile.js │ │ ├── current.js │ │ ├── data.js │ │ ├── dump.js │ │ ├── filters.js │ │ ├── hash.js │ │ ├── helpers.js │ │ ├── hl.js │ │ ├── if_eq.js │ │ ├── if_not_eq.js │ │ ├── imgs.js │ │ ├── inc.js │ │ ├── loop.js │ │ ├── md.js │ │ ├── save.js │ │ ├── sections.js │ │ ├── sep.js │ │ └── yield.js │ └── markdown.js ├── public │ ├── add.js │ ├── addPage.js │ ├── addPartial.js │ ├── addPost.js │ ├── compile.js │ ├── compileAll.js │ ├── compileMany.js │ ├── error.js │ ├── freeze.js │ ├── getErrorString.js │ ├── getType.js │ ├── mergeData.js │ ├── parseContent.js │ ├── preProcess.js │ └── transform.js ├── url.js ├── utils.js └── yaml.js ├── package.json ├── plugins └── stream.js ├── readme.md └── test ├── fixtures ├── _config-corrupt.json ├── _config-corrupt.yml ├── _config.json ├── _config.yml ├── _includes │ ├── button.html │ ├── context.html │ ├── heading.hbs │ └── markdown.html ├── _layouts │ ├── default.html │ ├── docs.html │ ├── parent.html │ └── post.hbs ├── _posts │ ├── post1.md │ └── post2.md ├── about.html ├── alt │ ├── button.html │ └── heading.hbs ├── css │ ├── hl.css │ └── main.css ├── data │ ├── about │ │ └── about.yaml │ ├── home.yaml │ └── staff │ │ ├── dummy.txt │ │ ├── shane.yaml │ │ └── simon.yaml ├── docs │ ├── cheatsheet.md │ ├── data.md │ ├── includes.md │ ├── layouts.md │ └── markdown.md ├── highlighting-markdown.html ├── highlighting.html ├── images.html ├── images │ ├── favicon.png │ ├── nested │ │ └── favicon2.png │ └── no-images │ │ └── non-image.txt ├── index.html ├── projects │ └── browser-sync.html ├── vars.html └── work │ ├── client1.yaml │ └── client2.yaml └── specs ├── add.js ├── add.partial.js ├── builder.js ├── cache.js ├── cache.update.js ├── compile.js ├── current.js ├── data ├── data.all.syntax.js ├── data.dir.syntax.js ├── data.file.syntax.js ├── data.helper.js ├── data.item.js └── file.data.js ├── filepath.js ├── helpers.js ├── helpers.lodash.js ├── includes.js ├── key.to.url.js ├── layouts.js ├── markdown └── highlight.js ├── padd.lines.js ├── pages.js ├── pages └── add.page.js ├── plugin.images.js ├── posts ├── add.post.categories.js ├── add.post.dates.js ├── add.post.js ├── add.post.tags.js ├── add.post.this.categories.js ├── add.post.this.tags.js └── categories.js ├── pre.process.js ├── pre.process.partials.js ├── simple.js ├── stream ├── stream.cache.front.matter.js ├── stream.cache.js ├── stream.js └── stream.posts.js ├── transforms.js ├── types.js └── urlpath.js /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .idea 3 | .aws* 4 | node_modules 5 | .sass-cache 6 | bower_components 7 | _fixtures 8 | ast.json 9 | funcs.js 10 | stream-out 11 | _bench/ 12 | _bench-out/ 13 | simple-out/ 14 | npm-debug.log 15 | example-*.js 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "laxbreak": true, 5 | "immed": true, 6 | "latedef": "nofunc", 7 | "newcap": true, 8 | "noarg": true, 9 | "sub": true, 10 | "undef": false, 11 | "unused": true, 12 | "quotmark": "double", 13 | "boss": true, 14 | "eqnull": true, 15 | "node": true, 16 | "mocha": true, 17 | "esnext": true 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | git: 3 | depth: 2 4 | language: node_js 5 | node_js: 6 | - 4 7 | - 6 8 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf input 3 | 4 | shallow_clone: true 5 | 6 | version: '1.0.{build}' 7 | 8 | environment: 9 | matrix: 10 | - nodejs_version: '1.2' 11 | - nodejs_version: '0.12' 12 | 13 | install: 14 | - ps: Install-Product node $env:nodejs_version 15 | - npm install 16 | 17 | test_script: 18 | - npm test 19 | 20 | build: off 21 | 22 | cache: 23 | - node_modules -> package.json 24 | 25 | -------------------------------------------------------------------------------- /example-bench.js: -------------------------------------------------------------------------------- 1 | var crossbow = require("./plugins/stream"); 2 | var fs = require("vinyl-fs"); 3 | var rimraf = require("rimraf").sync; 4 | var outpath = "./_bench-out"; 5 | 6 | var site = require("./").builder({ 7 | config: { 8 | base: "_bench" 9 | } 10 | }); 11 | 12 | rimraf(outpath); 13 | 14 | console.time("bench"); 15 | 16 | fs.src([ 17 | "_bench/*.md" 18 | //"_bench/1-file.html" 19 | ]) 20 | .pipe(crossbow({builder: site})) 21 | .pipe(fs.dest(outpath)).on("end", function () { 22 | console.timeEnd("bench"); 23 | }); 24 | 25 | 26 | -------------------------------------------------------------------------------- /example-simple.js: -------------------------------------------------------------------------------- 1 | var crossbow = require("./"); 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | var rimraf = require("rimraf").sync; 5 | var outpath = "./simple-out"; 6 | 7 | rimraf(outpath); 8 | 9 | var index = fs.readFileSync("./test/fixtures/index.html", "utf-8"); 10 | var about = fs.readFileSync("./test/fixtures/about.html", "utf-8"); 11 | var images = fs.readFileSync("./test/fixtures/images.html", "utf-8"); 12 | 13 | fs.mkdirSync(outpath); 14 | 15 | var site = crossbow.builder({ 16 | config: { 17 | base: "test/fixtures" 18 | } 19 | }); 20 | 21 | var out1 = site.add({key: "test/fixtures/index.html", content: index}); 22 | var out2 = site.add({key: "test/fixtures/about.html", content: about}); 23 | var out3 = site.add({key: "test/fixtures/images.html", content: images}); 24 | 25 | site.freeze(); 26 | 27 | site.compileAll({ 28 | cb: function (err, out) { 29 | if (err) throw err; 30 | 31 | out.forEach(function (item) { 32 | fs.writeFileSync(path.join(outpath, item.get("filepath")), item.get("compiled")); 33 | }) 34 | } 35 | }); 36 | 37 | // site.compile({ 38 | // item: out1, 39 | // cb: function (err, out) { 40 | // if (err) { 41 | // return console.log(err.stack); 42 | // } 43 | // fs.writeFileSync(path.join(outpath, out.get("filepath")), out.get("compiled")); 44 | // } 45 | // }); 46 | // 47 | // site.compile({ 48 | // item: out2, 49 | // cb: function (err, out) { 50 | // if (err) { 51 | // return console.log(err.stack); 52 | // } 53 | // fs.writeFileSync(path.join(outpath, out.get("filepath")), out.get("compiled")); 54 | // } 55 | // }); 56 | -------------------------------------------------------------------------------- /example-stream.js: -------------------------------------------------------------------------------- 1 | var crossbow = require("./"); 2 | var through = require("through2"); 3 | var fs = require("vinyl-fs"); 4 | var rimraf = require("rimraf").sync; 5 | var outpath = "./stream-out"; 6 | 7 | rimraf(outpath); 8 | 9 | fs.src([ 10 | "test/fixtures/*.html", 11 | "test/fixtures/_posts/**" 12 | //"test/fixtures/docs/**", 13 | //"test/fixtures/projects/**" 14 | ]) 15 | .pipe(crossbow.stream({ 16 | config: { 17 | base: "test/fixtures", 18 | prettyUrls: true, 19 | }, 20 | data: { 21 | work: 'dir:work' 22 | } 23 | })) 24 | .pipe(fs.dest(outpath)); 25 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var Readable = require('stream').Readable; 2 | var rs = Readable({objectMode: true}); 3 | var Writable = require('stream').Writable; 4 | var concat = require('concat-stream'); 5 | var Transform = require('stream').Transform; 6 | var fs = require("fs"); 7 | var content = fs.readFileSync("./_bench/1-file.html"); 8 | 9 | rs._read = function () { 10 | rs.push(content); 11 | rs.push(null); 12 | }; 13 | 14 | rs.pipe(modifier1()) 15 | .pipe((function () { 16 | return s(function (chunk, enc, next) { 17 | console.log(chunk.length); 18 | this.push(chunk); 19 | next(); 20 | }) 21 | }())) 22 | .pipe(concat(function (body) { 23 | console.log(body.toString()); 24 | })); 25 | 26 | var rs2 = Readable({objectMode: true}); 27 | 28 | rs2._read = function () { 29 | this.push({name: "shane"}); 30 | this.push(null); 31 | }; 32 | 33 | rs2.pipe((function () { 34 | return s(function (item, enc, next) { 35 | console.log(item); 36 | next(); 37 | }); 38 | })()) 39 | .pipe(concat(function (body) { 40 | console.log(body); 41 | })); 42 | 43 | 44 | /** 45 | * @returns {*} 46 | */ 47 | function modifier1 () { 48 | 49 | return s(function (chunk, enc, next) { 50 | var modded = chunk.toString() + chunk.toString(); 51 | this.push(modded); 52 | next(); 53 | }); 54 | } 55 | 56 | /** 57 | * @returns {*} 58 | */ 59 | function lastStep () { 60 | return concat(function (chunk, enc, next) { 61 | console.log(chunk.toString()); 62 | }); 63 | } 64 | 65 | function s (fn, fnend) { 66 | 67 | var ws = new Transform(); 68 | 69 | ws._write = fn; 70 | 71 | if (fnend) { 72 | ws._flush = fnend; 73 | } 74 | 75 | return ws; 76 | } 77 | 78 | function c () { 79 | 80 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var crossbow = require("./"); 3 | var browserSync = require("browser-sync"); 4 | var rimraf = require("rimraf"); 5 | var htmlInjector = require("bs-html-injector"); 6 | 7 | var site = crossbow.builder({ 8 | config: { 9 | base: "test/fixtures", 10 | defaultLayout: "default.html", 11 | prettyUrls: true 12 | }, 13 | data: { 14 | site: "file:_config.yml", 15 | cats: "file:_config.json", 16 | work: "dir:work" 17 | } 18 | }); 19 | 20 | /** 21 | * Start BrowserSync 22 | */ 23 | gulp.task("serve", function () { 24 | browserSync.use(htmlInjector); 25 | browserSync({ 26 | open: false, 27 | logLevel: "silent", 28 | server: { 29 | baseDir: "_site", 30 | routes: { 31 | "/img": "./img", 32 | "/css": "test/fixtures/css" 33 | } 34 | } 35 | }, function (err, bs) { 36 | site.logger.info("View your website at: {yellow:%s}", bs.getOptionIn(["urls", "local"])); 37 | if (bs.getOptionIn(["urls", "external"])) { 38 | site.logger.info("View your website at: {yellow:%s}", bs.getOptionIn(["urls", "external"])); 39 | } 40 | }); 41 | }); 42 | 43 | function buildSite() { 44 | return gulp.src([ 45 | "test/fixtures/index.html", 46 | "test/fixtures/docs/**", 47 | "test/fixtures/_posts/**" 48 | ]) 49 | .pipe(crossbow.stream({builder: site})) 50 | .pipe(gulp.dest("_site")); 51 | } 52 | 53 | process.stdin.on("data", function (buff) { 54 | if (buff.toString() === "cb\n") { 55 | buildSite().on("end", function () { 56 | browserSync.reload(); 57 | }); 58 | } 59 | }); 60 | 61 | /** 62 | * Default task 63 | */ 64 | gulp.task("crossbow", function () { 65 | buildSite(); 66 | }); 67 | 68 | gulp.task("watch", function () { 69 | gulp.watch(["test/fixtures/**"]).on("change", function (file) { 70 | var time = new Date().getTime(); 71 | if (file.type === "deleted" || file.type === "added" || file.type === "renamed") { 72 | buildSite().on("end", function () { 73 | site.logger.info("Recompiled in %sms", new Date().getTime() - time); 74 | browserSync.reload(); 75 | }); 76 | } else { 77 | gulp.src(file.path) 78 | .pipe(crossbow.stream({builder: site})) 79 | .pipe(gulp.dest("_site")) 80 | .on("end", function () { 81 | site.logger.info("Recompiled & saved to disk in {yellow:%sms}", new Date().getTime() - time); 82 | browserSync.notify("Crossbow: Injecting HTML"); 83 | htmlInjector(); 84 | }); 85 | } 86 | }); 87 | }); 88 | 89 | gulp.task("clean", function (done) { 90 | rimraf.sync("./_site"); 91 | done(); 92 | }); 93 | 94 | gulp.task("default", ["clean", "crossbow", "serve", "watch"]); 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var crossbow = require("./lib/crossbow"); 2 | var Immutable = require("immutable"); 3 | var merge = require("./lib/config").merge; 4 | var noKey = 0; 5 | 6 | /** 7 | * Simple compiler with no state 8 | * @returns {*} 9 | */ 10 | function compile (opts) { 11 | 12 | opts.config = opts.config || {}; 13 | opts.config.simpleMode = true; 14 | opts.cb = opts.cb || function () { /*noop*/ }; 15 | 16 | if (!opts.key) { 17 | opts.key = "crossbow-item-" + noKey; 18 | noKey += 1; 19 | } 20 | 21 | return crossbow 22 | .create(merge(opts.config)) 23 | .compile(opts); 24 | } 25 | 26 | module.exports.compile = compile; 27 | 28 | /** 29 | * Builder. 30 | * var site = crossbow.builder(); 31 | * site.addPage("index.html", "some content") 32 | */ 33 | function builder (opts) { 34 | 35 | opts = opts || {}; 36 | opts.config = opts.config || {}; 37 | opts.data = opts.data || {}; 38 | opts.cb = opts.cb || function () { /*noop*/ }; 39 | 40 | var site = crossbow.create(opts.config); 41 | 42 | site.defaultData = Immutable.fromJS(opts.data); 43 | 44 | return site; 45 | } 46 | 47 | module.exports.builder = builder; 48 | 49 | module.exports.stream = require("./plugins/stream"); 50 | -------------------------------------------------------------------------------- /lib/Compiler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param config 3 | * @constructor 4 | */ 5 | function Compiler (config) { 6 | 7 | /** 8 | * Avoid using `this` 9 | * @type {Compiler} 10 | */ 11 | var compiler = this; 12 | 13 | /** 14 | * Allow usage without `new` 15 | */ 16 | if (!(compiler instanceof Compiler)) { 17 | return new Compiler(config); 18 | } 19 | 20 | /** 21 | * Save configuration 22 | */ 23 | compiler.userConfig = config; 24 | 25 | /** 26 | * Set some state on this object before returning 27 | */ 28 | return compiler; 29 | } 30 | 31 | Compiler.prototype.getErrorString = require("./public/getErrorString"); 32 | Compiler.prototype.parseContent = require("./public/parseContent"); 33 | Compiler.prototype.preProcess = require("./public/preProcess"); 34 | Compiler.prototype.mergeData = require("./public/mergeData"); 35 | Compiler.prototype.getType = require("./public/getType"); 36 | Compiler.prototype.freeze = require("./public/freeze"); 37 | Compiler.prototype.error = require("./public/error"); 38 | Compiler.prototype.add = require("./public/add"); 39 | Compiler.prototype.compile = require("./public/compile"); 40 | Compiler.prototype.compileAll = require("./public/compileAll"); 41 | Compiler.prototype.compileMany = require("./public/compileMany"); 42 | Compiler.prototype.transform = require("./public/transform"); 43 | 44 | module.exports = Compiler; 45 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | var Immutable = require("immutable"); 2 | 3 | /** 4 | * @type {Cache} 5 | */ 6 | module.exports = Cache; 7 | 8 | /** 9 | * @constructor 10 | */ 11 | function Cache () { 12 | 13 | var cache = this; 14 | 15 | cache._items = Immutable.OrderedMap({}); 16 | 17 | cache.add = function (item) { 18 | cache._items = cache._items.set(item.get("key"), item); 19 | return item; 20 | }; 21 | 22 | cache.byType = function (type, filter) { 23 | var byType = cache._items.filter(function (item) { 24 | return item.get("type") === type; 25 | }); 26 | if (filter) { 27 | byType = byType.filter(filter); 28 | } 29 | return byType.toList(); 30 | }; 31 | 32 | cache.withoutType = function (type) { 33 | return cache._items.filter(function (item) { 34 | return item.get("type") !== type; 35 | }).toList(); 36 | }; 37 | 38 | cache.byKey = function (key) { 39 | return cache._items.get(key); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | var Immutable = require("immutable"); 2 | 3 | var defaults = Immutable.fromJS({ 4 | /** 5 | * Global log level 6 | */ 7 | logLevel: "info", 8 | /** 9 | * Should posts be rendered using Markdown? 10 | */ 11 | markdown: true, 12 | /** 13 | * 14 | */ 15 | inlineErrors: true, 16 | /** 17 | * 18 | */ 19 | base: process.cwd(), 20 | /** 21 | * Should code snippets be auto-highlighted? 22 | */ 23 | highlight: true, 24 | /** 25 | * Should ALL code fences be auto-highlighted? 26 | */ 27 | autoHighlight: false, 28 | /** 29 | * Data format for posts 30 | */ 31 | dateFormat: "LL", // http://momentjs.com/docs/#/utilities/ 32 | /** 33 | * Post url format, eg: /blog/:year/:month/:title 34 | */ 35 | urlFormat: { 36 | "page": "/:filename" 37 | }, 38 | /** 39 | * Instead of `blog/post1.html,` you can have `blog/post1` instead 40 | */ 41 | prettyUrls: false, 42 | /** 43 | * Padd content blocks to enable nicer source code 44 | */ 45 | prettyMarkup: true, 46 | /** 47 | * Should extra `\n` be striped from markup? 48 | *
Hi
49 | * 50 | * Bye 51 | * 52 | * Would become: 53 | *Hi
54 | * Bye 55 | */ 56 | markup: { 57 | stripNewLines: false 58 | }, 59 | /** 60 | * No layouts by default. 61 | */ 62 | defaultLayout: false, 63 | /** 64 | * Initial Site config 65 | */ 66 | siteConfig: {}, 67 | /** 68 | * Set lookup DIRS for layouts & includes 69 | */ 70 | dirs: { 71 | "layouts": "_layouts", 72 | "includes": "_includes" 73 | }, 74 | /** 75 | * Global error handler 76 | */ 77 | errorHandler: function (err, compiler) { 78 | compiler.logger.error(compiler.getErrorString(err)); 79 | } 80 | }); 81 | 82 | module.exports.merge = function (config) { 83 | return defaults.mergeDeep(config); 84 | }; -------------------------------------------------------------------------------- /lib/crossbow.js: -------------------------------------------------------------------------------- 1 | var Cache = require("./cache"); 2 | var events = require("events"); 3 | var Immutable = require("immutable"); 4 | var EE = require("easy-extender"); 5 | var merge = require("./config").merge; 6 | 7 | var defaultPlugins = { 8 | "template": require("./plugins/handlebars"), 9 | "logger": require("./logger"), 10 | "file": require("./file"), 11 | "file-helper": require("./file-helper"), 12 | "filter": require("./filter"), 13 | "markdown": require("./plugins/markdown"), 14 | "type:page": require("./public/addPage"), 15 | "type:partial": require("./public/addPartial"), 16 | "type:post": require("./public/addPost") 17 | }; 18 | 19 | /** 20 | * Create a Compiler Instance 21 | * @param config 22 | * @returns {*} 23 | */ 24 | module.exports.create = function (config) { 25 | var compiler = new require("./Compiler")(config); 26 | return setState(compiler); 27 | }; 28 | 29 | /** 30 | * Set state/plugins/hooks on the running instance 31 | * @param compiler 32 | */ 33 | function setState(compiler) { 34 | 35 | /** 36 | * Compiler helpers 37 | * @type {Function} 38 | */ 39 | // console.log(merge().toJS()); 40 | compiler.pluginManager = new EE(defaultPlugins, require("./hooks")); 41 | compiler.pluginManager.init(); 42 | compiler.config = compiler.pluginManager.hook("config", compiler, merge()); 43 | compiler.emitter = compiler.emitter || new events.EventEmitter(); 44 | compiler.cache = compiler.cache || new Cache(); 45 | compiler.logger = compiler.pluginManager.get("logger")(compiler); 46 | compiler.file = compiler.pluginManager.get("file")(compiler); 47 | compiler.fileCache = {}; 48 | compiler.template = compiler.pluginManager.get("template")(compiler); 49 | compiler.Handlebars = compiler.template.Handlebars; 50 | compiler.fileHelper = compiler.pluginManager.get("file-helper")(compiler); 51 | compiler.filters = compiler.pluginManager.hook("filters"); 52 | compiler.filter = compiler.pluginManager.get("filter")(compiler); 53 | compiler.defaultData = Immutable.Map({}); 54 | compiler.registerHelper = compiler.template.registerHelper; 55 | compiler.contentTransforms = compiler.pluginManager.hook("contentTransforms", compiler); 56 | compiler.dataTransforms = compiler.pluginManager.hook("dataTransforms", compiler); 57 | compiler.itemTransforms = compiler.pluginManager.hook("itemTransforms", compiler); 58 | compiler.frozenTransforms = compiler.pluginManager.hook("frozenTransforms", compiler); 59 | 60 | /** 61 | * Get types from hooks 62 | * @type {*} 63 | */ 64 | compiler.types = compiler.pluginManager.hook("types", compiler); 65 | 66 | return compiler; 67 | } 68 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var objPath = require("object-path"); 2 | var prefix = "{red:[ ERROR ]}"; 3 | 4 | var fails = { 5 | "include": function (data) { 6 | var hash = objPath.get(data.error, "_params", {}); 7 | if (hash.src) { 8 | return ["File not found: {yellow:%s}", hash.src]; 9 | } 10 | return [prefix + " %s", data.error.message]; 11 | }, 12 | "highlight:type": function (data) { 13 | var crossbow = objPath.get(data, "error._crossbow", {}); 14 | if (crossbow.message) { 15 | var str = crossbow.message; 16 | str += "\n"; 17 | str += "\n"; 18 | str += "File: {yellow:" + crossbow.file + "}\n"; 19 | str += "\n"; 20 | return [str]; 21 | } 22 | }, 23 | "include:src": function (data) { 24 | var crossbow = objPath.get(data, "error._crossbow", {}); 25 | if (crossbow.message) { 26 | var str = crossbow.message; 27 | str += "\n"; 28 | str += "\n"; 29 | str += "File: {yellow:" + crossbow.file + "}\n"; 30 | str += "Line: {yellow:" + crossbow.line + "}\n"; 31 | str += "\n"; 32 | str += "Example: {yellow:\\{\\{inc src=\"button.html\"\\}\\}}\n"; 33 | return [str]; 34 | } 35 | }, 36 | "yaml": function (data) { 37 | var crossbow = objPath.get(data, "error._crossbow", {}); 38 | if (crossbow.message) { 39 | var str = crossbow.message; 40 | str += "\n"; 41 | str += "\n"; 42 | str += "File: {yellow:" + crossbow.file + "}\n"; 43 | str += "Line: {yellow:" + crossbow.line + "}\n"; 44 | return [str]; 45 | } 46 | }, 47 | "json": function (data) { 48 | var crossbow = objPath.get(data, "error._crossbow", {}); 49 | if (crossbow.message) { 50 | var str = crossbow.message; 51 | str += "\n"; 52 | str += "\n"; 53 | str += "File: {yellow:" + crossbow.file + "}\n"; 54 | return [str]; 55 | } 56 | }, 57 | "data:params": function (data) { 58 | var crossbow = objPath.get(data, "error._crossbow", {}); 59 | if (crossbow.message) { 60 | var str = crossbow.message; 61 | str += "\n"; 62 | str += "\n"; 63 | str += "File: {yellow:" + crossbow.file + "}\n"; 64 | return [str]; 65 | } 66 | }, 67 | "compile": function (data) { 68 | 69 | var hash = objPath.get(data, "error.hash", {}); 70 | var position = objPath.get(hash, "position", {}); 71 | var crossbow = objPath.get(data, "error._crossbow", {}); 72 | 73 | var str = data.error.message; 74 | 75 | if (hash && hash.position) { 76 | str = ""; 77 | str += "You have an error in your syntax.\n"; 78 | str += "\n"; 79 | str += "File: {yellow:" + crossbow.file + "}\n"; 80 | str += "Line: {yellow:" + crossbow.line + "}\n"; 81 | str += "\n"; 82 | str += "{green:" + escapeCurlies(position.string[0]) + "}"; 83 | str += "\n"; 84 | str += "{cyan:" + escapeCurlies(position.string[1]) + "}"; 85 | str += "\n"; 86 | str += "\n"; 87 | } 88 | 89 | return [str]; 90 | }, 91 | "lodash:string:params": function (data) { 92 | var crossbow = objPath.get(data, "error._crossbow", {}); 93 | if (crossbow.message) { 94 | var str = crossbow.message; 95 | str += "\n"; 96 | str += "\n"; 97 | str += "File: {yellow:" + crossbow.file + "}\n"; 98 | str += "\n"; 99 | str += "Example 1: {yellow:\\{\\{$_ 'camelCase' 'Foo Bar'\\}\\}}\n"; 100 | str += "Output: {green:fooBar\n"; 101 | return [str]; 102 | } 103 | } 104 | }; 105 | 106 | function escapeCurlies(str) { 107 | return str.replace(/\{/g, "\\{").replace(/\}/g, "\\}"); 108 | } 109 | 110 | module.exports.fails = fails; 111 | 112 | var wrapper = "Crossbow WARNING: %s"; 113 | 114 | var inline = { 115 | "imgs:none-found" : function (dir) { 116 | return wrapper.replace("%s", "No images found in " + dir); 117 | }, 118 | "dir:notfound" : function (dir) { 119 | return wrapper.replace("%s", "Directory not found: " + dir); 120 | }, 121 | "file:notfound" : function (file) { 122 | return wrapper.replace("%s", "Include not found: " + file); 123 | }, 124 | "data:as": function () { 125 | return wrapper.replace("%s", "`as` parameter required for the data helper"); 126 | } 127 | }; 128 | module.exports.inline = inline; 129 | 130 | 131 | -------------------------------------------------------------------------------- /lib/file-helper.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | var path = require("./core/path"); 3 | var fs = require("fs"); 4 | var Handlebars = require("handlebars"); 5 | var errors = require("./errors").inline; 6 | 7 | 8 | module.exports.plugin = function (compiler) { 9 | 10 | var helper = { 11 | getOptions: function (args) { 12 | var arr = Array.prototype.slice.call(args); 13 | if (arr.length) { 14 | return arr[arr.length - 1]; 15 | } 16 | }, 17 | processParams: function (opts) { 18 | if (!opts.options.hash) { 19 | return {}; 20 | } 21 | if (opts.required && !utils.verifyParams(opts.options.hash, opts.required)) { 22 | var err = new Error(opts.error); 23 | err._params = opts.options.hash; 24 | err._crossbow = opts.options._crossbow || {}; 25 | err._type = opts.type; 26 | err._type = opts.type; 27 | err._crossbow.message = opts.error; 28 | err._crossbow.file = compiler.item.get("filepath"); 29 | compiler.error(err); 30 | return false; 31 | } 32 | return utils.processParams(opts.options.hash, opts.options.data.root, compiler); 33 | }, 34 | retrieveFile: function (opts) { 35 | 36 | /** 37 | * Data sandbox for partial 38 | */ 39 | var sandbox = utils.prepareSandbox(opts.params, opts.options.data.root); 40 | 41 | /** 42 | * File content only 43 | * @type {string} 44 | */ 45 | var file = compiler.file.getFile({path: opts.params.src}); 46 | 47 | /** 48 | * Return early if not retrievable 49 | */ 50 | if (!file) { 51 | return { 52 | inlineError: new Handlebars.SafeString(errors["file:notfound"](opts.params.src)) 53 | }; 54 | } 55 | 56 | /** 57 | * Compile include 58 | */ 59 | var compiled = compiler.safeCompile(file.content || "", sandbox); 60 | 61 | /** 62 | * Default to not padded 63 | */ 64 | var padded = utils.paddLines({ 65 | stripNewLines: compiler.config.getIn(["markup", "stripNewLines"]), 66 | content: compiled || "", 67 | count: opts.options._crossbow.column || 0 68 | }); 69 | 70 | /** 71 | * Return processed string 72 | */ 73 | return { 74 | data: file.data, 75 | content: file.content, 76 | compiled: compiled, 77 | padded: padded 78 | }; 79 | }, 80 | scanDir: function (dir, exts) { 81 | 82 | var base = compiler.config.get('base'); 83 | const filepath = path.join(compiler.config.get("base"), dir); 84 | 85 | const maybePath = (function () { 86 | if (fs.existsSync(filepath)) { 87 | return filepath; 88 | } 89 | if (fs.existsSync(dir)) { 90 | base = process.cwd(); 91 | return path.resolve(dir); 92 | } 93 | })(); 94 | 95 | if (!maybePath) { 96 | return { 97 | errors: [ 98 | { 99 | type: "Directory not found", 100 | error: new Error("Directory not found") 101 | } 102 | ] 103 | } 104 | } 105 | 106 | const files = fs.readdirSync(maybePath) 107 | .map(function (item) { 108 | return path.join(base, dir, item); 109 | }) 110 | .map(function (item) { 111 | return [item, path.parse(item)]; 112 | }) 113 | .filter(function (item) { 114 | return exts.indexOf(item[1].ext.toLowerCase()) > -1; 115 | }) 116 | .map(function (tuple) { 117 | return { 118 | absolute: tuple[0], 119 | parsed: tuple[1] 120 | } 121 | }); 122 | // .reduce(function (acc, item) { 123 | // const resolved = item[0]; 124 | // const parsed = item[1]; 125 | // acc[parsed.name] = {}; 126 | // acc[parsed.name].resolved = resolved; 127 | // acc[parsed.name].parsed = parsed; 128 | // return acc; 129 | // }, {}); 130 | 131 | return { 132 | errors: [], 133 | files: files 134 | } 135 | } 136 | }; 137 | 138 | return helper; 139 | }; 140 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var path = require("./core/path"); 3 | var yaml = require("./yaml"); 4 | var utils = require("./utils"); 5 | 6 | /** 7 | * @param compiler 8 | * @returns {{getFile: Function, resolvePath: Function, getOneFromFileSystem: Function, getJsonData: Function, getYamlData: Function}} 9 | */ 10 | module.exports.plugin = function (compiler) { 11 | 12 | var file = { 13 | /** 14 | * {{path: string}} opts 15 | */ 16 | getFile: function (opts) { 17 | 18 | compiler.logger.debug("{fs:Looking for file: {file:%s}", opts.path); 19 | 20 | //if (compiler.fileCache[opts.path]) { 21 | // return compiler.fileCache[opts.path]; 22 | //} 23 | 24 | var filepath = file.resolvePath(opts); 25 | 26 | if (!filepath) { 27 | compiler.logger.warn("{fs:Failed to load the file: {file:%s} from the file system", opts.path); 28 | return false; 29 | } 30 | 31 | compiler.logger.debug("{fs:Returning file: {file:%s}", filepath); 32 | 33 | var fileout = file.getOneFromFileSystem(filepath); 34 | 35 | //compiler.fileCache[opts.path] = fileout; 36 | 37 | return fileout; 38 | }, 39 | /** 40 | * @param opts 41 | * @returns {*} 42 | */ 43 | resolvePath: function (opts) { 44 | 45 | if (!opts.path) { 46 | return false; 47 | } 48 | 49 | var filepath = path.join(compiler.config.get("base"), opts.path); 50 | 51 | if (!fs.existsSync(filepath)) { 52 | 53 | // always try relative path first 54 | if (fs.existsSync(opts.path)) { 55 | return opts.path; 56 | } else { 57 | 58 | var resolved = file.resolver(compiler.config)(opts.path); 59 | 60 | if (fs.existsSync(resolved)) { 61 | compiler.logger.debug("{fssuccess:Resolved from File System: {file:%s}", resolved); 62 | return resolved; 63 | } 64 | 65 | compiler.logger.warn("{fs:Tried (failed) to resolve file: {file:%s}", resolved); 66 | compiler.logger.warn("{fs:Perhaps you should set your {magenta:base}? It's currently {magenta:%s}", compiler.config.get("base")); 67 | 68 | return false; 69 | } 70 | } else { 71 | return filepath; 72 | } 73 | }, 74 | /** 75 | * @param filepath 76 | * @returns {{data: {}, content: *}} 77 | */ 78 | getOneFromFileSystem: function (filepath) { 79 | 80 | var data = {}; 81 | var absPath = require("path").resolve(filepath); 82 | var content = fs.readFileSync(absPath, "utf-8"); 83 | 84 | if (filepath.match(/\.json$/i)) { 85 | data = file.getJsonData(filepath, content); 86 | } 87 | if (filepath.match(/\.ya?ml$/i)) { 88 | data = file.getYamlData(filepath, content); 89 | } 90 | 91 | return { 92 | data: data, 93 | content: content, 94 | absolutePath: absPath 95 | }; 96 | }, 97 | /** 98 | * @param filepath 99 | * @param content 100 | * @returns {{}} 101 | */ 102 | getJsonData: function getJsonData (filepath, content) { 103 | try { 104 | return JSON.parse(content); 105 | } catch (e) { 106 | e._crossbow = { 107 | file: filepath, 108 | line: 0, 109 | message: "Could not parse JSON" 110 | }; 111 | e._type = "json"; 112 | compiler.error(e); 113 | return {}; 114 | } 115 | }, 116 | /** 117 | * @param filepath 118 | * @param content 119 | * @returns {*} 120 | */ 121 | getYamlData: function getYamlData (filepath, content) { 122 | try { 123 | return yaml.parseYaml(content); 124 | } catch (e) { 125 | e._crossbow = { 126 | line: e.mark.line + 1, 127 | file: filepath, 128 | message: e.message 129 | }; 130 | e._type = "yaml"; 131 | compiler.error(e); 132 | return {}; 133 | } 134 | }, 135 | /** 136 | * @param config 137 | * @returns {Function} 138 | */ 139 | resolver: function (config) { 140 | return function (filep) { 141 | return utils.makeFsPath( 142 | utils.getFilePath( 143 | filep, 144 | config.get("dirs").get("includes") 145 | ), 146 | config.get("base") 147 | ); 148 | }; 149 | } 150 | }; 151 | 152 | return file; 153 | }; -------------------------------------------------------------------------------- /lib/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply a filter if it exists 3 | * @param {Compiler} compiler 4 | * @returns {Object} 5 | */ 6 | module.exports.plugin = function (compiler) { 7 | 8 | var filter = { 9 | /** 10 | * @param {{content: string, name: string, params: object}} opts 11 | * @returns {*} 12 | */ 13 | apply: function (opts) { 14 | var fn = compiler.filters[opts.name]; 15 | if (fn) { 16 | return fn(opts, compiler); 17 | } 18 | return opts.content; 19 | } 20 | }; 21 | 22 | return filter; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/hooks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | contentTransforms: function (hooks) { 3 | return hooks[0]; 4 | }, 5 | dataTransforms: function (hooks) { 6 | return hooks[0]; 7 | }, 8 | itemTransforms: function () { 9 | return []; 10 | }, 11 | frozenTransforms: function (hooks) { 12 | return hooks; 13 | }, 14 | filters: function (hooks) { 15 | return hooks[0]; 16 | }, 17 | types: function (hooks) { 18 | return hooks.reduce(function (combined, item) { 19 | if (!combined[item.name]) { 20 | combined[item.name] = item; 21 | } 22 | return combined; 23 | }, {}); 24 | }, 25 | config: function (hooks, compiler, initial) { 26 | return initial.mergeDeep(hooks[0]).mergeDeep(compiler.userConfig); 27 | } 28 | }; -------------------------------------------------------------------------------- /lib/layouts.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | var yaml = require("./yaml"); 3 | 4 | /** 5 | * Allow layouts to have layouts. 6 | * Recursively render layout from the inside out (allows any number of nested layouts until the current 7 | * one does not specify a layouts 8 | * @param {{compiler: Compiler, item: Compiler.item, content: string, layout: string}} opts 9 | */ 10 | function addLayout(opts) { 11 | 12 | var compiler = opts.compiler; 13 | var layoutPath = utils.getFilePath(opts.layout, compiler.config.getIn(["dirs", "layouts"])); 14 | var layoutFile = compiler.file.getFile({path: layoutPath}); 15 | var data = compiler.frozen; 16 | 17 | if (!layoutFile) { 18 | 19 | compiler.logger.warn("The layout file {red:%s} does not exist", layoutPath); 20 | compiler.logger.warn("Check the file exists and that you've set the {magenta:base} property"); 21 | 22 | return emptyReturn(); 23 | } 24 | 25 | function emptyReturn () { 26 | return opts.cb(null, { 27 | content: opts.content 28 | }); 29 | } 30 | 31 | if (layoutFile && yaml.hasFrontMatter(layoutFile.content)) { 32 | 33 | var _data = yaml.readFrontMatter({ 34 | content: layoutFile.content, 35 | compiler: compiler, 36 | key: opts.item.get("key") 37 | }); 38 | 39 | return compiler.template.render(_data.content, data, function (err, out) { 40 | 41 | compiler.template.addContent({ 42 | content: out, 43 | config: compiler.config, 44 | context: data 45 | }); 46 | 47 | opts.layout = _data.front.layout; 48 | opts.content = out; 49 | 50 | addLayout(opts); 51 | }); 52 | } 53 | 54 | opts.cb(null, { 55 | content: compiler.template.render(layoutFile.content, data) 56 | }); 57 | } 58 | 59 | module.exports = addLayout; -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.plugin = function (compiler) { 4 | 5 | /** 6 | * Create logger that anyone can use 7 | */ 8 | var logger = require("eazy-logger").Logger({ 9 | prefix: "{magenta:Crossbow:{blue: ⇰ }", 10 | useLevelPrefixes: true, 11 | level: compiler.config.get("logLevel"), 12 | custom: { 13 | file: function (string) { 14 | return this.compile("{yellow:" + string + "}"); 15 | }, 16 | fs: function (string) { 17 | return this.compile("{yellow:[FS]} " + string); 18 | }, 19 | fssuccess: function (string) { 20 | return this.compile("{green:[FS] " + string); 21 | } 22 | } 23 | }); 24 | 25 | return logger; 26 | }; -------------------------------------------------------------------------------- /lib/paginator.js: -------------------------------------------------------------------------------- 1 | //var utils = require("./utils"); 2 | //var Page = require("./page"); 3 | //var _ = require("lodash"); 4 | //var Immutable = require("immutable"); 5 | // 6 | ///** 7 | // * @type {Paginator} 8 | // */ 9 | //module.exports = Paginator; 10 | // 11 | ///** 12 | // * @param items 13 | // * @param item 14 | // * @param count 15 | // * @param {Immutable.Map} [config] 16 | // * @returns {Paginator} 17 | // * @constructor 18 | // */ 19 | //function Paginator (items, item, count, config) { 20 | // 21 | // // Front matter/content split 22 | // count = count || 2; 23 | // 24 | // this._config = config || Immutable.Map({}); 25 | // 26 | // this._paged = this.paginate(count, items); 27 | // this._pages = this.makePaginationPages(item, this._paged, config); 28 | // this._index = item; 29 | // 30 | // this.perPage = count; 31 | // 32 | // return this; 33 | //} 34 | // 35 | ///** 36 | // * @param item 37 | // * @param config 38 | // * @param i 39 | // * @returns {*} 40 | // */ 41 | //Paginator.prototype.getMetaData = function (item, config, i) { 42 | // 43 | // var next = this._pages[i+1]; 44 | // var prev = this._pages[i-1]; 45 | // 46 | // return { 47 | // perPage: this.perPage, 48 | // items: utils.prepareFrontVars({items: item.items, config: config}), 49 | // next: next ? utils.prepareFrontVars({items: next.page, config: config}) : null, 50 | // prev: prev ? utils.prepareFrontVars({items: prev.page, config: config}) : null 51 | // }; 52 | //}; 53 | ///** 54 | // * @returns {*} 55 | // */ 56 | //Paginator.prototype.pages = function () { 57 | // return this._pages; 58 | //}; 59 | // 60 | ///** 61 | // * Get the previous post 62 | // */ 63 | //Paginator.prototype.paginate = function (count, items) { 64 | // 65 | // var arrays = []; 66 | // var clonedItems = _.cloneDeep(items); 67 | // 68 | // while (clonedItems.length > 0) { 69 | // arrays.push(clonedItems.splice(0, count)); 70 | // } 71 | // 72 | // return arrays; 73 | //}; 74 | // 75 | ///** 76 | // * @param item 77 | // * @param collection 78 | // * @param config 79 | // */ 80 | //Paginator.prototype.makePaginationPages = function(item, collection, config) { 81 | // 82 | // var _this = this; 83 | // 84 | // return _.map(collection, function (items, i) { 85 | // 86 | // var prepared = utils.prepareFrontVars({ 87 | // items: new Page(getKey(item.paths, i), item.original, _this.getItemConfig(i)), 88 | // config: config 89 | // }); 90 | // 91 | // return { 92 | // page: prepared, 93 | // items: items 94 | // }; 95 | // }); 96 | //}; 97 | // 98 | ///** 99 | // * @param i 100 | // */ 101 | //Paginator.prototype.getItemConfig = function(i) { 102 | // return Immutable.Map(this._config.set("transform", getTransforms(i))); 103 | //}; 104 | // 105 | ///** 106 | // * @param paths 107 | // * @param i 108 | // */ 109 | //function getKey(paths, i) { 110 | // 111 | // var name = paths.filePath; 112 | // var basename = paths.url.replace(/^\//, ""); 113 | // 114 | // if (i !== 0) { 115 | // name = basename + "/page%s/index.html".replace("%s", i + 1); 116 | // } 117 | // 118 | // return name; 119 | //} 120 | // 121 | ///** 122 | // * Paginated pages need transforms, such as title 123 | // * @param i 124 | // * @returns {Function} 125 | // */ 126 | //function getTransforms(i) { 127 | // 128 | // return function (item) { 129 | // if (i !== 0) { 130 | // item.front.title = item.front.title + " - Page " + (i+1); 131 | // } 132 | // }; 133 | //} -------------------------------------------------------------------------------- /lib/plugins/code-fences.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils"); 2 | 3 | /** 4 | * Escape code-fences automatically 5 | * @param out 6 | * @param {Immutable.map} config 7 | * @returns {*|XML|string|void} 8 | */ 9 | function codeFences (opts) { 10 | return opts.content; 11 | return utils.escapeInlineCode(utils.escapeCodeFences(opts.content)); // todo: remove? 12 | } 13 | 14 | module.exports = codeFences; -------------------------------------------------------------------------------- /lib/plugins/handlebars.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | var utils = require("../utils"); 3 | var objPath = require("object-path"); 4 | var Transform = require("stream").Transform; 5 | 6 | 7 | /** 8 | * @returns {{renderTemplate: renderTemplate, registerPartial: registerPartial}} 9 | */ 10 | module.exports.plugin = function (compiler) { 11 | 12 | /** 13 | * Wrap Handlebars rendering for error control 14 | * @param template 15 | * @param data 16 | * @param cb 17 | * @returns {string} 18 | */ 19 | compiler.safeCompile = function safeCompile(template, data, cb) { 20 | cb = cb || function() { /* noop */ }; 21 | var out = ""; 22 | try { 23 | out = renderTemplate(template, data); 24 | cb(null, out); 25 | return out; 26 | } catch (e) { 27 | cb(decorateError(e, compiler)); 28 | } 29 | }; 30 | 31 | var dump = require("./handlebars/dump") (compiler); 32 | var cleandump = require("./handlebars/clean-dump")(compiler); 33 | var sections = require("./handlebars/sections") (compiler); 34 | var helpers = require("./handlebars/helpers") (compiler); 35 | 36 | Handlebars.registerHelper("section", sections["section"]); 37 | Handlebars.registerHelper("$yield", sections["yield"]); 38 | Handlebars.registerHelper("$sep", require("./handlebars/sep")); 39 | 40 | /** 41 | * Return at least the required interface for crossbow 42 | */ 43 | return { 44 | "addToTemplateCache": function () { /*noop*/ }, 45 | "readFrontMatter": compiler.readFrontMatter, 46 | "render": compiler.safeCompile, 47 | "registerPartial": registerPartial, 48 | "addContent": addContent, 49 | "Handlebars": Handlebars, 50 | "registerHelper": function (name, fn) { 51 | Handlebars.registerHelper(name, fn(compiler)); 52 | } 53 | }; 54 | }; 55 | 56 | module.exports["plugin:name"] = "templates"; 57 | 58 | /** 59 | * @param {{content: string, config: Map}} opts 60 | * @returns {*} 61 | */ 62 | function addContent (opts) { 63 | Handlebars.registerHelper("content", function (options) { 64 | return new Handlebars.SafeString((function () { 65 | if (opts.config.get("prettyMarkup")) { 66 | return utils.paddLines({ 67 | stripNewLines: opts.config.getIn(["markup", "stripNewLines"]), 68 | content: opts.content, 69 | count: options._crossbow.column || 0 70 | }); 71 | } 72 | return opts.content; 73 | })()); 74 | }); 75 | return opts.context; 76 | } 77 | 78 | /** 79 | * @param key 80 | * @param value 81 | */ 82 | function registerPartial(key, value) { 83 | Handlebars.registerPartial(key, value); 84 | } 85 | 86 | /** 87 | * @param template 88 | * @param data 89 | */ 90 | function renderTemplate(template, data) { 91 | return Handlebars.compile(template)(data); 92 | } 93 | 94 | /** 95 | * @param e 96 | * @param data 97 | * @returns {*} 98 | */ 99 | function decorateError (e, data) { 100 | 101 | var yamlLineCount = data.item.getIn(["front", "lineCount"]); 102 | var HBlinecount = objPath.get(e, "hash.loc.first_line", 0); 103 | 104 | if (!e._crossbow) { 105 | e._crossbow = { 106 | line: yamlLineCount + HBlinecount 107 | }; 108 | } else { 109 | e._crossbow.line += yamlLineCount; 110 | } 111 | 112 | e._crossbow.file = data.item.get("key"); 113 | 114 | if (!e._type) { 115 | e._type = "compile"; 116 | } 117 | if (!e._ctx) { 118 | e._ctx = data.item; 119 | } 120 | 121 | return e; 122 | } 123 | 124 | module.exports.hooks = { 125 | dataTransforms: [ 126 | { 127 | when: "before templates", 128 | fn: function (opts) { 129 | 130 | [ 131 | "inc", 132 | "data", 133 | "save", 134 | "hl", 135 | "current", 136 | "if_eq", 137 | "if_not_eq", 138 | "loop", 139 | "hash", 140 | "md", 141 | "compile", 142 | "$_", 143 | "$md", 144 | "imgs" 145 | ].forEach(function (item) { 146 | Handlebars.registerHelper(item, require("./handlebars/" + item)(opts.compiler)); 147 | }); 148 | 149 | return opts.compiler.globalData; 150 | } 151 | } 152 | ] 153 | }; 154 | 155 | function s (fn, fnend) { 156 | var ws = new Transform(); 157 | ws._write = fn; 158 | if (fnend) { 159 | ws._flush = fnend; 160 | } 161 | return ws; 162 | } 163 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/$_.js: -------------------------------------------------------------------------------- 1 | /** 2 | * $_ (lodash) string helper. 3 | * 4 | * Check the docs here https://lodash.com/docs#camelCase 5 | * 6 | * EG: 7 | * 8 | * {{$_ 'startCase' 'node js'}} 9 | * 10 | * OUTPUT: 11 | * 12 | * Node Js 13 | * 14 | * @param {Compiler} compiler 15 | * @returns {function} 16 | */ 17 | module.exports = function (compiler) { 18 | 19 | return function $_Helper() { 20 | 21 | var _ = require("lodash"); 22 | var args = _.toArray(arguments); 23 | var paramErrors = "$_ (lodash) helper requires at least 2 params (method and value)"; 24 | var err; 25 | 26 | if (args.length < 3) { 27 | err = new Error(paramErrors); 28 | err._type = "lodash:string:params"; 29 | err._crossbow = { 30 | file: compiler.item.get("filepath"), 31 | message: paramErrors 32 | }; 33 | compiler.error(err); 34 | return ""; 35 | } 36 | 37 | var lodashArgs = _.dropRight(args, 1); 38 | var lodashFn = _[lodashArgs[0]]; 39 | 40 | if (!lodashFn) { 41 | var msg = "`" + lodashArgs[0] + "` is not a lodash string helper. Check the docs here https://lodash.com/docs#camelCase"; 42 | err = new Error(msg); 43 | err._type = "lodash:string:params"; 44 | err._crossbow = { 45 | file: compiler.item.get("filepath"), 46 | message: msg 47 | }; 48 | compiler.error(err); 49 | return ""; 50 | } 51 | 52 | return lodashFn.apply(null, lodashArgs.slice(1)); 53 | }; 54 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/$md.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | var markdown = require("../markdown"); 3 | 4 | module.exports = function mdHelper(compiler) { 5 | return function (options) { 6 | if (options.hash && options.hash.inlinesrc) { 7 | 8 | var out = new compiler.Handlebars.SafeString(markdown.processMardownContent({content: 9 | options.hash.inlinesrc 10 | }, compiler)); 11 | 12 | return new compiler.Handlebars.SafeString(compiler.safeCompile(out.string, compiler.frozen)); 13 | } 14 | return ""; 15 | }; 16 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/clean-dump.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | 3 | module.exports = function () { 4 | Handlebars.registerHelper("$cleandump", dumpHelperFn); 5 | }; 6 | 7 | function dumpHelperFn (item) { 8 | const stringified = JSON.stringify(item, null, 2); 9 | return new Handlebars.SafeString( 10 | `${stringified}` 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/compile.js: -------------------------------------------------------------------------------- 1 | module.exports = function compile(cbUtils) { 2 | return function (content) { 3 | return cbUtils.safeCompile(content, cbUtils.frozen); 4 | }; 5 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/current.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param compiler 3 | * @returns {Function} 4 | */ 5 | module.exports = function currentHelper (compiler) { 6 | return function (var1, options) { 7 | if (var1 === compiler.frozen.page.url) { 8 | return options.fn(this); 9 | } 10 | return options.inverse(this); 11 | }; 12 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/data.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | var errors = require("../../errors").inline; 3 | 4 | 5 | module.exports = function dataHelper(compiler) { 6 | 7 | return function () { 8 | 9 | var fileHelper = compiler.fileHelper; 10 | var inlineErrors = compiler.config.get("inlineErrors"); 11 | 12 | /** 13 | * Process options 14 | * @type {BrowserSync.options|*} 15 | */ 16 | var options = fileHelper.getOptions(arguments); 17 | 18 | /** 19 | * Process/verify params 20 | */ 21 | var params = fileHelper.processParams({ 22 | options: options, 23 | required: ["src", "as"], 24 | error: "You must provide both `src` & `as` parameters to the data helper", 25 | type: "data:params" 26 | }); 27 | 28 | if (!params && inlineErrors) { 29 | return new Handlebars.SafeString(errors["data:as"]()); 30 | } 31 | 32 | var fileout = fileHelper.retrieveFile({ 33 | options: options, 34 | params: params 35 | }); 36 | 37 | if (fileout.inlineError && inlineErrors) { 38 | return new Handlebars.SafeString(fileout.inlineError); 39 | } 40 | 41 | var data = fileHelper.retrieveFile({options: options, params: params}).data; 42 | 43 | var sandbox = require("lodash/lang/cloneDeep")(compiler.frozen); 44 | 45 | sandbox[params.as] = data; 46 | 47 | /** 48 | * Attempt to get a file from cache, or wherever 49 | */ 50 | return options.fn(sandbox); 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/dump.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | var utils = require("../../utils"); 3 | 4 | module.exports = function () { 5 | Handlebars.registerHelper("$dump", dumpHelperFn); 6 | }; 7 | 8 | function dumpHelperFn (item) { 9 | return new Handlebars.SafeString( 10 | utils.wrapCode( 11 | utils.escapeHtml(JSON.stringify(item, null, 4)) 12 | ) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/filters.js: -------------------------------------------------------------------------------- 1 | var markdown = require("../markdown"); 2 | var utils = require("../../utils"); 3 | 4 | var filters = { 5 | 6 | "hl": function (content, params) { 7 | 8 | var lang = utils.getLang(params); 9 | 10 | var highlighted = markdown.highlight(content, lang); 11 | 12 | if (lang === "") { 13 | highlighted = utils.escapeHtml(highlighted); 14 | } 15 | 16 | return utils.wrapCode( 17 | highlighted, lang 18 | ); 19 | } 20 | }; 21 | 22 | module.exports.filters = filters; 23 | 24 | var applyFilters = function (params, out, emitter) { 25 | 26 | var filter = params.filter; 27 | 28 | if (!filter) { 29 | return out; 30 | } 31 | 32 | if (filters[filter]) { 33 | return filters[filter](out, params); 34 | } 35 | 36 | if (!filters[filter]) { 37 | emitter.emit("_error", { 38 | _type: "filter", 39 | msg: "Filter does not exist: " + filter 40 | }); 41 | } 42 | 43 | return out; 44 | }; 45 | 46 | module.exports.applyFilters = applyFilters; 47 | 48 | function getFilter (name) { 49 | 50 | if (filters[name]) { 51 | return filters[name]; 52 | } 53 | 54 | // return a noop 55 | return function (content) { 56 | return content; 57 | }; 58 | } 59 | 60 | module.exports.getFilter = getFilter; -------------------------------------------------------------------------------- /lib/plugins/handlebars/hash.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | 3 | /** 4 | * @returns {Function} 5 | */ 6 | module.exports = function includeHelper (compiler) { 7 | 8 | return function () { 9 | 10 | var fileHelper = compiler.fileHelper; 11 | 12 | /** 13 | * Process options 14 | * @type {BrowserSync.options|*} 15 | */ 16 | var options = fileHelper.getOptions(arguments); 17 | 18 | /** 19 | * Process/verify params 20 | */ 21 | var params = fileHelper.processParams({ 22 | options: options, 23 | required: ["src"], 24 | error: "You must provide a `src` parameter to the hash helper", 25 | type: "hash:src" 26 | }); 27 | 28 | /** 29 | * Attempt to get a file from cache, or wherever 30 | */ 31 | 32 | /** 33 | * File content only 34 | * @type {string} 35 | */ 36 | var file = compiler.file.getFile({path: params.src}); 37 | 38 | /** 39 | * Return early if not retrievable 40 | */ 41 | if (!file) { 42 | return { 43 | inlineError: new Handlebars.SafeString(errors["file:notfound"](params.src)) 44 | }; 45 | } 46 | 47 | const crypto = require('crypto'); 48 | const hash = crypto.createHash('sha256').update(file.content).digest("hex"); 49 | 50 | /** 51 | * Just return compiled 52 | */ 53 | return new Handlebars.SafeString(hash.slice(0, 20)); 54 | }; 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/helpers.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | 3 | module.exports = function () { 4 | Handlebars.registerHelper("math", mathHelper); 5 | }; 6 | 7 | function mathHelper(lvalue, operator, rvalue) { 8 | lvalue = parseFloat(lvalue); 9 | rvalue = parseFloat(rvalue); 10 | return { 11 | "+": lvalue + rvalue, 12 | "-": lvalue - rvalue, 13 | "*": lvalue * rvalue, 14 | "/": lvalue / rvalue, 15 | "%": lvalue % rvalue 16 | }[operator]; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/hl.js: -------------------------------------------------------------------------------- 1 | var lang = require("lodash/lang"); 2 | var Handlebars = require("handlebars"); 3 | 4 | /** 5 | * @returns {Function} 6 | */ 7 | module.exports = function highlightHelper (compiler) { 8 | 9 | return function () { 10 | 11 | var fileHelper = compiler.fileHelper; 12 | 13 | /** 14 | * Process options 15 | * @type {BrowserSync.options|*} 16 | */ 17 | var options = fileHelper.getOptions(arguments); 18 | 19 | /** 20 | * Process/verify params 21 | */ 22 | var params = fileHelper.processParams({ 23 | options: options 24 | }); 25 | 26 | /** 27 | * Use src for retrieving a file, or process the block. 28 | */ 29 | var content = params.src 30 | ? fileHelper.retrieveFile({options: options, params: params}).content 31 | : lang.isFunction(options.fn) ? options.fn() : ""; 32 | 33 | if (!content) { 34 | return; 35 | } 36 | 37 | /** 38 | * Run the content through the HL filter 39 | */ 40 | return new Handlebars.SafeString( 41 | compiler.filter.apply({ 42 | name: "hl", 43 | content: content, 44 | params: params 45 | }) 46 | ); 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/if_eq.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | 3 | module.exports = function ifeqHelper(compiler) { 4 | return function (a, b, options) { 5 | if (a === b) { 6 | return options.fn(utils.prepareSandbox(this, compiler.frozen)); 7 | } 8 | return options.inverse(utils.prepareSandbox(this, compiler.frozen)); 9 | }; 10 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/if_not_eq.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | 3 | module.exports = function ifNotEqHelper(cbUtils) { 4 | return function (a, b, options) { 5 | if (a !== b) { 6 | return options.fn(utils.prepareSandbox(this, cbUtils.frozen)); 7 | } 8 | return options.inverse(utils.prepareSandbox(this, cbUtils.frozen)); 9 | }; 10 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/imgs.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | var errors = require("../../errors"); 3 | var path = require("../../core/path"); 4 | 5 | const validExts = [ 6 | '.bmp', 7 | '.gif', 8 | '.jpg', 9 | '.jpeg', 10 | '.png', 11 | '.TIFF', 12 | '.webp', 13 | '.svg' 14 | ]; 15 | 16 | function fromString (string) { 17 | const split = string.split(":"); 18 | if (split[0] === 'dir') { 19 | return split[1]; 20 | } 21 | return split[0]; 22 | } 23 | 24 | /** 25 | * @returns {Function} 26 | */ 27 | module.exports = function includeHelper (compiler) { 28 | 29 | return function () { 30 | 31 | var fileHelper = compiler.fileHelper; 32 | // const absoluteBase = path.resolve(compiler.config.get('base')); 33 | 34 | /** 35 | * Process options 36 | * @type {BrowserSync.options|*} 37 | */ 38 | var options = fileHelper.getOptions(arguments); 39 | 40 | /** 41 | * Process/verify params 42 | */ 43 | var params = fileHelper.processParams({ 44 | options: options, 45 | required: ["src"], 46 | error: "You must provide a `src` parameter to the hash helper", 47 | type: "imgs:src" 48 | }); 49 | 50 | const dirs = (function () { 51 | if (typeof params.src === "string") { 52 | return [fromString(params.src)]; 53 | } 54 | if (Array.isArray(params.src)) { 55 | return params.src.map(fromString); 56 | } 57 | })(); 58 | 59 | const images = fileHelper.scanDir(dirs[0], validExts); 60 | 61 | 62 | /** 63 | * Directory ERROR (missing ETC) 64 | */ 65 | if (images.errors.length) { 66 | return new Handlebars.SafeString(errors.inline["dir:notfound"](dirs[0])); 67 | } 68 | 69 | /** 70 | * Show error when no images found 71 | */ 72 | 73 | if (images.files.length === 0) { 74 | return new Handlebars.SafeString(errors.inline["imgs:none-found"](dirs[0], validExts)); 75 | } 76 | 77 | /** 78 | * Now group files based on indexing 79 | * 80 | * eg: 01-thumb 81 | * 01-large 82 | * @type {*|{}} 83 | */ 84 | const grouped = images.files.reduce(function (acc, item) { 85 | 86 | const indexed = item.parsed.name.match(/^(\d{0,5})-(.*)$/); 87 | 88 | if (indexed) { 89 | const name = indexed[1]; 90 | if (!acc[name]) { 91 | acc[name] = {}; 92 | } 93 | acc[name][indexed[2]] = decorate(item); 94 | } else { 95 | acc[item.parsed.name] = decorate(item); 96 | } 97 | 98 | return acc; 99 | 100 | }, {}); 101 | 102 | function decorate(item) { 103 | var sizeOf = require('image-size'); 104 | var dimensions = sizeOf(item.absolute); 105 | var relbase = compiler.config.get('base'); 106 | item.dimensions = dimensions; 107 | item.width = dimensions.width; 108 | item.height = dimensions.height; 109 | 110 | if (params.rel) { 111 | relbase = path.join(process.cwd(), params.rel); 112 | } 113 | 114 | item.src = '/' + path.relative(relbase, item.absolute); 115 | 116 | return item; 117 | } 118 | 119 | return new Handlebars.SafeString( 120 | Object.keys(grouped).reduce(function (string, key) { 121 | const item = grouped[key]; 122 | return string + options.fn(item); 123 | }, "") 124 | ); 125 | 126 | // console.log(compiler.config.get('base')); 127 | // console.log(dirs); 128 | 129 | // console.log(options); 130 | // /** 131 | // * Attempt to get a file from cache, or wherever 132 | // */ 133 | // 134 | // /** 135 | // * File content only 136 | // * @type {string} 137 | // */ 138 | // var file = compiler.file.getFile({path: params.src}); 139 | // 140 | // /** 141 | // * Return early if not retrievable 142 | // */ 143 | // if (!file) { 144 | // return { 145 | // inlineError: new Handlebars.SafeString(errors["file:notfound"](params.src)) 146 | // }; 147 | // } 148 | // 149 | // const crypto = require('crypto'); 150 | // const hash = crypto.createHash('sha256').update(file.content).digest("hex"); 151 | 152 | /** 153 | * Just return compiled 154 | */ 155 | return new Handlebars.SafeString("shane"); 156 | }; 157 | }; 158 | 159 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/inc.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require("handlebars"); 2 | 3 | /** 4 | * @returns {Function} 5 | */ 6 | module.exports = function includeHelper (compiler) { 7 | 8 | return function () { 9 | 10 | var fileHelper = compiler.fileHelper; 11 | 12 | /** 13 | * Process options 14 | * @type {BrowserSync.options|*} 15 | */ 16 | var options = fileHelper.getOptions(arguments); 17 | 18 | /** 19 | * Process/verify params 20 | */ 21 | var params = fileHelper.processParams({ 22 | options: options, 23 | required: ["src"], 24 | error: "You must provide a `src` parameter to the include helper", 25 | type: "include:src" 26 | }); 27 | 28 | /** 29 | * Attempt to get a file from cache, or wherever 30 | */ 31 | var fileout = fileHelper.retrieveFile({options: options, params: params}); 32 | 33 | /** 34 | * Allow inline errors or empty 35 | */ 36 | if (!fileout.content) { 37 | return fileout.inlineError || ""; 38 | } 39 | 40 | var content = fileout.compiled; 41 | 42 | if (params.filter) { 43 | content = compiler.filter.apply({ 44 | name: params.filter, 45 | content: fileout.compiled, 46 | params: params 47 | }); 48 | } else { 49 | /** 50 | * If pretty markup in config, pad lines 51 | */ 52 | if (compiler.config.get("prettyMarkup")) { 53 | return new Handlebars.SafeString(fileout.padded || ""); 54 | } 55 | } 56 | 57 | /** 58 | * Just return compiled 59 | */ 60 | return new Handlebars.SafeString(content || ""); 61 | }; 62 | }; 63 | 64 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/loop.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | 3 | module.exports = function loopHelper (cbUtils) { 4 | return function (var1, options) { 5 | if (!var1) { 6 | return ""; 7 | } 8 | return var1.reduce(function (all, item) { 9 | return all + options.fn(utils.prepareSandbox(item, cbUtils.frozen)); 10 | }, ""); 11 | }; 12 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/md.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | var markdown = require("../markdown"); 3 | 4 | module.exports = function mdHelper(compiler) { 5 | 6 | return function (options) { 7 | return markdown.processMardownContent({content: 8 | options.fn(utils.prepareSandbox(this, compiler.frozen)) 9 | }, compiler); 10 | }; 11 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/save.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns {Function} 3 | */ 4 | module.exports = function saveHelper () { 5 | 6 | //return function () { 7 | // 8 | // var args = Array.prototype.slice.call(arguments); 9 | // var options = args[args.length-1]; 10 | // var data = cbUtils.frozen; 11 | // var out; 12 | // 13 | // try { 14 | // out = fileStyle(options, cbUtils); 15 | // } catch (e) { 16 | // cbUtils.emitter.emit("_error", { 17 | // error: e, 18 | // _type: "save" 19 | // }); 20 | // return ""; 21 | // } 22 | // 23 | // if (!data._saved) { 24 | // data.saved = {}; 25 | // } 26 | // 27 | // data.saved[out.params.src] = out.processed; 28 | // 29 | // return ""; 30 | //}; 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /lib/plugins/handlebars/sections.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | 3 | /** 4 | * @returns {{section: section}} 5 | */ 6 | module.exports = function sectionsHelper(cbUtils) { 7 | 8 | var sections = {}; 9 | 10 | return { 11 | "section": function () { 12 | 13 | var args = Array.prototype.slice.call(arguments); 14 | var options = args[args.length-1]; 15 | 16 | var params = utils.processParams(options.hash || {}, options.data.root, cbUtils); 17 | 18 | if (!utils.verifyParams(params, ["name"])) { 19 | cbUtils.emitter.emit("log", { 20 | msg: "You must provide a `name` parameter to the section helper", 21 | _type: "params", 22 | _crossbow: options._crossbow 23 | }); 24 | return; 25 | } 26 | 27 | var content = options.fn(options.data.root); 28 | 29 | if (params.filter) { 30 | content = cbUtils.filter.apply({ 31 | name: params.filter, 32 | content: content, 33 | params: params 34 | }); 35 | } 36 | 37 | cbUtils.logger.debug("Saving content as section name: {magenta:%s}", params["name"]); 38 | sections[params["name"]] = content; 39 | }, 40 | "yield": require("./yield")(sections, cbUtils) 41 | }; 42 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/sep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * $sep helper. Provide a separator in between lists (omitting the last) 3 | * Given: posts = [{title: "Hello"}, {title: "World"}] 4 | * Template: 5 | * 6 | * {{#each posts}} 7 | * {{this.title}}{{$sep " - "}} 8 | * {{/each}} 9 | * 10 | * Output: 11 | * hello - world 12 | * 13 | * @param sep 14 | * @param options 15 | * @returns {string} 16 | */ 17 | module.exports = function (sep, options) { 18 | if (require("../../utils").isUndefined(options.data.last)) { 19 | return ""; 20 | } 21 | return options.data.last ? "" : sep; 22 | }; -------------------------------------------------------------------------------- /lib/plugins/handlebars/yield.js: -------------------------------------------------------------------------------- 1 | var utils = require("../../utils"); 2 | var Handlebars = require("handlebars"); 3 | 4 | /** 5 | * @param sections 6 | * @param cbUtils 7 | * @returns {Function} 8 | */ 9 | module.exports = function (sections, cbUtils) { 10 | 11 | return function () { 12 | 13 | var args = Array.prototype.slice.call(arguments); 14 | var options = args[args.length-1]; 15 | 16 | var params = utils.processParams(options.hash || {}, options.data.root, cbUtils); 17 | 18 | if (!utils.verifyParams(params, ["name"])) { 19 | cbUtils.emitter.emit("log", { 20 | msg: "You must provide a `name` parameter to the section helper", 21 | _type: "params", 22 | _crossbow: options._crossbow 23 | }); 24 | return; 25 | } 26 | cbUtils.logger.debug("Yielding section with name: {magenta:%s}", params["name"]); 27 | return new Handlebars.SafeString(sections[params["name"]] || ""); 28 | }; 29 | }; -------------------------------------------------------------------------------- /lib/plugins/markdown.js: -------------------------------------------------------------------------------- 1 | var marked = require("marked"); 2 | var highlight = require("highlight.js"); 3 | var utils = require("../utils"); 4 | var codeFences = require("../plugins/code-fences"); 5 | 6 | /** 7 | * Markdown Support 8 | * @param {{item: Map}} opts 9 | * @returns {*} 10 | */ 11 | var markdownTransform = function (opts) { 12 | 13 | var item = opts.item; 14 | var content = opts.content; 15 | var compiler = opts.compiler; 16 | var front = item.get("front").toJS() || {}; 17 | 18 | /** 19 | * Always look at front-matter args first 20 | */ 21 | if (!utils.isUndefined(front.markdown)) { 22 | return front.markdown 23 | ? processMardownContent({content: content}, compiler) 24 | : content; 25 | } 26 | 27 | /** 28 | * Has the flag been set to false in global config? 29 | */ 30 | if (!compiler.config.get("markdown")) { 31 | return content; 32 | } 33 | 34 | /** 35 | * Finally, has this file got an MD/markdown filename? 36 | */ 37 | if (item.getIn(["path", "ext"]).match(/\.md|\.markdown$/i)) { 38 | return processMardownContent({content: content}, compiler); 39 | } 40 | 41 | return content; 42 | }; 43 | 44 | /** 45 | * @type {markdownTransform} 46 | */ 47 | module.exports = markdownTransform; 48 | 49 | /** 50 | * @param {{content: string}} opts 51 | * @param {Compiler} compiler 52 | * @returns {string} 53 | */ 54 | function processMardownContent(opts, compiler) { 55 | 56 | var config = compiler.config; 57 | 58 | if (!config.get("highlight")) { 59 | marked.setOptions({highlight: false}); 60 | return marked(opts.content); 61 | } 62 | 63 | marked.setOptions({ 64 | highlight: (function () { 65 | return config.markdown && config.markdown.highlight 66 | ? config.markdown.highlight 67 | : function (code, lang) { 68 | return highlightSnippet(code, lang, compiler); 69 | }; 70 | })() 71 | }); 72 | 73 | return marked(opts.content); 74 | } 75 | 76 | module.exports.processMardownContent = processMardownContent; 77 | 78 | /** 79 | * @param code 80 | * @param [lang] 81 | * @returns {*} 82 | */ 83 | function highlightSnippet (code, language, compiler) { 84 | 85 | // Never auto-highlight any block. 86 | if (utils.isUndefined(language) || language === "") { 87 | return code; 88 | } 89 | 90 | var supportedLang = highlight.getLanguage(language); 91 | 92 | if (!supportedLang) { 93 | var msg = "Language `"+language+"` not supported by Highlight.js. "; 94 | msg += "You should ask them to support it - https://github.com/isagalaev/highlight.js"; 95 | var err = new Error(msg); 96 | err._type = "highlight:type"; 97 | err._crossbow = { 98 | file: compiler.item.get("key"), 99 | message: msg 100 | }; 101 | compiler.error(err); 102 | return code; 103 | } 104 | 105 | return highlight.highlight(language, code).value; 106 | } 107 | 108 | /** 109 | * @param {{params: object, content: string}} opts 110 | * @returns {string} 111 | */ 112 | function highlightString (opts, compiler) { 113 | 114 | var lang = utils.getLang(opts.params); 115 | 116 | var highlighted = highlightSnippet( 117 | opts.content, 118 | lang, 119 | compiler 120 | ); 121 | 122 | if (lang === "") { 123 | highlighted = utils.escapeHtml(highlighted); 124 | } 125 | 126 | highlighted = utils.wrapCode( 127 | highlighted, lang 128 | ); 129 | 130 | if (compiler.config.hasIn(['highlight', 'postProcess'])) { 131 | highlighted = compiler.config.getIn(['highlight', 'postProcess'])(highlighted, lang); 132 | } 133 | 134 | return highlighted; 135 | } 136 | 137 | module.exports.plugin = function (compiler) { 138 | return {}; 139 | }; 140 | 141 | module.exports.hooks = { 142 | contentTransforms: [ 143 | { 144 | when: "before layouts", 145 | fn: markdownTransform 146 | }, 147 | { 148 | when: "before templates", 149 | fn: codeFences 150 | } 151 | ], 152 | filters: { 153 | "md": processMardownContent, 154 | "hl": highlightString 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /lib/public/add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{type: [string], key: string, content: string, stat: [object]}} opts 3 | */ 4 | module.exports = function add (opts) { 5 | 6 | var compiler = this; 7 | 8 | opts.type = opts.type || "page"; 9 | 10 | var itemtype = compiler.types[opts.type]; 11 | 12 | if (itemtype) { 13 | var item = itemtype.input(compiler)(opts); 14 | return item.withMutations(function (item) { 15 | compiler.itemTransforms.forEach(function (trans) { 16 | if (trans.when === "before add") { 17 | trans.fn({item: item, compiler: compiler}); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | compiler.error(new TypeError(opts.type + " does not exist")); 24 | }; -------------------------------------------------------------------------------- /lib/public/addPage.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils"); 2 | var url = require("../url"); 3 | 4 | /** 5 | * @type {{types: {name: string, input: input}}} 6 | */ 7 | module.exports.hooks = { 8 | "frozenTransforms": [ 9 | { 10 | when: "before freeze", 11 | fn: function () {} 12 | } 13 | ], 14 | "types": { 15 | name: "page", 16 | input: input 17 | } 18 | }; 19 | 20 | function createPage(opts, compiler) { 21 | return compiler.preProcess(opts) 22 | .withMutations(function (item) { 23 | promoteFrontVars(item, compiler.config); 24 | addMissingUrl(item, compiler.config); 25 | addMissingTitle(item); 26 | addMissingType(item); 27 | }); 28 | } 29 | 30 | /** 31 | * Handle page input 32 | */ 33 | function input (compiler) { 34 | return function addPage (opts) { 35 | return compiler.cache.add( 36 | createPage(opts, compiler) 37 | ); 38 | }; 39 | } 40 | 41 | /** 42 | * @param item 43 | */ 44 | function promoteFrontVars(item) { 45 | item.mergeDeep(item.get("front")); 46 | } 47 | 48 | /** 49 | * Add a faux title if not in front matter 50 | * @param item 51 | */ 52 | function addMissingTitle (item) { 53 | if (!item.getIn(["front", "title"])) { 54 | item.set("title", 55 | utils.deslugify( 56 | item.getIn(["path", "name"]) 57 | ) 58 | ); 59 | } 60 | } 61 | 62 | /** 63 | * Default to a type of "page" 64 | * @param item 65 | */ 66 | function addMissingType (item) { 67 | if (!item.getIn(["front", "type"])) { 68 | item.set("type", "page"); 69 | } 70 | } 71 | 72 | /** 73 | * Default to a type of "page" 74 | * @param item 75 | * @param config 76 | */ 77 | function addMissingUrl (item, config) { 78 | item.set("url", url.keyToUrl(item.get("key"), config)); 79 | } -------------------------------------------------------------------------------- /lib/public/addPartial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {{types: {name: string, input: input}}} 3 | */ 4 | module.exports.hooks = { 5 | "types": { 6 | name: "partial", 7 | input: input 8 | } 9 | }; 10 | 11 | /** 12 | * @param {Compiler} compiler 13 | * @returns {Function} 14 | */ 15 | function input (compiler) { 16 | return function addPartial (opts) { 17 | return compiler.cache.add( 18 | compiler.preProcess(opts) 19 | .withMutations(function (item) { 20 | addMissingType(item); 21 | }) 22 | ); 23 | }; 24 | } 25 | 26 | /** 27 | * Default to a type of "partial" 28 | * @param item 29 | */ 30 | function addMissingType (item) { 31 | item.set("type", "partial"); 32 | } -------------------------------------------------------------------------------- /lib/public/compileAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {{cb: function}} opts 3 | */ 4 | module.exports = function compileAll (opts) { 5 | 6 | var compiler = this; 7 | opts.collection = compiler.cache.withoutType("partial"); 8 | compiler.compileMany(opts); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/public/compileMany.js: -------------------------------------------------------------------------------- 1 | var Immutable = require("immutable"); 2 | 3 | module.exports = function compileMany (opts) { 4 | 5 | var compiler = this; 6 | var items = opts.collection; 7 | if (Array.isArray(items)) { 8 | items = Immutable.List(items); 9 | } 10 | var compiled = []; 11 | var count = 0; 12 | 13 | /** 14 | * Run with first item 15 | */ 16 | runAgain(items.get(count)); 17 | 18 | function runAgain(item) { 19 | compiler.compile({ 20 | item: item, 21 | cb: function (err, out) { 22 | if (err) { 23 | return opts.cb(err); 24 | } 25 | count += 1; 26 | compiled.push(out); 27 | if (count === items.size) { 28 | opts.cb(null, Immutable.List(compiled)); 29 | } else { 30 | /** 31 | * Keep compiling with next item 32 | */ 33 | runAgain(items.get(count)); 34 | } 35 | } 36 | }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /lib/public/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global error handler 3 | * @param {Error} err 4 | * @returns {*} 5 | */ 6 | module.exports = function handleGlobalError (err) { 7 | 8 | var compiler = this; 9 | 10 | return compiler.config.get("errorHandler")(err, compiler); 11 | }; -------------------------------------------------------------------------------- /lib/public/freeze.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Freeze internal data, further mutations are not permitted 3 | * to any data following the calling of this function. 4 | */ 5 | module.exports = function freeze () { 6 | 7 | var compiler = this; 8 | 9 | if (compiler.defaultData.toJS) { 10 | compiler.defaultData = compiler.defaultData.toJS(); 11 | } 12 | 13 | compiler.logger.warn("Freezing data"); 14 | compiler.frozen = {}; 15 | 16 | Object.keys(compiler.types).forEach(function (type) { 17 | 18 | /** 19 | * Apply per-type filters, if any 20 | */ 21 | var items = compiler.cache.byType(type, compiler.config.getIn(["filters", "type:" + type])).toJS(); 22 | 23 | /** 24 | * Apply per-type sorting, if any 25 | */ 26 | if (compiler.types[type].sort) { 27 | items = compiler.types[type].sort(items); 28 | } 29 | 30 | /** 31 | * Now apply the plural name as a property - for example: pages, posts etc. 32 | */ 33 | compiler.frozen[type + "s"] = items; 34 | }); 35 | 36 | /** 37 | * 38 | */ 39 | compiler.mergeData(compiler.defaultData, compiler.frozen); 40 | 41 | /** 42 | * Transform Global data 43 | */ 44 | compiler.globalData = transFormGlobalData({ 45 | scope: "before templates", 46 | compiler: compiler, 47 | item: compiler.item, 48 | transforms: compiler.dataTransforms 49 | }); 50 | 51 | compiler.frozen = transFormFrozenData({ 52 | scope: "before templates", 53 | compiler: compiler, 54 | frozen: compiler.frozen, 55 | item: compiler.item, 56 | transforms: compiler.frozenTransforms 57 | }); 58 | 59 | /** 60 | * 61 | */ 62 | return compiler.frozen; 63 | }; 64 | 65 | /** 66 | * 67 | * @param {{compiler: Compiler, scope: string, item: Compiler.item}} opts 68 | * @returns {*} 69 | */ 70 | function transFormGlobalData (opts) { 71 | 72 | Object.keys(opts.transforms).forEach(function (key) { 73 | if (opts.transforms[key].when === opts.scope) { 74 | opts.compiler.globalData = opts.transforms[key].fn(opts); 75 | } 76 | }); 77 | 78 | return opts.compiler.globalData; 79 | } 80 | 81 | /** 82 | * @param {{compiler: Compiler, scope: string, item: Compiler.item}} opts 83 | * @returns {*} 84 | */ 85 | function transFormFrozenData (opts) { 86 | 87 | opts.transforms.forEach(function (transforms) { 88 | transforms.forEach(function (item) { 89 | if (item.when === opts.scope) { 90 | opts.compiler.frozen = item.fn(opts); 91 | } 92 | }); 93 | }); 94 | 95 | return opts.compiler.frozen; 96 | } 97 | 98 | /** 99 | * todo: dupe code 100 | * @param opts 101 | * @returns {*} 102 | */ 103 | function itemTransforms (opts) { 104 | opts.transforms.forEach(function (transforms) { 105 | transforms.forEach(function (item) { 106 | if (item.when === opts.scope) { 107 | opts.item = item.fn(opts); 108 | } 109 | }); 110 | }); 111 | return opts.item; 112 | } 113 | 114 | module.exports.itemTransforms = itemTransforms; -------------------------------------------------------------------------------- /lib/public/getErrorString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Error} err 3 | * @returns {*} 4 | */ 5 | module.exports = function getErrorString (err) { 6 | var errors = require("../errors").fails; 7 | if (errors[err._type]) { 8 | return errors[err._type]({error: err}); 9 | } 10 | return err.message || err; 11 | }; -------------------------------------------------------------------------------- /lib/public/getType.js: -------------------------------------------------------------------------------- 1 | var url = require("../url"); 2 | 3 | /** 4 | * From a path, and using config try to infer the type 5 | * @param {string} filepath 6 | * @returns {string} 7 | */ 8 | module.exports = function getType(filepath) { 9 | 10 | var compiler = this; 11 | 12 | var rel = url.makeFilepath(filepath, compiler.config).split("/")[0]; 13 | 14 | var match; 15 | 16 | compiler.config.get("dirs").forEach(function (value, key) { 17 | if (value === rel) { 18 | match = key; 19 | return true; 20 | } 21 | }); 22 | 23 | if (match) { 24 | match = match.split(":"); 25 | if (match[1]) { 26 | return match[1]; 27 | } 28 | } 29 | 30 | // no match if no _underscore, assume page 31 | 32 | if (rel[0] !== "_") { 33 | return "page"; 34 | } 35 | 36 | return "partial"; 37 | }; -------------------------------------------------------------------------------- /lib/public/mergeData.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var whitelist = ['.json', '.yaml', '.yml']; 4 | 5 | module.exports = function mergeData (inData, inObj) { 6 | 7 | var compiler = this; 8 | 9 | /** 10 | * Take a directory and build an object tree 11 | * representing data/directories as found from 12 | * recursively entering each dir 13 | * @param {string} dirpath 14 | * @param {object} obj 15 | * @returns {object} 16 | */ 17 | function addDataToObject (dirpath, obj) { 18 | var stat = fs.lstatSync(dirpath); 19 | var parsed = path.parse(dirpath); 20 | if (stat.isFile()) { 21 | if (whitelist.indexOf(parsed.ext) > -1) { 22 | obj[parsed.name] = compiler.file.getFile({path: dirpath}).data; 23 | } 24 | return obj; 25 | } 26 | if (stat.isDirectory()) { 27 | 28 | var files = fs.readdirSync(dirpath); 29 | var current = obj[path.basename(dirpath)] = {}; 30 | files.forEach(function (item) { 31 | addDataToObject(path.join(dirpath, item), current); 32 | }); 33 | return obj; 34 | } 35 | } 36 | 37 | /** 38 | * Read a single file from disk 39 | * @param {string} filepath 40 | * @returns {*} 41 | */ 42 | function addDataFromFile (filepath) { 43 | return compiler.file.getFile({path: filepath}).data; 44 | } 45 | 46 | /** 47 | * Read all valid files from a single Directory and load them 48 | * as an array (for easier looping) 49 | * @param {string} dirpath 50 | * @returns {array} 51 | */ 52 | function addDataFromFilesAsArray (dirpath) { 53 | var files = fs.readdirSync(dirpath); 54 | return files 55 | .filter(x => { 56 | const parsed = path.parse(x); 57 | return whitelist.indexOf(parsed.ext) > -1; 58 | }) 59 | .map(x => path.join(dirpath, x)) 60 | .map(x => { 61 | return compiler.file.getFile({path: x}).data; 62 | }) 63 | } 64 | 65 | Object.keys(inData).forEach(function (key) { 66 | var value = inData[key]; 67 | if (typeof value === "string") { 68 | // files 69 | var name = value.split(':'); 70 | if (name.length > 1) { 71 | var parsed = path.parse(name[1]); 72 | if (name[0] === 'file') { 73 | inObj[key] = addDataFromFile(name[1]); 74 | } 75 | if (name[0] === 'all') { 76 | var dirpath = compiler.file.resolvePath({path: name[1]}); 77 | inObj[key] = addDataToObject(dirpath, {})[parsed.name]; 78 | } 79 | if (name[0] === 'dir') { 80 | var dir = name[1]; 81 | var dirpath = compiler.file.resolvePath({path: dir}); 82 | inObj[key] = addDataFromFilesAsArray(dirpath); 83 | } 84 | return; 85 | } 86 | } 87 | 88 | inObj[key] = value; 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /lib/public/parseContent.js: -------------------------------------------------------------------------------- 1 | var path = require("../core/path"); 2 | var yaml = require("../yaml"); 3 | 4 | /** 5 | * @param {{key: string, content: string}} opts 6 | * @returns {{key: string, path: {root, dir, base, ext, name}}} 7 | */ 8 | module.exports = function parseContent (opts) { 9 | 10 | var compiler = this; 11 | 12 | var out = { 13 | key: opts.key, 14 | path: path.parse(opts.key) 15 | }; 16 | 17 | var split = yaml.readFrontMatter({ 18 | key: opts.key, 19 | content: opts.content, 20 | compiler: compiler 21 | }); 22 | 23 | out.front = split.front; 24 | out.content = split.content; 25 | 26 | return out; 27 | }; -------------------------------------------------------------------------------- /lib/public/preProcess.js: -------------------------------------------------------------------------------- 1 | var url = require("../url"); 2 | var Immutable = require("immutable"); 3 | 4 | module.exports = function preProcess (opts) { 5 | 6 | var compiler = this; 7 | 8 | var parsed = compiler.parseContent(opts); 9 | 10 | parsed.filepath = url.makeFilepath(opts.key, compiler.config); 11 | parsed.stat = opts.stat ? Immutable.Map(opts.stat) : false; 12 | 13 | return Immutable 14 | .fromJS(parsed); 15 | }; -------------------------------------------------------------------------------- /lib/public/transform.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module crossbow 3 | * @method transform 4 | * Add a content or data transform 5 | * 6 | * ``` 7 | * var site = crossbow.builder(); 8 | * site.transform({type: "content", when: "before templates", fn: function (opts) { 9 | * return opts.content + " Kittie"; 10 | * }}); 11 | * ``` 12 | * 13 | * @param {{fn: function, when: string, type: string}} opts 14 | */ 15 | module.exports = function (opts) { 16 | 17 | var compiler = this; 18 | 19 | if (opts.type === "content") { 20 | compiler.contentTransforms.push(opts); 21 | } 22 | 23 | if (opts.type === "item") { 24 | compiler.itemTransforms.push(opts); 25 | } 26 | 27 | return compiler; 28 | }; -------------------------------------------------------------------------------- /lib/url.js: -------------------------------------------------------------------------------- 1 | var path = require("./core/path"); 2 | var utils = require("./utils"); 3 | var util = exports; 4 | 5 | /** 6 | * @param filepath 7 | * @param config 8 | */ 9 | util.makeFilepath = function (filepath, config) { 10 | 11 | var parsed = path.parse(filepath); 12 | var base = config.get("base"); 13 | 14 | /** 15 | * Strip leading / 16 | */ 17 | if (parsed.dir.match(/^\//)) { 18 | parsed.dir = parsed.dir.slice(1); 19 | parsed.root = ""; 20 | } 21 | 22 | /** 23 | * Strip base 24 | */ 25 | if (utils.isString(base)) { 26 | var replaced = parsed.dir.replace(base, ""); 27 | parsed.dir = replaced.replace(/^\//, ""); 28 | } 29 | 30 | /** 31 | * Add ext 32 | * @type {string} 33 | */ 34 | parsed.ext = ".html"; 35 | 36 | /** 37 | * Add base 38 | */ 39 | parsed.base = parsed.name + parsed.ext; 40 | 41 | if (config.get("prettyUrls")) { 42 | if (parsed.base !== "index.html") { 43 | parsed.dir = path.join(parsed.dir, parsed.name); 44 | parsed.base = "index.html"; 45 | } 46 | } 47 | 48 | return path.format(parsed); 49 | }; 50 | 51 | /** 52 | * @param filepath 53 | * @param config 54 | */ 55 | util.makeUrlpath = function (filepath, config) { 56 | 57 | var parsed = path.parse(filepath); 58 | var out; 59 | 60 | if (config.get("prettyUrls")) { 61 | if (parsed.base === "index.html") { 62 | out = parsed.dir; 63 | } else { 64 | out = [parsed.dir, parsed.name].join("/"); 65 | } 66 | } else { 67 | out = [parsed.dir, parsed.base].join("/"); 68 | } 69 | 70 | return out.charAt(0) === "/" ? out : "/" + out; 71 | }; 72 | 73 | /** 74 | * @param key 75 | * @param config 76 | * @returns {*} 77 | */ 78 | util.keyToUrl = function (key, config) { 79 | return util.makeUrlpath(util.makeFilepath(key, config), config); 80 | }; 81 | 82 | /** 83 | * Create short keys to be used as unique Identifiers. 84 | * In: _posts/post1.md 85 | * Out: posts/post1.md 86 | * @param {String} key 87 | * @returns {String} 88 | * @param base 89 | */ 90 | util.makeShortKey = function (key, base) { 91 | return path.relative(base || process.cwd(), key); 92 | }; 93 | 94 | 95 | /** 96 | * 97 | * @param date 98 | * @param title 99 | * @param categories 100 | * @returns {{pretty: *}} 101 | */ 102 | //util.getReplacers = function (date, title, categories) { 103 | // 104 | // var obj = { 105 | // ":title": title, 106 | // ":filename": title 107 | // }; 108 | // 109 | // if (date) { 110 | // obj[":month"] = getMonth(date); 111 | // obj[":year"] = getYear(date); 112 | // obj[":day"] = getDay(date); 113 | // } 114 | // 115 | // if (categories && categories.length) { 116 | // obj[":categories"] = getCategories(categories); 117 | // obj[":category"] = getCategories(categories); 118 | // } 119 | // 120 | // return obj; 121 | //}; 122 | // 123 | ///** 124 | // * Remove any url sections that don't exist 125 | // * EG: /blog/:kats/:title -> /blog/:title 126 | // * @param template 127 | // * @param replacers 128 | // * @returns {string} 129 | // */ 130 | //util.filterReplacements = function (template, replacers) { 131 | // return template.split("/").filter(function (item) { 132 | // if (item.match(/^:/)) { 133 | // return replacers[item]; 134 | // } else { 135 | // return item.length; 136 | // } 137 | // }).join("/"); 138 | //}; 139 | 140 | ///** 141 | // * @param template 142 | // * @param initial 143 | // * @param replacers 144 | // * @returns {*} 145 | // */ 146 | //function replaceSections(template, initial, replacers) { 147 | // 148 | // template = filterReplacements(template, replacers); 149 | // 150 | // _.each(replacers, function (value, key) { 151 | // template = template.replace(key, value); 152 | // }); 153 | // 154 | // return template; 155 | //} 156 | // 157 | //function getMonth(key) { 158 | // return key.split("-")[1]; 159 | //} 160 | //function getDay(key) { 161 | // return key.split("-")[2]; 162 | //} 163 | //function getYear(key) { 164 | // return key.split("-")[0]; 165 | //} 166 | //function getCategories (items) { 167 | // return items.map(function (item) { 168 | // return item.slug; 169 | // }).join("/"); 170 | //} 171 | 172 | /** 173 | * Remove a date from front of key. 174 | * Format: YYYY-MM-DD 175 | * EG: 2014-06-21-post1 -> {date: 2014-06-21, url: post1} 176 | * @param {String} url 177 | * @returns {{date: string, url: string}|string} 178 | */ 179 | //util.extractDateFromKey = function (url) { 180 | // 181 | // var match = url.match(/^(\d{4}-\d{2}-\d{2})-(.+)/); 182 | // 183 | // var date; 184 | // var tempUrl = url; 185 | // 186 | // if (match && match.length) { 187 | // date = match[1]; 188 | // tempUrl = match[2]; 189 | // } 190 | // 191 | // return { 192 | // date: date, 193 | // url: tempUrl 194 | // }; 195 | //}; 196 | 197 | ///** 198 | // * Add forward slash if it doesn't yet exist. 199 | // * @param current 200 | // * @returns {*} 201 | // */ 202 | //function completeUrl(current) { 203 | // return current.match(/^\//) 204 | // ? current 205 | // : "/" + current; 206 | //} 207 | 208 | ///** 209 | // * Remove any forward slashes from file-paths 210 | // * @param current 211 | // * @returns {*} 212 | // */ 213 | //function completePath(current) { 214 | // return current.replace(/^\//, ""); 215 | //} -------------------------------------------------------------------------------- /lib/yaml.js: -------------------------------------------------------------------------------- 1 | var yaml = require("js-yaml"); 2 | 3 | /** 4 | * @type {{hasFrontMatter: hasFrontMatter, readFrontMatter: readFrontMatter, parseYaml: parseYaml, isYaml: isYaml}} 5 | */ 6 | module.exports = { 7 | hasFrontMatter: hasFrontMatter, 8 | readFrontMatter: readFrontMatter, 9 | parseYaml: parseYaml, 10 | isYaml: isYaml 11 | }; 12 | 13 | /** 14 | * Check if file has front matter 15 | * @param file 16 | * @returns {Array} 17 | */ 18 | function hasFrontMatter(file) { 19 | return file.match(/^---\n/); 20 | } 21 | 22 | /** 23 | * @param {{file: String, context: Object, filePath: String}} opts 24 | * @returns {*} 25 | */ 26 | function readFrontMatter(opts) { 27 | 28 | var frontYaml; 29 | var content = opts.content; 30 | 31 | if (/^---\n/.test(opts.content)) { 32 | 33 | var end = opts.content.search(/\n---/); 34 | 35 | if (end !== -1) { 36 | 37 | try { 38 | var slice = opts.content.slice(4, end + 1); 39 | frontYaml = yaml.load(slice); 40 | frontYaml.lineCount = slice.split("\n").length + 1; 41 | } catch (e) { 42 | e._type = "yaml"; 43 | e._crossbow = { 44 | line: e.mark.line + 1, 45 | file: opts.key, 46 | message: e.message 47 | 48 | }; 49 | opts.compiler.error(e); 50 | } 51 | 52 | content = opts.content.slice(end + 5); 53 | } 54 | } 55 | 56 | return { 57 | front: frontYaml || {}, 58 | content: content 59 | }; 60 | } 61 | 62 | /** 63 | * @param string 64 | * @returns {*} 65 | */ 66 | function parseYaml(string) { 67 | return yaml.safeLoad(string); 68 | } 69 | 70 | /** 71 | * @param key 72 | */ 73 | function isYaml(key) { 74 | return key.match(/yml$/i); 75 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crossbow-sites", 3 | "version": "1.0.1", 4 | "description": "Static Site Generator - built for speed & extendibility", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "jshint lib/*.js lib/plugins/**/*.js lib/public/*.js test/specs/**/*.js", 8 | "unit": "mocha --recursive test/specs", 9 | "test": "npm run unit" 10 | }, 11 | "homepage": "https://github.com/crossbow-js/crossbow-sites.js", 12 | "author": { 13 | "name": "Shane Osbourne" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/crossbow-js/crossbow-sites.js.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/crossbow-js/crossbow-sites.js/issues" 21 | }, 22 | "license": "ISC", 23 | "devDependencies": { 24 | "browser-sync": "^2.0.1", 25 | "bs-html-injector": "^1.1.0", 26 | "chai": "^1.9.1", 27 | "concat-stream": "^1.4.7", 28 | "d-logger": "^1.0.1", 29 | "gulp": "^3.8.8", 30 | "gulp-autoprefixer": "^2.0.0", 31 | "gulp-jscs": "^1.1.2", 32 | "gulp-util": "^3.0.0", 33 | "gulp-yuidoc": "^0.1.2", 34 | "jshint": "^2.6.0", 35 | "mocha": "*", 36 | "multiline": "^1.0.1", 37 | "no-abs": "0.0.0", 38 | "object-path": "^0.9.0", 39 | "rimraf": "^2.2.8", 40 | "sinon": "^1.10.3", 41 | "through2": "^0.6.3", 42 | "vinyl": "^0.4.6", 43 | "vinyl-fs": "^0.3.13" 44 | }, 45 | "dependencies": { 46 | "easy-extender": "^2.2.0", 47 | "eazy-logger": "^2.1.1", 48 | "gulp-util": "^3.0.3", 49 | "handlebars": "git://github.com/shakyshane/hb-fork", 50 | "highlight.js": "9.6.0", 51 | "image-size": "0.5.0", 52 | "immutable": "3.6.2", 53 | "js-yaml": "3.6.1", 54 | "lodash": "^3.4.0", 55 | "marked": "^0.3.3", 56 | "moment": "^2.9.0", 57 | "object-path": "^0.9.0", 58 | "q": "^1.2.0", 59 | "readable-stream": "^1.0.33", 60 | "slugify": "^0.1.0", 61 | "through2": "^0.6.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /plugins/stream.js: -------------------------------------------------------------------------------- 1 | var crossbow = require("../index"); 2 | var yaml = require("../lib/yaml"); 3 | var utils = crossbow.utils; 4 | var through2 = require("through2"); 5 | var gutil = require("gulp-util"); 6 | var File = gutil.File; 7 | var path = require("path"); 8 | var Immutable = require("immutable"); 9 | var _ = require("lodash"); 10 | var errors = require("../lib/errors").fails; 11 | 12 | /** 13 | * @returns {Function} 14 | */ 15 | module.exports = function (opts) { 16 | 17 | opts = opts || {}; 18 | opts.config = opts.config || {}; 19 | 20 | var files = {}; 21 | var stream; 22 | 23 | var site = opts.builder || crossbow.builder(opts); 24 | 25 | return through2.obj(function (file, enc, cb) { 26 | 27 | stream = this; 28 | 29 | if (file._contents) { 30 | var contents = file._contents.toString(); 31 | var relFilePath = file.path.replace(file.cwd, ""); 32 | relFilePath = relFilePath.replace(/^\//, ""); 33 | files[relFilePath] = {stat: file.stat, content: contents}; 34 | } 35 | cb(); 36 | 37 | }, function (cb) { 38 | 39 | var queue = []; 40 | var compileAll = false; 41 | 42 | Object.keys(files).forEach(function (key) { 43 | 44 | var existing = site.cache._items.has(key); 45 | var front; 46 | 47 | if (existing) { 48 | front = site.cache.byKey(key).get("front"); 49 | } 50 | 51 | var added = site.add({ 52 | type: site.getType(key), 53 | key: key, 54 | content: files[key].content, 55 | stat: files[key].stat 56 | }); 57 | 58 | if (front && !added.get("front").equals(front)) { 59 | compileAll = true; 60 | } 61 | 62 | queue.push(added); 63 | }); 64 | 65 | if (queue.length) { 66 | 67 | if (queue.some(function (item) { 68 | return item.get("type") === "partial"; 69 | }) || compileAll) { 70 | site.logger.debug("Re-compiling all items"); 71 | site.freeze(); 72 | site.compileAll({ 73 | cb: function (err, out) { 74 | if (err) { 75 | return console.log("ERROR"); 76 | } 77 | streampush(out, stream); 78 | cb(); 79 | } 80 | }); 81 | } else { 82 | var timestart = new Date().getTime(); 83 | site.compileMany({ 84 | collection: queue, 85 | cb: function (err, out) { 86 | if (err) { 87 | return console.log("ERROR"); 88 | } 89 | site.logger.debug("Compiling {yellow:%s} item%s took {yellow:%sms}", 90 | queue.length, 91 | queue.length > 1 ? "s" : "", 92 | new Date().getTime() - timestart 93 | ); 94 | streampush(out, stream); 95 | cb(); 96 | } 97 | }); 98 | } 99 | } 100 | }); 101 | }; 102 | 103 | /** 104 | * Push multiple files down stream 105 | * @param collection 106 | * @param stream 107 | */ 108 | function streampush(collection, stream) { 109 | collection.forEach(function (item) { 110 | stream.push(new File({ 111 | cwd: "./", 112 | base: "./", 113 | path: item.get("filepath"), 114 | contents: new Buffer(item.get("compiled")) 115 | })); 116 | }); 117 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
6 | Crossbow is a lightning fast Static Site Generator & Blog Engine 7 |
8 | 9 |10 | It gives you insane speed, unique features and the best development workflow on the planet! 11 |
12 | 13 |14 | So, if you want Jekyll-style thing, minus the all the Ruby and the slowness, then you've come to correct place! 15 | Checkout this 2 min screencast for a preview. Also, there's a gif below. 16 |
17 | 18 | #Crossbow [](https://travis-ci.org/shakyShane/crossbow.js) 19 | 20 | [](https://gitter.im/Crossbow-js/crossbow?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 21 | 22 |  23 | 24 | # Killer Features 25 | 26 | - **Speed** - In comparison to Jekyll, the most popular static site generator, our benchmark 27 | tests of `1000` markdown files with syntax highlighting, partials & layouts show Crossbow 28 | to be approx **8 - 13 times faster**. Seriously. 29 | 30 | - **Development Workflow First** - Crossbow has incredible HTML injecting technology that 31 | allows you write in markdown, compile into html (with highlighting) and inject into 32 | all connected browsers - all this in **less** than 20ms. This has to be seen to be believed. 33 | 34 | - **Incremental builds** - When in 'writing mode' only the file you are editing 35 | will be recompiled making the already blazing-fast process even quicker. It's really smart too, 36 | if you change a partial (that could affect many files) or change the front-matter (that could affect 37 | lists of post/pages etc) then the entire site will be rebuilt. This could take a upto a whopping 100ms... :p 38 | 39 | - **Pretty Markup** - We forked Handlebars to create a incredible feature where we perfectly preserve your indentation 40 | even when using partials. This is especially helpful when debuggin markup. 41 | 42 | We'll have more info and more docs coming soon, but just to keep you interested, why not try out the beta? 43 | 44 | ## Quick start 45 | Crossbow is a brand new project, as such we're light on documentation right now as we're 46 | still in beta. It's completely stable though and if you want to see what it's like 47 | to run a Crossbow static site/blog you can check out the [Crossbow Starter Blog](https://github.com/Crossbow-js/starter-blog) 48 | 49 | ## Install (beta) 50 | 51 | ```bash 52 | npm install crossbow --save-dev 53 | ``` 54 | 55 | ## Usage with Gulp 56 | ```js 57 | var gulp = require("gulp"); 58 | var crossbow = require("crossbow"); 59 | 60 | gulp.task("crossbow", function () { 61 | return gulp.src(["app/*.html"]) 62 | .pipe(crossbow.stream({ 63 | config: { 64 | base: "app" 65 | } 66 | })) 67 | .pipe(gulp.dest("_site")); 68 | }); 69 | ``` 70 | ## API usage 71 | 72 | ```js 73 | var site = crossbow.builder(); 74 | var page = site.add({type: "page", key: "index.html", content: "Hey I'm an index file
"}); 75 | site.compile({ 76 | item: page, 77 | cb: function (err, out) { 78 | if (err) { 79 | throw err 80 | } 81 | console.log(out.get("compiled")); 82 | } 83 | }); 84 | ``` 85 | 86 | ### Credits 87 | 88 | - Logo design by [@chrisbailey](https://dribbble.com/chris3ailey) 89 | 90 | ### Todo 91 | 92 | - [ ] Allow options for https://github.com/chjj/marked#overriding-renderer-methods 93 | - [x] Categories for posts 94 | - [x] Tags for posts 95 | - [ ] Permalinks in front-matter 96 | - [ ] Pagination 97 | -------------------------------------------------------------------------------- /test/fixtures/_config-corrupt.json: -------------------------------------------------------------------------------- 1 | { 2 | "css": "/css/main.css 3 | } -------------------------------------------------------------------------------- /test/fixtures/_config-corrupt.yml: -------------------------------------------------------------------------------- 1 | name: "shane" 2 | css "/css/main.css" -------------------------------------------------------------------------------- /test/fixtures/_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "css": "/css/main.css" 3 | } -------------------------------------------------------------------------------- /test/fixtures/_config.yml: -------------------------------------------------------------------------------- 1 | title: "Crossbow" 2 | name: "shane" 3 | css: "/css/main.css" -------------------------------------------------------------------------------- /test/fixtures/_includes/button.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/_includes/context.html: -------------------------------------------------------------------------------- 1 |2 | Site Title: {{site.title}} 3 |
4 |5 | Page Title: {{page.title}} 6 |
7 |8 | Page Url: {{page.url}} 9 |
10 |11 | Param Title: {{title}} 12 |
-------------------------------------------------------------------------------- /test/fixtures/_includes/heading.hbs: -------------------------------------------------------------------------------- 1 |Tags: 8 | {{#post.tags}} 9 | {{$_ 'startCase' this.name}} 10 | {{/post.tags}} 11 |
12 |Categories: 13 | {{#post.categories}} 14 | {{$_ 'startCase' this.name}} 15 | {{/post.categories}} 16 |
17 | {{content}} 18 |\{{inc}}
helper.
9 |
10 | Inc, short for **include**, is the way in which you `include`
11 | other pieces of content. Having worked with a number of programming languages
12 | in the past, the Crossbow author has developed what he believes to be a
13 | superior include/partial system.
14 |
15 | A simple example would be to break small chunks of code into re-usable components
16 |
17 |
18 | ```hbs
19 | \{{inc src="hero-section.html"}}
20 | \{{inc src="button.html"}}
21 | ```
22 |
23 | ### Sub directories
24 |
25 | Given that Crossbow first resolves all files from a **base**. You can
26 | easily traverse the file system in both ways to include the file you want.
27 |
28 | ```hbs
29 | \{{inc src="sub/directory/hero-section.html"}}
30 | \{{inc src="../../some/button.html"}}
31 | ```
32 |
33 | **Note**: In Crossbow, files are *always* resolved from the
34 | base, no matter which file you are currently editing. This is one of our
35 | strong opinions regarding file system access in Crossbow and it
36 | makes for a much simpler workflow as you always know exactly how to get
37 | to a file.
38 |
39 | ### Include params
40 |
41 | Say you have the following in a partial...
42 |
43 | ```hbs
44 | 45 | 46 |
47 | ``` 48 | 49 | ... if you wanted to make the "Click me" text dynamic, the easiest way 50 | would be to to replace the text with a variable... 51 | 52 | ```hbs 53 |54 | 55 |
56 | ``` 57 | 58 | ... now, when you include this partial, you can pass the value of `\{{text}}` 59 | as a parameter. 60 | 61 | ```hbs 62 | \{{inc src="button.html" text="Click me!"}} 63 | ``` 64 | 65 | ### Includes + Filters 66 | Image how cool it would be if you could include the content of another 67 | file, but also process the contents using a filter? No problem with Crossbow, 68 | this is a built-in feature. This example uses the built-in `hl` or `highlight` 69 | filter to provide syntax highlighting on the file contents. 70 | 71 | ```hbs 72 | \{{inc src="button.html" filter="hl" name="Highlighted"}} 73 | ``` 74 | 75 | results in... 76 | 77 | {{inc src="button.html" filter="hl" name="Highlighted"}} 78 | 79 | ### Inline errors 80 | 81 | We think there's nothing more frustrating that silent errors. That's 82 | why we provide error messages to the console as you work, but also in the 83 | html content - making it super easy to see where you've mis-spelled a file 84 | path. 85 | 86 | Here, I've deliberately tried to use a file that doesn't exist with the 87 | following: 88 | 89 | ```hbs 90 | {{inc src="does/not/exist.html"}} 91 | ``` 92 | 93 | ... which results in the following output in the html (don't worry, you 94 | can disable this feature, if you *really* want to. 95 | 96 | {{inc src="does/not/exist.html"}} 97 | 98 | 99 | ### Pretty markup 100 | 101 | Includes benefit from the pretty markup feature that's unique to Crossbow. 102 | This means you can format your html perfectly and the resulting markup will 103 | be exactly what you expect. -------------------------------------------------------------------------------- /test/fixtures/docs/layouts.md: -------------------------------------------------------------------------------- 1 | Crossbow has been built from the ground to support powerful layout features. 2 | By default, Layouts live in the `_layouts` directory (although you configure this) 3 | and are just regular `.html` or `.hbs` files that contain the special `\{{ content }}` tag. 4 | 5 | Here's a tiny example of a layout file... 6 | 7 | **_layouts/default.html** 8 | 9 | ```html 10 | 11 | 12 | 13 |Any content in this file will be processed as markdown.
30 | 31 |Because this file is named docs.md, all content will automatically be processed
32 | ``` 33 | 34 | ### Auto highlighting 35 | 36 | If you've ever used [Jekyll]({{site.links.jekyll}}) before to write 37 | blogs, or to provide syntax highlighting - you've probably seen something 38 | along these lines: 39 | 40 | ```hbs 41 | # How highlighting is done in jekyll 42 | 43 | {% highlight js %} 44 | var someJavascriptCode = "here"; 45 | {% highlightend %} 46 | ``` 47 | 48 | Code inside the special `{% tags %}` is plucked out and auto 49 | highlighted. This is pretty cool, however, you end up with those `{% tags %}` 50 | littered throughout your markdown file. 51 | 52 | In Crossbow, we offer the same highlighting functionality, except we do it 53 | automatically using the three back ticks technique made popular by github and 54 | other platforms. This means if you start a block with```js
, then
55 | then contents inside it will be auto-highlighted.
56 |
57 | ### Crossbow example
58 |
59 | Now for an example - to auto highlight a block of code in Crossbow, simply provide
60 | the following...
61 |
62 | ```js
63 | if (!opts.key) {
64 | opts.key = "crossbow-item-" + noKey;
65 | noKey += 1;
66 | }
67 | ```
68 | ... which ends up being this in the output, pretty nice right?
69 |
70 | ```js
71 | if (!opts.key) {
72 | opts.key = "crossbow-item-" + noKey;
73 | noKey += 1;
74 | }
75 | ```
76 |
77 | The real goal behind this automation, is to allow full, 100% portability
78 | of your markdown documents. You can literally use the exact same Markdown
79 | document to output to your blog, and will also read correctly if viewed
80 | by a platform such as github.
81 |
82 | ```js
83 | var shane = "kittie";
84 |
85 | ```
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/test/fixtures/highlighting-markdown.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: "default"
3 | markdown: true
4 | buttons: ["Primary"]
5 | ---
6 |
7 | ##{{page.title}}
8 |
9 | \{{#hl}}
helper with no lang
10 |
11 | {{#hl}}
12 |
13 | {{/hl}}
14 |
15 | Using \{{#hl lang='html'}}
to highlight
16 |
17 | {{#hl lang='html'}}
18 |
19 | {{/hl}}
20 |
21 | \{{hl src='button.html'}}
33 |
34 | {{hl src='button.html'}}
35 |
36 | {{#md}}
37 | ##Highlighting an external file relative to base
38 |
39 | All files includes are relative to the base setting, regardless of the current file you are working in.
40 |
41 | In addition, paths that begin with a dot `.` will be resolved before looking in includes directories
42 | etc. So, in this example if I wanted to include the contents of the `.gitignore`
43 | and the **base** is set to `test/fixtures`, all I need do is:
44 |
45 | {{/md}}
46 |
47 | \{{hl src='../../.gitignore'}}
48 |
49 | {{hl src='../../.gitignore'}}
50 |
--------------------------------------------------------------------------------
/test/fixtures/images.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{itemTitle}} is rad, {{page.url}}, {{site.title}}
" 13 | }); 14 | }); 15 | }); -------------------------------------------------------------------------------- /test/specs/add.partial.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var crossbow = require("../../index"); 3 | 4 | describe("Adding a partial", function() { 5 | 6 | it("Add & update 1 partial", function(done) { 7 | 8 | var site = crossbow.builder(); 9 | 10 | var content = "{{itemTitle}} is rad, {{page.url}}, {{site.title}}
"; 11 | var content2 = content + "another"; 12 | var key = "lay/default.hbs"; 13 | 14 | var index = site.add({type: "partial", key: key, content: content}); 15 | 16 | assert.equal(index.get("type"), "partial"); 17 | assert.equal(index.get("key"), key); 18 | assert.equal(index.get("content"), content); 19 | 20 | index = site.add({type: "partial", key: key, content: content2}); 21 | 22 | assert.equal(index.get("content"), content2); 23 | 24 | assert.deepEqual(site.cache.byType("partial").size, 1); 25 | 26 | assert.equal(site.cache.byType("partial").get(0).get("content"), content2); 27 | 28 | done(); 29 | 30 | }); 31 | }); -------------------------------------------------------------------------------- /test/specs/builder.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crossbow-js/crossbow-sites/3ed3380a61e012c5d0739064be3b34e29b55cbd2/test/specs/builder.js -------------------------------------------------------------------------------- /test/specs/cache.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var crossbow = require("../../index"); 3 | 4 | describe("Working with the cache", function() { 5 | 6 | it("should add/retrieve cache items manually", function() { 7 | 8 | var site = crossbow.builder(); 9 | 10 | var item = site.preProcess({key: "index.html", content: ""}); 11 | 12 | site.cache.add(item); 13 | 14 | var out = site.cache.byKey("index.html"); 15 | 16 | assert.isUndefined(out.get("title")); 17 | assert.equal(out.get("filepath"), "index.html"); 18 | assert.equal(out.get("content"), ""); 19 | }); 20 | 21 | it("should accept a filter function when retrieving by type", function() { 22 | 23 | var site = crossbow.builder(); 24 | 25 | var post = site.add({type: "post", key: "about.md", content: ""}); 26 | var draft = site.add({type: "post", key: "_about.md", content: ""}); 27 | 28 | site.cache.add(post); 29 | site.cache.add(draft); 30 | 31 | var out = site.cache.byType("post", function (item) { 32 | return item.getIn(["path", "name"])[0] !== "_"; 33 | }); 34 | 35 | assert.equal(out.size, 1); 36 | assert.equal(out.get(0).get("title"), "About"); 37 | assert.equal(out.get(0).get("key"), "about.md"); 38 | }); 39 | 40 | it("should add/retrieve cache items with page semantics", function() { 41 | 42 | var site = crossbow.builder(); 43 | 44 | site.add({key: "index.html", content: ""}); 45 | 46 | var out = site.cache.byKey("index.html"); 47 | 48 | assert.equal(out.get("content"), ""); 49 | 50 | site.add({key: "index.html", content: ""}); 51 | 52 | out = site.cache.byKey("index.html"); 53 | 54 | assert.equal(out.get("content"), ""); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/specs/cache.update.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var crossbow = require("../../index"); 3 | 4 | describe("Updating an item from the cache", function() { 5 | 6 | it("should update an item in the cache without creating dupes", function() { 7 | 8 | var site = crossbow.builder(); 9 | 10 | var post1 = site.add({type: "post", key: "about.md", content: "About page"}); 11 | var post2 = site.add({type: "post", key: "blog.md", content: "Blog page"}); 12 | 13 | site.cache.add(post1); 14 | site.cache.add(post2); 15 | 16 | var collection = site.cache.byType("post"); 17 | 18 | assert.equal(collection.get(0).get("content"), "About page"); 19 | assert.equal(collection.get(1).get("content"), "Blog page"); 20 | 21 | site.cache.add(post1.set("shane", "awesome")); 22 | 23 | collection = site.cache.byType("post"); 24 | assert.equal(collection.size, 2); 25 | 26 | assert.equal(collection.get(0).get("content"), "About page"); 27 | assert.equal(collection.get(0).get("shane"), "awesome"); 28 | assert.equal(collection.get(1).get("content"), "Blog page"); 29 | }); 30 | }); -------------------------------------------------------------------------------- /test/specs/compile.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var crossbow = require("../../index"); 3 | 4 | describe("Public method: compileAll()", function() { 5 | 6 | it("Add 2 pages, compile all and returns an Immutable List", function(done) { 7 | 8 | var site = crossbow.builder(); 9 | 10 | var index = site.add({key: "src/docs/index.html", content: "{{itemTitle}} is rad, {{page.url}}, {{site.title}}
"}); 11 | var about = site.add({key: "src/docs/about.html", content: "This is not a P
"); 64 | assert.include(out.get("compiled"), "This is not a P"); 65 | done(); 66 | } 67 | }); 68 | }); 69 | 70 | it("Does an include with a HL filter", function(done) { 71 | 72 | var site = crossbow.builder({ 73 | config: { 74 | base: "test/fixtures" 75 | } 76 | }); 77 | 78 | var item = site.add({key: "src/docs/index.html", content: "{{inc src='_includes/button.html' filter='hl'}}"}); 79 | 80 | site.freeze(); 81 | 82 | site.compile({ 83 | item: item, 84 | cb: function (err, out) { 85 | assert.include(out.get("compiled"), '<'); // jshint ignore:line
113 | done();
114 | }
115 | });
116 | });
117 | it("includes with correct context", function (done) {
118 |
119 | var site = crossbow.builder({
120 | config: {
121 | base: "test/fixtures"
122 | },
123 | data: {
124 | buttons: ["one", "two"],
125 | site: {
126 | title: "Crossbow"
127 | }
128 | }
129 | });
130 |
131 | var item = site.add({key: "src/docs/index.html", content: ":{{#each buttons}}{{inc src='context.html' title='Param title'}}{{/each}}:"});
132 |
133 | site.freeze();
134 |
135 | site.compile({
136 | item: item,
137 | cb: function (err, out) {
138 | var compiled = out.get("compiled");
139 | assert.include(compiled, "Site Title: Crossbow");
140 | assert.include(compiled, "Page Url: /src/docs/index.html");
141 | assert.include(compiled, "Param Title: Param title");
142 | done();
143 | }
144 | });
145 | });
146 | });
--------------------------------------------------------------------------------
/test/specs/key.to.url.js:
--------------------------------------------------------------------------------
1 | var path = require("../../lib/core/path");
2 | var assert = require("chai").assert;
3 | var crossbow = require("../../index");
4 | var url = require("../../lib/url");
5 | var merge = require("../../lib/config").merge;
6 |
7 | describe("e2e, making url from key", function() {
8 |
9 | it("urlpaths containing index", function() {
10 |
11 | assert.equal(url.keyToUrl("index.html", merge()), "/index.html");
12 | assert.equal(url.keyToUrl("app/index.html", merge()), "/app/index.html");
13 | });
14 | it("urlpaths containing index + Pretty urls", function() {
15 |
16 | assert.equal(url.keyToUrl("index.html", merge({prettyUrls: true})), "/");
17 | assert.equal(url.keyToUrl("app/index.html", merge({prettyUrls: true})), "/app");
18 | assert.equal(url.keyToUrl("app/about/index.html", merge({prettyUrls: true})), "/app/about");
19 | });
20 |
21 | it("urlpaths not containing index", function () {
22 |
23 | assert.equal(url.keyToUrl("about-this.html", merge()), "/about-this.html");
24 | assert.equal(url.keyToUrl("docs/about-this.html", merge()), "/docs/about-this.html");
25 | });
26 |
27 | it("urlpaths not containing index + Pretty urls", function () {
28 |
29 | assert.equal(url.keyToUrl("about.html", merge({prettyUrls: true})), "/about");
30 | assert.equal(url.keyToUrl("app/about.html", merge({prettyUrls: true})), "/app/about");
31 | assert.equal(url.keyToUrl("app/about/index2.html", merge({prettyUrls: true})), "/app/about/index2");
32 | });
33 |
34 |
35 | it("urlpaths not containing index + Pretty urls", function () {
36 |
37 | assert.equal(url.keyToUrl("_src/app/about.html", merge({prettyUrls: true, base: "_src"})), "/app/about");
38 | assert.equal(url.keyToUrl("_src/app/about/index2.html", merge({prettyUrls: true, base: "_src"})), "/app/about/index2");
39 | });
40 |
41 | describe("E2E post urls", function () {
42 |
43 | it("in default directory", function () {
44 |
45 | var key = "_posts/blog/post1.md";
46 | var expected = "/blog/post1.html";
47 |
48 | var site = crossbow.builder();
49 |
50 | var url = site.add({
51 | type: site.getType(key),
52 | key: key,
53 | content: "Sus"
54 | }).get("url");
55 |
56 | assert.equal(url, expected);
57 | });
58 |
59 | it("in default directory + base", function () {
60 | var key = "_src/_posts/blog/post1.md";
61 | var expected = "/blog/post1.html";
62 |
63 | var site = crossbow.builder({
64 | config: {
65 | base: "_src"
66 | }
67 | });
68 |
69 | var url = site.add({
70 | type: site.getType(key),
71 | key: key,
72 | content: "Sus"
73 | }).get("url");
74 |
75 | assert.equal(url, expected);
76 | });
77 | it("in default directory + base + pretty urls", function () {
78 | var key = "_src/_posts/blog/post1.md";
79 | var expected = "/blog/post1";
80 |
81 | var site = crossbow.builder({
82 | config: {
83 | base: "_src",
84 | prettyUrls: true
85 | }
86 | });
87 |
88 | var url = site.add({
89 | type: site.getType(key),
90 | key: key,
91 | content: "Sus"
92 | }).get("url");
93 |
94 | assert.equal(url, expected);
95 | });
96 | });
97 | });
--------------------------------------------------------------------------------
/test/specs/layouts.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Configuring layouts", function() {
5 |
6 | it("Uses NO layout by default", function(done) {
7 |
8 | var site = crossbow.builder({
9 | config: {
10 | base: "test/fixtures"
11 | }
12 | });
13 |
14 | var index = site.add({key: "src/docs/index-2.html", content: "{{pages.length}}
"});
15 |
16 | site.freeze();
17 |
18 | site.compile({
19 | item: index,
20 | cb: function (err, out) {
21 | assert.equal(out.get("compiled"), "1
");
22 | done();
23 | }
24 | });
25 | });
26 | it("Adds a layout using defaultLayout property", function(done) {
27 |
28 | var site = crossbow.builder({
29 | config: {
30 | base: "test/fixtures",
31 | defaultLayout: "default.html"
32 | }
33 | });
34 |
35 | var index = site.add({key: "src/docs/index-2.html", content: "{{pages.length}}
"});
36 |
37 | site.freeze();
38 |
39 | site.compile({
40 | item: index,
41 | cb: function (err, out) {
42 | assert.include(out.get("compiled"), "Index 2 ");
43 | done();
44 | }
45 | });
46 | });
47 | it("Add's a defaultLayouts property for type:page", function(done) {
48 |
49 | var site = crossbow.builder({
50 | config: {
51 | base: "test/fixtures",
52 | defaultLayouts: {
53 | "type:page": "default.html"
54 | }
55 | }
56 | });
57 |
58 | var index = site.add({key: "src/docs/index-2.html", content: "{{pages.length}}
"});
59 |
60 | site.freeze();
61 |
62 | site.compile({
63 | item: index,
64 | cb: function (err, out) {
65 | assert.include(out.get("compiled"), "Index 2 ");
66 | done();
67 | }
68 | });
69 | });
70 | it("Adds multiple defaultLayouts properties for type:page & type:post", function(done) {
71 |
72 | var site = crossbow.builder({
73 | config: {
74 | base: "test/fixtures",
75 | defaultLayouts: {
76 | "type:page": "default.html",
77 | "type:post": "post.hbs"
78 | }
79 | }
80 | });
81 |
82 | var index = site.add({key: "test/fixtures/docs/index-2.html", content: "{{posts.length}}
"});
83 | var post = site.add({type: "post", key: "test/fixtures/_posts/test.md", content: "Some amazing post"});
84 |
85 | site.compileAll({
86 | cb: function (err, out) {
87 | assert.include(out.get(0).get("compiled"), "");
88 | assert.include(out.get(1).get("compiled"), "");
89 | done();
90 | }
91 | });
92 | });
93 | });
--------------------------------------------------------------------------------
/test/specs/markdown/highlight.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Doing Highlight includes", function() {
5 |
6 | it("Can highlight an external file", function(done) {
7 |
8 | var site = crossbow.builder({
9 | config: {
10 | base: "test/fixtures"
11 | }
12 | });
13 |
14 | var item = site.add({key: "src/docs/index.html", content: "{{hl src='_includes/button.html'}}"});
15 |
16 | site.freeze();
17 |
18 | site.compile({
19 | item: item,
20 | cb: function (err, out) {
21 | assert.include(out.get("compiled"), '<{{showme}}{{{{/hl}}}}"});
36 |
37 | site.freeze();
38 |
39 | site.compile({
40 | item: item,
41 | cb: function (err, out) {
42 | assert.include(out.get("compiled"), '{{showme}}'); // jshint ignore:line
43 | done();
44 | }
45 | });
46 | });
47 |
48 | it("Does not blow up if language not available", function(done) {
49 |
50 | var site = crossbow.builder({
51 | config: {
52 | base: "test/fixtures"
53 | }
54 | });
55 |
56 | var item = site.add({key: "_posts/docs/layout.md", content: "``` whtevs\nvar shane;\n```"});
57 |
58 | site.freeze();
59 |
60 | site.compile({
61 | item: item,
62 | cb: function (err, out) {
63 | //console.log(out.get("compiled"));
64 | assert.include(out.get("compiled"), 'var shane;'); // jshint ignore:line
65 | done();
66 | }
67 | });
68 | });
69 | it("Gives good errors if language not available", function(done) {
70 |
71 | var site = crossbow.builder({
72 | config: {
73 | base: "test/fixtures",
74 | errorHandler: function (err) {
75 | assert.equal(err._crossbow.file, "_posts/docs/layout.md");
76 | assert.include(err.message, "Language `whtevs` not supported by Highlight.js");
77 | site.logger.error(site.getErrorString(err));
78 | done();
79 | }
80 | }
81 | });
82 |
83 | var item = site.add({key: "_posts/docs/layout.md", content: "``` whtevs\nvar shane;\n```"});
84 |
85 | site.freeze();
86 |
87 | site.compile({
88 | item: item,
89 | cb: function () {
90 |
91 | }
92 | });
93 | });
94 |
95 | it("Gives good errors if language not available via filter", function(done) {
96 |
97 | var site = crossbow.builder({
98 | config: {
99 | base: "test/fixtures",
100 | errorHandler: function (err) {
101 | console.log(err);
102 | assert.equal(err._crossbow.file, "index.html");
103 | assert.include(err.message, "Language `whtevs` not supported by Highlight.js");
104 | return done();
105 | }
106 | }
107 | });
108 |
109 | var item = site.add({key: "index.html", content: "{{#hl lang='whtevs'}}\nvar shane;\n{{/hl}}"});
110 |
111 | site.freeze();
112 |
113 | site.compile({
114 | item: item,
115 | cb: function () {}
116 | });
117 | });
118 | });
--------------------------------------------------------------------------------
/test/specs/padd.lines.js:
--------------------------------------------------------------------------------
1 | var utils = require("../../lib/utils");
2 | var pad = utils.paddLines;
3 | var assert = require("chai").assert;
4 |
5 | describe("Padding content", function() {
6 | it("does not padd with empty string", function() {
7 | var input = "line 1\nline 2";
8 | var actual = pad({content: input, count: 0});
9 | assert.equal(input, actual);
10 | });
11 | it("Does NOT padd the first line", function() {
12 | var input = "line 1\nline 2";
13 | var actual = pad({content: input, count: 4});
14 | var expected = "line 1\n line 2";
15 | assert.equal(expected, actual);
16 | });
17 | it("Pads multi lines", function () {
18 | var input = "line 1\nline 2\nline3";
19 | var actual = pad({content: input, count: 4});
20 | var expected = "line 1\n line 2\n line3";
21 | assert.equal(expected, actual);
22 | });
23 | it("Does not stip extra new lines by default", function () {
24 | var input = "line 1\nline 2\n\nline3";
25 | var actual = pad({content: input, count: 4});
26 | var expected = "line 1\n line 2\n\n line3";
27 | assert.equal(actual, expected);
28 | });
29 | it("Does stip extra new lines when given in config", function () {
30 | var input = "line 1\nline 2\n\nline3";
31 | var actual = pad({content: input, count: 4, stripNewLines: true});
32 | var expected = "line 1\n line 2\n line3";
33 | assert.equal(actual, expected);
34 | });
35 | });
--------------------------------------------------------------------------------
/test/specs/pages.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Adding muliple pages", function() {
5 |
6 | it("Add multi pages & compile with knowledge", function(done) {
7 |
8 | var site = crossbow.builder();
9 |
10 | var index = site.add({key: "src/docs/index.html", content: "{{pages.length}}
"});
11 | var about = site.add({key: "src/docs/about.html", content: "About page"});
12 |
13 | site.freeze();
14 |
15 | site.compile({
16 | item: index,
17 | cb: function (err, out) {
18 | assert.include(out.get("compiled"), "2
");
19 | done();
20 | }
21 | });
22 | });
23 | });
--------------------------------------------------------------------------------
/test/specs/pages/add.page.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding a page", function() {
5 |
6 | it("Add 1 page & compile", function(done) {
7 |
8 | var site = crossbow.builder();
9 |
10 | var index = site.add({key: "src/docs/index.html", content: "{{itemTitle}} is rad, {{page.url}}, {{site.title}}
"});
11 | var about = site.add({key: "src/docs/about.html", content: "About page"}); //jshint ignore:line
12 |
13 | assert.equal(index.get("key"), "src/docs/index.html");
14 | assert.equal(index.get("url"), "/src/docs/index.html");
15 | assert.equal(index.get("filepath"), "src/docs/index.html");
16 | assert.equal(index.get("title"), "Index");
17 |
18 | assert.equal(site.cache.byType("page").size, 2);
19 |
20 | var collection = site.cache.byType("page");
21 |
22 | assert.equal(collection.get(0).get("url"), "/src/docs/index.html");
23 | assert.equal(collection.get(0).get("title"), "Index");
24 | assert.equal(collection.get(1).get("url"), "/src/docs/about.html");
25 | assert.equal(collection.get(1).get("title"), "About");
26 |
27 | site.freeze();
28 |
29 | site.compile({
30 | item: index,
31 | data: {
32 | site: {
33 | title: "browsersync"
34 | },
35 | itemTitle: "Crossbow"
36 | },
37 | cb: function (err, out) {
38 | assert.include(out.get("compiled"), "Crossbow is rad, /src/docs/index.html, browsersync
");
39 | done();
40 | }
41 | });
42 | });
43 | it("Add 1 page & compile with layouts", function(done) {
44 |
45 | var site = crossbow.builder({
46 | config: {
47 | base: "test/fixtures"
48 | }
49 | });
50 |
51 | var item = site.add({key: "src/docs/index.html", content: "---\nlayout: 'docs.html'\n---\n{{itemTitle}} is rad, {{page.url}}, {{site.title}}
"});
52 |
53 | site.freeze();
54 |
55 | assert.equal(item.get("key"), "src/docs/index.html");
56 | assert.equal(item.get("url"), "/src/docs/index.html");
57 | assert.equal(item.get("title"), "Index");
58 |
59 | assert.equal(site.cache.byType("page").size, 1);
60 | assert.equal(site.cache.byType("page").get(0).get("title"), "Index");
61 | assert.equal(site.cache.byType("page").get(0).get("url"), "/src/docs/index.html");
62 |
63 | site.compile({
64 | item: item,
65 | data: {
66 | site: {
67 | title: "browsersync"
68 | },
69 | itemTitle: "Crossbow"
70 | },
71 | cb: function (err, out) {
72 |
73 | if (err) {
74 | done(err);
75 | } else {
76 | assert.include(out.get("compiled"), "Parent Layout
");
77 | assert.include(out.get("compiled"), "Crossbow is rad, /src/docs/index.html, browsersync
");
78 | done();
79 | }
80 | }
81 | });
82 | });
83 | });
--------------------------------------------------------------------------------
/test/specs/plugin.images.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("#imgs plugin", function() {
5 |
6 | it("works with relative dir", function(done) {
7 |
8 | var site = crossbow.builder({
9 | config: {
10 | base: "test/fixtures"
11 | },
12 | errorHandler: function (err) {
13 | console.log(err);
14 | }
15 | });
16 |
17 | var item = site.add({key: "output.html", content: `
18 | Images
19 | {{#imgs src="dir:images"}}
20 |
21 | {{/imgs}}
22 | `});
23 |
24 | site.freeze();
25 |
26 | site.compile({
27 | item: item,
28 | cb: function (err, out) {
29 | console.log(out.get("compiled"));
30 | // assert.include(out.get("compiled"), "
");
31 | done();
32 | }
33 | });
34 | });
35 | it("works with nested dir + width/height", function(done) {
36 |
37 | var site = crossbow.builder({
38 | config: {
39 | base: "test/fixtures"
40 | },
41 | errorHandler: function (err) {
42 | console.log(err);
43 | }
44 | });
45 |
46 | var item = site.add({key: "output.html", content: `
47 | Images
48 | {{#imgs src="dir:images/nested"}}
49 |
50 | {{/imgs}}
51 | `});
52 |
53 | site.freeze();
54 |
55 | site.compile({
56 | data: {
57 | items: ['kittie', 'jimbo']
58 | },
59 | item: item,
60 | cb: function (err, out) {
61 | assert.include(out.get("compiled"), 'Images
\n
');
62 | done();
63 | }
64 | });
65 | });
66 | it("Shows errors when directory not found", function(done) {
67 |
68 | var site = crossbow.builder({
69 | config: {
70 | base: "test/fixtures"
71 | },
72 | errorHandler: function (err) {
73 | console.log(err);
74 | }
75 | });
76 |
77 | var item = site.add({key: "output.html", content: `
78 | Images
79 | {{#imgs src="dir:nope"}}
80 |
81 | {{/imgs}}
82 | `});
83 |
84 | site.freeze();
85 |
86 | site.compile({
87 | data: {
88 | items: ['kittie', 'jimbo']
89 | },
90 | item: item,
91 | cb: function (err, out) {
92 | assert.include(out.get("compiled"), 'Directory not found: nope');
93 | done();
94 | }
95 | });
96 | });
97 | it("Shows errors when directory found, but no matching images were found", function(done) {
98 |
99 | var site = crossbow.builder({
100 | config: {
101 | base: "test/fixtures"
102 | },
103 | errorHandler: function (err) {
104 | console.log(err);
105 | }
106 | });
107 |
108 | var item = site.add({key: "output.html", content: `
109 | Images
110 | {{#imgs src="dir:images/no-images"}}
111 |
112 | {{/imgs}}
113 | `});
114 |
115 | site.freeze();
116 |
117 | site.compile({
118 | data: {
119 | items: ['kittie', 'jimbo']
120 | },
121 | item: item,
122 | cb: function (err, out) {
123 | assert.include(out.get("compiled"), 'No images found in images/no-images');
124 | done();
125 | }
126 | });
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/test/specs/posts/add.post.categories.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding a post with categories", function() {
5 | it("promotes front-matter categories to main obj", function() {
6 |
7 | var site = crossbow.builder({
8 | config: {
9 | base: "src",
10 | urlFormat: {
11 | "type:post": "/:filepath"
12 | }
13 | }
14 | });
15 |
16 | var index = site.add({
17 | type: "post",
18 | key: "src/_posts/javascript/whatevers.md",
19 | content: "---\ncategories: js, node\n---\n{{post.date}}"
20 | });
21 |
22 | assert.equal(index.get("key"), "src/_posts/javascript/whatevers.md");
23 | assert.equal(index.get("url"), "/javascript/whatevers.html");
24 | assert.deepEqual(index.get("_categories").toJS(), ["js", "node"]);
25 | });
26 | });
--------------------------------------------------------------------------------
/test/specs/posts/add.post.tags.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding a post with tags", function() {
5 | it("promotes front-matter tags to main obj", function() {
6 |
7 | var site = crossbow.builder({
8 | config: {
9 | base: "src",
10 | urlFormat: {
11 | "type:post": "/:filepath"
12 | }
13 | }
14 | });
15 |
16 | var index = site.add({
17 | type: "post",
18 | key: "src/_posts/javascript/whatevers.md",
19 | content: "---\ntags: js, node\n---\n{{post.date}}"
20 | });
21 |
22 | assert.equal(index.get("key"), "src/_posts/javascript/whatevers.md");
23 | assert.equal(index.get("url"), "/javascript/whatevers.html");
24 | assert.deepEqual(index.get("_tags").toJS(), ["js", "node"]);
25 | });
26 | });
--------------------------------------------------------------------------------
/test/specs/posts/add.post.this.categories.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding a post with categories", function() {
5 | it("knows about the current posts categories", function(done) {
6 |
7 | var site = crossbow.builder({
8 | config: {
9 | base: "src",
10 | markdown: false,
11 | urlFormat: {
12 | "type:post": "/:filepath"
13 | }
14 | }
15 | });
16 |
17 | site.add({
18 | type: "post",
19 | key: "src/_posts/javascript/whatevers.md",
20 | content: "---\ncategories: js, node\n---\nThis is the first post here"
21 | });
22 |
23 | site.add({
24 | type: "post",
25 | key: "src/_posts/javascript/whatevers2.md",
26 | content: "---\ncategories: js\n---\nThis is the second post here"
27 | });
28 |
29 | site.add({
30 | type: "post",
31 | key: "src/_posts/javascript/whatevers3.md",
32 | content: "---\ncategories: js\n---\nThis is the second post here"
33 | });
34 |
35 | var index = site.add({
36 | type: "page",
37 | key: "src/index.html",
38 | content: "{{#each categories}}\n{{this.name}}
\n\n{{#each this.items}} - {{this.url}}
\n{{/each}}
\n{{/each}}"
39 | });
40 |
41 | site.compile({
42 | item: index,
43 | cb: function (err, out) {
44 | var compiled = out.get("compiled");
45 | assert.include(compiled, "js
");
46 | assert.include(compiled, "node
");
47 | assert.include(compiled, "/javascript/whatevers.html ");
48 | assert.include(compiled, "/javascript/whatevers3.html ");
49 | assert.include(compiled, "/javascript/whatevers2.html ");
50 | done();
51 | }
52 | });
53 | });
54 | });
--------------------------------------------------------------------------------
/test/specs/posts/add.post.this.tags.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding a post with tags", function() {
5 | it("knows about the current posts tags", function(done) {
6 |
7 | var site = crossbow.builder({
8 | config: {
9 | base: "src",
10 | markdown: false,
11 | urlFormat: {
12 | "type:post": "/:filepath"
13 | }
14 | }
15 | });
16 |
17 | site.add({
18 | type: "post",
19 | key: "src/_posts/javascript/whatevers.md",
20 | content: "---\ntags: js, node\n---\nThis is the first post here"
21 | });
22 |
23 | site.add({
24 | type: "post",
25 | key: "src/_posts/javascript/whatevers2.md",
26 | content: "---\ntags: js\n---\nThis is the second post here"
27 | });
28 |
29 | site.add({
30 | type: "post",
31 | key: "src/_posts/javascript/whatevers3.md",
32 | content: "---\ntags: js\n---\nThis is the second post here"
33 | });
34 |
35 | var index = site.add({
36 | type: "page",
37 | key: "src/index.html",
38 | content: "{{#each tags}}\n{{this.name}}
\n\n{{#each this.items}} - {{this.url}}
\n{{/each}}
\n{{/each}}"
39 | });
40 |
41 | site.compile({
42 | item: index,
43 | cb: function (err, out) {
44 | var compiled = out.get("compiled");
45 | assert.include(compiled, "js
");
46 | assert.include(compiled, "node
");
47 | assert.include(compiled, "/javascript/whatevers.html ");
48 | assert.include(compiled, "/javascript/whatevers3.html ");
49 | assert.include(compiled, "/javascript/whatevers2.html ");
50 | done();
51 | }
52 | });
53 | });
54 | });
--------------------------------------------------------------------------------
/test/specs/posts/categories.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../../index");
3 |
4 | describe("Adding categories var to global vars", function() {
5 | it("promotes front-matter categories to main obj", function(done) {
6 |
7 | var site = crossbow.builder({
8 | config: {
9 | base: "src",
10 | urlFormat: {
11 | "type:post": "/:filepath"
12 | }
13 | }
14 | });
15 |
16 | site.add({
17 | type: "post",
18 | key: "src/_posts/javascript/whatevers.md",
19 | content: "---\ncategories: js, node, abstract, zindex css\n---\n{{post.date}}"
20 | });
21 |
22 | site.add({
23 | type: "post",
24 | key: "src/_posts/javascript/whatevers2.md",
25 | content: "---\ncategories: js, node\n---\n{{post.date}}"
26 | });
27 |
28 | var index = site.add({
29 | key: "src/index.html",
30 | content: "{{#categories}}{{this.name}}{{$sep ', '}}{{/categories}}"
31 | });
32 |
33 | site.freeze();
34 |
35 | assert.equal(site.frozen.categories.length, 4);
36 |
37 | site.compileAll({
38 | item: index,
39 | cb: function (err, out) {
40 | if (err) {
41 | return done(err);
42 | }
43 | assert.equal(out.get(2).get("compiled"), "abstract, js, node, zindex css");
44 | done();
45 | }
46 | });
47 | });
48 | });
--------------------------------------------------------------------------------
/test/specs/pre.process.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Pre-processing an item", function() {
5 |
6 | it("should return the key", function() {
7 |
8 | var site = crossbow.builder();
9 |
10 | var item = site.preProcess({key: "src/docs/index.html", content: "Shane is rad, {{page.url}}
"});
11 |
12 | assert.equal(item.get("key"), "src/docs/index.html");
13 | });
14 | it("should return the front matter + content", function() {
15 |
16 | var site = crossbow.builder();
17 |
18 | var item = site.preProcess({key: "src/docs/index.html", content: "Shane is rad, {{page.url}}
"});
19 |
20 | assert.equal(item.get("content"), "Shane is rad, {{page.url}}
");
21 | assert.deepEqual(item.get("front").toJS(), {});
22 | });
23 | it("should return the parsed path", function() {
24 |
25 | var site = crossbow.builder();
26 |
27 | var item = site.preProcess({key: "src/docs/index.html", content: "Shane is rad, {{page.url}}
"});
28 |
29 | var path = item.get("path").toJS();
30 |
31 | assert.equal(path.ext, ".html");
32 | assert.equal(path.base, "index.html");
33 | assert.equal(path.name, "index");
34 | assert.equal(path.dir, "src/docs");
35 | });
36 | it("should return the filepath", function() {
37 |
38 | var site = crossbow.builder();
39 |
40 | var item = site.preProcess({key: "src/about.html", content: "Shane is rad, {{page.url}}
"});
41 |
42 | assert.equal(item.get("filepath"), "src/about.html");
43 | });
44 | });
--------------------------------------------------------------------------------
/test/specs/pre.process.partials.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Pre-processing a random partial", function() {
5 |
6 | it("should return the key", function(done) {
7 |
8 | var site = crossbow.builder();
9 | var item = site.preProcess({key: "_layouts/main.html", content: "Content: {{content}}"});
10 |
11 | assert.equal(item.get("key"), "_layouts/main.html");
12 | assert.equal(item.get("content"), "Content: {{content}}");
13 |
14 | assert.isUndefined(item.get("type"));
15 |
16 | var page = site.add({key: "main.html", content: "Hello from main"});
17 |
18 | assert.equal(page.get("url"), "/main.html");
19 | assert.equal(page.get("key"), "main.html");
20 | assert.equal(page.get("filepath"), "main.html");
21 | assert.equal(page.get("title"), "Main");
22 |
23 | site.freeze();
24 |
25 | site.compile({
26 | item: page,
27 | cb: function (err, out) {
28 | assert.equal(out.get("compiled"), "Hello from main");
29 | done();
30 | }
31 | });
32 | });
33 | });
--------------------------------------------------------------------------------
/test/specs/simple.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Compiling in simple mode", function() {
5 |
6 | it("can render a template", function(done) {
7 |
8 | var content = "{{itemTitle}} is rad, {{page.url}}, {{site.title}}
";
9 | var content2 = content + "another";
10 | var key = "lay/default.hbs";
11 |
12 | var index = crossbow.compile({
13 | key: "lay/default.hbs",
14 | content: "{{name}} {{inc src='test/fixtures/_includes/button.html'}}",
15 | data: {
16 | name: "shane"
17 | },
18 | cb: function (err, out) {
19 | if (err) {
20 | return done(err);
21 | }
22 | assert.equal(out.get("compiled"), "shane ");
23 | done();
24 | }
25 | });
26 | });
27 | });
--------------------------------------------------------------------------------
/test/specs/stream/stream.cache.front.matter.js:
--------------------------------------------------------------------------------
1 | var crossbow = require("../../../index");
2 | var through = require("through2");
3 | var File = require("vinyl");
4 | var assert = require("chai").assert;
5 |
6 | var outpath = "./stream-out";
7 | var rimraf = require("rimraf").sync;
8 |
9 | var Readable = require("stream").Readable;
10 |
11 | function readStream (content) {
12 | var rs = new Readable({objectMode: true});
13 | rs._read = function () {
14 | if (content) {
15 | this.push(content);
16 | this.push(null);
17 | }
18 | };
19 | return rs;
20 | }
21 |
22 | rimraf(outpath);
23 |
24 | describe("E2E stream - posts with cache (2)", function() {
25 | it("knows when frontmatter has changed", function(done) {
26 |
27 | var site = crossbow.builder({
28 | config: {
29 | base: "test/fixtures"
30 | }
31 | });
32 |
33 | var expected = ["blog/post1.html", "blog/post2.html"];
34 | var out = [];
35 | var rs = readStream();
36 | var files = {};
37 |
38 | rs.push(new File({
39 | cwd: "test/fixtures",
40 | base: "/test/",
41 | path: "/test/fixtures/_posts/post1.md",
42 | contents: new Buffer("Post 1, first run!")
43 | }));
44 | rs.push(new File({
45 | cwd: "test/fixtures",
46 | base: "/test/",
47 | path: "/test/fixtures/_posts/post2.md",
48 | contents: new Buffer("Post 2, first run!")
49 | }));
50 | rs.push(null);
51 |
52 | rs.pipe(crossbow.stream({builder: site}))
53 | .pipe(through.obj(function (file, enc, cb) {
54 | files[file.path] = file.contents.toString();
55 | out.push(file.relative);
56 | cb();
57 | }, function (cb) {
58 | this.emit("end");
59 | cb();
60 | }))
61 | .on("end", function () {
62 | assert.equal(Object.keys(files).length, 2);
63 | assert.deepEqual(out, expected);
64 | assert.include(files["blog/post1.html"], "Post 1, first run!
");
65 | assert.include(files["blog/post2.html"], "Post 2, first run!
");
66 | //done();
67 | secondRun();
68 | });
69 |
70 | function secondRun() {
71 | var files = {};
72 | var rs2 = readStream();
73 | rs2.push(new File({
74 | cwd: "test/fixtures",
75 | base: "/test/",
76 | path: "/test/fixtures/_posts/post1.md",
77 | contents: new Buffer("---\ntitle: Crossbow\n---\nPost 1, first run!")
78 | }));
79 | rs2.push(null);
80 | rs2.pipe(crossbow.stream({builder: site}))
81 | .pipe(through.obj(function (file, enc, cb) {
82 | files[file.path] = file.contents.toString();
83 | cb();
84 | }, function (cb) {
85 | assert.equal(Object.keys(files).length, 2);
86 | //console.log(files);
87 | //assert.include(outfiles["blog/post1.html"], "Post 1, second run!
");
88 | //assert.equal(site.cache.byType("post").size, 2);
89 | //assert.include(site.cache.byType("post").get(0).get("compiled"), "Post 1, second run!
");
90 | //assert.include(site.cache.byType("post").get(1).get("compiled"), "Post 2, first run!
");
91 | this.emit("end");
92 | cb();
93 | done();
94 | }));
95 | }
96 | });
97 | });
--------------------------------------------------------------------------------
/test/specs/stream/stream.cache.js:
--------------------------------------------------------------------------------
1 | var crossbow = require("../../../index");
2 | var through = require("through2");
3 | var File = require("vinyl");
4 | var assert = require("chai").assert;
5 |
6 | var outpath = "./stream-out";
7 | var rimraf = require("rimraf").sync;
8 |
9 | var Readable = require("stream").Readable;
10 |
11 | function readStream (content) {
12 | var rs = new Readable({objectMode: true});
13 | rs._read = function () {
14 | if (content) {
15 | this.push(content);
16 | this.push(null);
17 | }
18 | };
19 | return rs;
20 | }
21 |
22 | rimraf(outpath);
23 |
24 | describe("E2E stream - posts with cache", function() {
25 |
26 | it("works with base config", function(done) {
27 |
28 | var site = crossbow.builder({
29 | config: {
30 | base: "test/fixtures"
31 | }
32 | });
33 |
34 | var expected = ["blog/post1.html", "blog/post2.html"];
35 | var out = [];
36 | var rs = readStream();
37 | var outfiles = {};
38 |
39 | rs.push(new File({
40 | cwd: "test/fixtures",
41 | base: "/test/",
42 | path: "/test/fixtures/_posts/post1.md",
43 | contents: new Buffer("Post 1, first run!")
44 | }));
45 | rs.push(new File({
46 | cwd: "test/fixtures",
47 | base: "/test/",
48 | path: "/test/fixtures/_posts/post2.md",
49 | contents: new Buffer("Post 2, first run!")
50 | }));
51 | rs.push(null);
52 |
53 | rs.pipe(crossbow.stream({builder: site}))
54 | .pipe(through.obj(function (file, enc, cb) {
55 | outfiles[file.path] = file.contents.toString();
56 | out.push(file.relative);
57 | cb();
58 | }, function (cb) {
59 | this.emit("end");
60 | cb();
61 | }))
62 | .on("end", function () {
63 | assert.deepEqual(out, expected);
64 | assert.include(outfiles["blog/post1.html"], "Post 1, first run!
");
65 | assert.include(outfiles["blog/post2.html"], "Post 2, first run!
");
66 | secondRun();
67 | });
68 |
69 | function secondRun() {
70 | var rs2 = readStream();
71 | rs2.push(new File({
72 | cwd: "test/fixtures",
73 | base: "/test/",
74 | path: "/test/fixtures/_posts/post1.md",
75 | contents: new Buffer("Post 1, second run!")
76 | }));
77 | rs2.push(null);
78 | rs2.pipe(crossbow.stream({builder: site}))
79 | .pipe(through.obj(function (file, enc, cb) {
80 | outfiles[file.path] = file.contents.toString();
81 | cb();
82 | }, function (cb) {
83 | assert.include(outfiles["blog/post1.html"], "Post 1, second run!
");
84 | assert.equal(site.cache.byType("post").size, 2);
85 | assert.include(site.cache.byType("post").get(0).get("compiled"), "Post 1, second run!
");
86 | assert.include(site.cache.byType("post").get(1).get("compiled"), "Post 2, first run!
");
87 | this.emit("end");
88 | cb();
89 | done();
90 | }));
91 | }
92 | });
93 | });
--------------------------------------------------------------------------------
/test/specs/stream/stream.js:
--------------------------------------------------------------------------------
1 | var crossbow = require("../../../index");
2 | var through = require("through2");
3 | var fs = require("vinyl-fs");
4 | var path = require("path");
5 | var assert = require("chai").assert;
6 |
7 | var outpath = "./stream-out";
8 | var rimraf = require("rimraf").sync;
9 |
10 | rimraf(outpath);
11 |
12 | function vp (dir, file) {
13 | return path.resolve(process.cwd(), outpath, dir, path.basename(file));
14 | }
15 |
16 | describe("E2E stream", function(){
17 | it("works with noe config", function(done){
18 | fs.src([
19 | "test/fixtures/*.html"
20 | ])
21 | .pipe(crossbow.stream())
22 | .pipe(fs.dest(outpath))
23 | .pipe(through.obj(function (file, enc, cb) {
24 | assert.equal(vp("test/fixtures", file.path), file.path);
25 | cb();
26 | }, function (cb) {
27 | this.emit("end");
28 | cb();
29 | }))
30 | .on("end", function () {
31 | done();
32 | });
33 | });
34 | it("works with base config", function(done) {
35 | var count = 0;
36 | fs.src([
37 | "test/fixtures/*.html"
38 | ])
39 | .pipe(crossbow.stream({
40 | config: {
41 | base: "test/fixtures",
42 | defaultLayout: "default.html"
43 | },
44 | data: {
45 | site: "file:_config.yml"
46 | }
47 | }))
48 | .pipe(fs.dest(outpath))
49 | .pipe(through.obj(function (file, enc, cb) {
50 | if (file._contents) {
51 | count += 1;
52 | assert.include(file._contents.toString(), ''); // jshint ignore:line
53 | }
54 | assert.equal(vp("", file.path), file.path);
55 | cb();
56 | }, function (cb) {
57 | this.emit("end");
58 | cb();
59 | }))
60 | .on("end", function () {
61 | assert.equal(count, 6);
62 | done();
63 | });
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/test/specs/stream/stream.posts.js:
--------------------------------------------------------------------------------
1 | var crossbow = require("../../../index");
2 | var through = require("through2");
3 | var fs = require("vinyl-fs");
4 | var assert = require("chai").assert;
5 |
6 | var outpath = "./stream-out";
7 | var rimraf = require("rimraf").sync;
8 |
9 | rimraf(outpath);
10 |
11 | describe("E2E stream - posts", function(){
12 | it("works with base config", function(done){
13 |
14 | var expected = ["blog/post1.html", "blog/post2.html"];
15 | var out = [];
16 |
17 | fs.src([
18 | "test/fixtures/_posts/**"
19 | ])
20 | .pipe(crossbow.stream({
21 | config: {
22 | base: "test/fixtures"
23 | }
24 | }))
25 | .pipe(through.obj(function (file, enc, cb) {
26 | out.push(file.relative);
27 | cb();
28 | }, function (cb) {
29 | this.emit("end");
30 | cb();
31 | }))
32 | .on("end", function () {
33 | assert.deepEqual(expected, out);
34 | done();
35 | });
36 | });
37 | it("works with Pretty urls config", function(done){
38 |
39 | var expected = ["blog/post1/index.html", "blog/post2/index.html"];
40 | var out = [];
41 |
42 | fs.src([
43 | "test/fixtures/_posts/**"
44 | ])
45 | .pipe(crossbow.stream({
46 | config: {
47 | base: "test/fixtures",
48 | prettyUrls: true
49 | }
50 | }))
51 | .pipe(through.obj(function (file, enc, cb) {
52 | out.push(file.relative);
53 | cb();
54 | }, function (cb) {
55 | this.emit("end");
56 | cb();
57 | }))
58 | .on("end", function () {
59 | assert.deepEqual(expected, out);
60 | done();
61 | });
62 | });
63 | it("works with Pretty urls config + urlFormat", function(done){
64 |
65 | var expected = ["shane/post1/index.html", "shane/post2/index.html"];
66 | var out = [];
67 |
68 | fs.src([
69 | "test/fixtures/_posts/**"
70 | ])
71 | .pipe(crossbow.stream({
72 | config: {
73 | base: "test/fixtures",
74 | prettyUrls: true,
75 | urlFormat: {
76 | "type:post": "/shane/:filename"
77 | }
78 | }
79 | }))
80 | .pipe(through.obj(function (file, enc, cb) {
81 | out.push(file.relative);
82 | cb();
83 | }, function (cb) {
84 | this.emit("end");
85 | cb();
86 | }))
87 | .on("end", function () {
88 | assert.deepEqual(expected, out);
89 | done();
90 | });
91 | });
92 | it("works with urlFormat", function(done) {
93 |
94 | var expected = ["shane/kittie/post1.html", "shane/kittie/post2.html"];
95 | var out = [];
96 |
97 | fs.src([
98 | "test/fixtures/_posts/**"
99 | ])
100 | .pipe(crossbow.stream({
101 | config: {
102 | base: "test/fixtures",
103 | urlFormat: {
104 | "type:post": "/shane/kittie/:filename"
105 | }
106 | }
107 | }))
108 | .pipe(through.obj(function (file, enc, cb) {
109 | out.push(file.relative);
110 | cb();
111 | }, function (cb) {
112 | this.emit("end");
113 | cb();
114 | }))
115 | .on("end", function () {
116 | assert.deepEqual(expected, out);
117 | done();
118 | });
119 | });
120 | });
--------------------------------------------------------------------------------
/test/specs/transforms.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Content transforms", function() {
5 |
6 | it("Adds a content transform", function(done) {
7 |
8 | var site = crossbow.builder();
9 |
10 | site.transform({type: "content", when: "before templates", fn: function (opts) {
11 | return opts.content + " Kittie";
12 | }}).compile({
13 | content: "Hi",
14 | key: "page1.html",
15 | cb: function (err, out) {
16 | assert.equal(out.get("compiled"), "Hi Kittie");
17 | done();
18 | }
19 | });
20 | });
21 |
22 | it("Adds an item transform", function(done) {
23 |
24 | var site = crossbow.builder();
25 |
26 | site.transform({type: "item", when: "before add", fn: function (opts) {
27 | return opts.item.set("url", "/shane");
28 | }});
29 |
30 | site.compile({
31 | content: "Hi",
32 | key: "page1.html",
33 | cb: function (err, out) {
34 | assert.equal(out.get("url"), "/shane");
35 | done();
36 | }
37 | });
38 | });
39 | });
--------------------------------------------------------------------------------
/test/specs/types.js:
--------------------------------------------------------------------------------
1 | var assert = require("chai").assert;
2 | var crossbow = require("../../index");
3 |
4 | describe("Workign with Types", function() {
5 |
6 | it("Can determine a `post type` using the filepath & base", function(done) {
7 |
8 | var type = crossbow.builder({
9 | config: {
10 | base: "src"
11 | }
12 | }).getType("src/_posts/test.md");
13 |
14 | assert.equal(type, "post");
15 |
16 | done();
17 | });
18 |
19 | it("Can determine a `post type` using the filepath & base", function(done) {
20 |
21 | var type = crossbow.builder({
22 | config: {
23 | base: "src",
24 | dirs: {
25 | "type:post": "_blog"
26 | }
27 | }
28 | }).getType("src/_blog/test.md");
29 |
30 | assert.equal(type, "post");
31 |
32 | done();
33 | });
34 |
35 | it("Can determine a `partial type` when type:type syntax not being used", function(done) {
36 |
37 | var type = crossbow.builder().getType("_layouts/test.hbs");
38 | assert.equal(type, "partial");
39 | done();
40 | });
41 |
42 | it("Can determine a `page type` file has MD extension", function(done) {
43 | var type = crossbow.builder({config: {base: "src"}}).getType("src/docs/test.md");
44 | assert.equal(type, "page");
45 | done();
46 | });
47 | });
--------------------------------------------------------------------------------
/test/specs/urlpath.js:
--------------------------------------------------------------------------------
1 | var path = require("../../lib/core/path");
2 | var assert = require("chai").assert;
3 | var crossbow = require("../../index");
4 | var url = require("../../lib/url");
5 | var merge = require("../../lib/config").merge;
6 |
7 | describe("Resolving url paths", function() {
8 | it("urlpaths", function() {
9 | var filepath = url.makeFilepath("_src/app/about.html", merge({base: "_src"}));
10 | assert.equal(url.makeUrlpath(filepath, merge()), "/app/about.html");
11 | });
12 | it("urlpaths with pretty urls", function() {
13 | assert.equal(url.makeUrlpath("app/index.html", merge({prettyUrls: true})), "/app");
14 | assert.equal(url.makeUrlpath("app/shane/index.html", merge({prettyUrls: true})), "/app/shane");
15 | assert.equal(url.makeUrlpath("app/shane/index_.html", merge({prettyUrls: true})), "/app/shane/index_");
16 | assert.equal(url.makeUrlpath("app/shane/kittie/index.html", merge({prettyUrls: true})), "/app/shane/kittie");
17 | assert.equal(url.makeUrlpath("app/shane/kittie/about.html", merge({prettyUrls: true})), "/app/shane/kittie/about");
18 | });
19 | it("urlpaths with pretty urls + non-index base", function() {
20 |
21 | var filepath = url.makeFilepath("_src/app/shane/kittie/index2.html", merge({base: "_src"}));
22 |
23 | assert.equal(filepath, "app/shane/kittie/index2.html");
24 |
25 | assert.equal(url.makeUrlpath(filepath, merge({prettyUrls: false})), "/app/shane/kittie/index2.html");
26 | assert.equal(url.makeUrlpath(filepath, merge({prettyUrls: true})), "/app/shane/kittie/index2");
27 | });
28 | it("urlpaths with pretty urls + non-index base", function() {
29 | var filepath = url.makeFilepath("_src/app/shane/kittie/index2.html", merge({base: "_src"}));
30 | assert.equal(filepath, "app/shane/kittie/index2.html");
31 | assert.equal(url.makeUrlpath(filepath, merge({prettyUrls: true})), "/app/shane/kittie/index2");
32 | });
33 | });
--------------------------------------------------------------------------------