├── .gitignore ├── assets └── css │ └── site.css ├── views ├── page.ejs └── index.ejs ├── package.json ├── lib ├── yaml-indent-documents.js ├── page │ ├── index.js │ └── render.js └── render.js ├── Gruntfile.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | -------------------------------------------------------------------------------- /assets/css/site.css: -------------------------------------------------------------------------------- 1 | #content { 2 | width: 600px; 3 | } 4 | 5 | .highlight { 6 | border: 1px solid #ccc; 7 | background: #eee; 8 | padding: 0 0.5em; 9 | } 10 | -------------------------------------------------------------------------------- /views/page.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= meta.title %> 6 | 7 | 8 | 9 |
10 | Index 11 |

<%= meta.title %>

12 | <%- render('content') %> 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Index 6 | 7 | 8 | 9 |
10 |

Index

11 | <% pages.forEach(function(page) { %> 12 | <%= dateformat(page.meta.date, 'mm/dd/yyyy') %> <%= page.meta.title %>
13 | <% }) %> 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "benalman.com", 3 | "version": "0.1.0", 4 | "description": "Website?", 5 | "main": "index.js", 6 | "dependencies": { 7 | "github-flavored-markdown": "~1.0.1", 8 | "js-yaml": "~1.0.2", 9 | "gitteh": "~0.1.0", 10 | "ejs": "~0.8.3", 11 | "dateformat": "~1.0.2-1.2.3" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "grunt test" 16 | }, 17 | "repository": "", 18 | "author": "", 19 | "license": "BSD" 20 | } 21 | -------------------------------------------------------------------------------- /lib/yaml-indent-documents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * yaml-literal-hack 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | 13 | var yaml = module.exports = require('js-yaml'); 14 | 15 | // https://github.com/nodeca/js-yaml/issues/53 16 | var hack = function(src) { 17 | return String(src).split(/^\-{3}/gm).map(function(s) { 18 | var matches = s.match(/^\s+([\|\^].*?)\n([\s\S]*?)\n$/); 19 | if (!matches) { return s; } 20 | var literal = matches[1]; 21 | var indented = matches[2].split('\n').map(function(s) { 22 | return ' ' + s; 23 | }).join('\n'); 24 | return '\n__LITERALLY_AWESOME__: ' + literal + '\n' + indented + '\n'; 25 | }).join('---'); 26 | }; 27 | 28 | var unhack = function(docs) { 29 | return docs.map(function(doc) { 30 | return doc && doc.__LITERALLY_AWESOME__ || doc; 31 | }); 32 | }; 33 | 34 | // This seems to do what I want it to. 35 | yaml.parseDocs = function(src) { 36 | var docs = []; 37 | yaml.loadAll(hack(src), function(doc) { 38 | docs.push(doc); 39 | }); 40 | return unhack(docs); 41 | }; 42 | 43 | // So does this. 44 | yaml.parseDocsFile = function(filepath) { 45 | return yaml.parseDocs(fs.readFileSync(filepath)); 46 | }; 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * a new benalman.com, maybe 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = function(grunt) { 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | // Task configuration. 16 | jshint: { 17 | options: { 18 | curly: true, 19 | eqeqeq: true, 20 | immed: true, 21 | latedef: true, 22 | newcap: true, 23 | noarg: true, 24 | sub: true, 25 | undef: true, 26 | unused: true, 27 | boss: true, 28 | eqnull: true, 29 | es5: true, 30 | node: true, 31 | }, 32 | gruntfile: { 33 | src: 'Gruntfile.js' 34 | }, 35 | lib_test: { 36 | src: ['lib/**/*.js', 'test/**/*.js'] 37 | } 38 | }, 39 | nodeunit: { 40 | files: ['test/**/*.js'] 41 | }, 42 | watch: { 43 | gruntfile: { 44 | files: '<%= jshint.gruntfile.src %>', 45 | tasks: ['jshint:gruntfile'] 46 | }, 47 | lib_test: { 48 | files: '<%= jshint.lib_test.src %>', 49 | tasks: ['jshint:lib_test', 'nodeunit'] 50 | } 51 | } 52 | }); 53 | 54 | // Default task. 55 | grunt.registerTask('default', ['jshint', 'nodeunit']); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * a new benalman.com, maybe 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var ejs = require('ejs'); 15 | var dateformat = require('dateformat'); 16 | 17 | var Index = require('./lib/page/index').Index; 18 | 19 | var src = '../benalman.com-content/new'; 20 | var indices = fs.readdirSync(src).map(function(dirname) { 21 | // console.log(dirname); 22 | var abspath = path.resolve(src, dirname, 'index.md'); 23 | var index = new Index(abspath); 24 | var dest = path.resolve('./build', dirname + '.html'); 25 | 26 | var options = Object.create(index); 27 | options.filename = './views/page.js'; 28 | var html = ejs.render('<% include page %>', options); 29 | fs.writeFileSync(dest, html); 30 | 31 | return {href: dirname + '.html', meta: index.meta}; 32 | }).reverse(); 33 | 34 | var options = { 35 | dateformat: dateformat, 36 | pages: indices, 37 | filename: './views/index.js' 38 | }; 39 | var html = ejs.render('<% include index %>', options); 40 | fs.writeFileSync('build/index.html', html); 41 | 42 | // // var page = page.create('../benalman.com-content/new/jquery-throttle-debounce'); 43 | // var page = page.create('../benalman.com-content/new/iife'); 44 | // // var page = page.create('./new/'); 45 | 46 | // console.log(page.index); 47 | // //console.log(page.documents); 48 | // console.log(page.meta); 49 | // console.log('title', page.meta.title); 50 | // console.log('tags', page.meta.tags); 51 | // page.meta.foo = 123; 52 | // console.log('foo', page.meta.foo); 53 | 54 | // var options = Object.create(page); 55 | // options.filename = './views/page.js'; 56 | // var html = ejs.render('<% include page %>', options); 57 | // // console.log(html); 58 | 59 | // fs.writeFileSync('out.html', html); 60 | -------------------------------------------------------------------------------- /lib/page/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * a new benalman.com, maybe 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var path = require('path'); 12 | var yaml = require('../yaml-indent-documents'); 13 | var render = require('./render'); 14 | 15 | var Index = exports.Index = function(indexfile) { 16 | this.indexfile = path.resolve(indexfile); 17 | this.basepath = path.dirname(this.indexfile); 18 | }; 19 | 20 | Index.prototype.toString = function() { 21 | return '<>'; 22 | }; 23 | 24 | // An array of parsed YAML documents. 25 | Object.defineProperties(Index.prototype, { 26 | _documents: { 27 | writable: true, 28 | configurable: true, 29 | }, 30 | documents: { 31 | enumerable: true, 32 | get: function() { 33 | if (!this._documents) { 34 | this._documents = yaml.parseDocsFile(this.indexfile); 35 | } 36 | return this._documents; 37 | }, 38 | }, 39 | }); 40 | 41 | // Metadata from the first YAML document that's an object, otherwise 42 | // an empty object. 43 | Object.defineProperties(Index.prototype, { 44 | _meta: { 45 | writable: true, 46 | configurable: true, 47 | }, 48 | meta: { 49 | enumerable: true, 50 | get: function() { 51 | var i; 52 | if (!this._meta) { 53 | this._meta = {}; 54 | for (i = 0; i < this.documents.length; i++) { 55 | if (this.documents[i] instanceof Object) { 56 | this._meta = this.documents[i]; 57 | break; 58 | } 59 | } 60 | } 61 | return this._meta; 62 | }, 63 | }, 64 | }); 65 | 66 | // An array of all YAML documents that are strings. 67 | Object.defineProperty(Index.prototype, 'contents', { 68 | enumerable: true, 69 | get: function() { 70 | return this.documents.filter(function(document) { 71 | return typeof document === 'string'; 72 | }); 73 | }, 74 | }); 75 | 76 | // All content, concatenated together. 77 | Object.defineProperty(Index.prototype, 'content', { 78 | enumerable: true, 79 | get: function() { 80 | return this.contents.join('\n'); 81 | }, 82 | }); 83 | 84 | // Excerpt. first string YAML doc, if there are multiples, otherwise an 85 | // empty string (TODO: grab first N chars from body?) 86 | Object.defineProperty(Index.prototype, 'excerpt', { 87 | enumerable: true, 88 | get: function() { 89 | return this.contents.length > 1 ? this.contents[0] : ''; 90 | }, 91 | }); 92 | 93 | // Body content. first string YAML doc, if there is only one, otherwise 94 | // all but the first joined. 95 | Object.defineProperty(Index.prototype, 'body', { 96 | enumerable: true, 97 | get: function() { 98 | return this.contents.length === 1 ? this.contents[0] : this.contents.slice(1).join('\n'); 99 | }, 100 | }); 101 | 102 | // Render content. 103 | Index.prototype.render = function(part) { 104 | return render.render(this, part); 105 | }; 106 | -------------------------------------------------------------------------------- /lib/render.js: -------------------------------------------------------------------------------- 1 | /* 2 | * a new benalman.com, maybe 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | 14 | var ghm = require('github-flavored-markdown'); 15 | var ejs = require('ejs'); 16 | 17 | // Create a hyphenated slug from an arbitrary title string. (TODO: transliteration?) 18 | var slugify = function(str) { 19 | return str.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase(); 20 | }; 21 | 22 | // Build the TOC from the page content markdown. 23 | var buildToc = function() { 24 | exports._toc = []; 25 | exports.raw = exports.raw.replace(/^(#+)\s+(.*)/gm, function(str, header, title) { 26 | exports._toc.push({title: title, depth: header.length}); 27 | return header + ' ' + title; 28 | }); 29 | }; 30 | 31 | exports.render = function(page, part) { 32 | exports.page = page; 33 | exports.meta = page.meta; 34 | exports.raw = page[part]; 35 | 36 | buildToc(); 37 | 38 | // Render page content as EJS template. 39 | var result = ejs.render(exports.raw, exports); 40 | // Parse result as GitHub-flavored-markdown. 41 | result = ghm.parse(result, exports.page.meta.github); 42 | 43 | return result; 44 | }; 45 | 46 | var define = Object.defineProperty.bind(null, exports); 47 | 48 | define('_toc', {writable: true, configurable: true}); 49 | 50 | // Render the TOC. 51 | define('toc', { 52 | enumerable: true, 53 | get: function() { 54 | var fn = function(options) { 55 | if (!options) { options = {}; } 56 | return exports._toc.map(function(item) { 57 | var md = ''; 58 | if (!options.flatten) { 59 | md += new Array(item.depth).join(' '); 60 | } 61 | md += '* [' + item.title + '](#' + slugify(item.title) + ')'; 62 | return md; 63 | }).join('\n'); 64 | }; 65 | // If the function is accessed as `toc` it will be coerced to a string 66 | // by calling `toc()`. 67 | fn.toString = fn; 68 | // But it can be also called as `toc({...})` with options! 69 | return fn; 70 | }, 71 | }); 72 | 73 | var includer = function(filepath, options) { 74 | if (!options) { options = {}; } 75 | if (!options.type) { 76 | options.type = path.extname(filepath).replace(/^\./, ''); 77 | } 78 | options.filepath = filepath; 79 | options.abspath = path.resolve(exports.page.basepath, filepath); 80 | return this(options); 81 | }; 82 | 83 | // Source code. 84 | exports.source = includer.bind(function(options) { 85 | var syntax = options.syntax || options.type; 86 | return '```' + syntax + '\n' + fs.readFileSync(options.abspath) + '\n```'; 87 | }); 88 | 89 | // Image. 90 | exports.image = includer.bind(function(options) { 91 | var alt = options.alt || ''; 92 | return '![' + alt + '](' + options.abspath + ')\n'; 93 | }); 94 | 95 | // Flickr photo. 96 | exports.flickr = function(url/*, context*/) { 97 | return '![](' + url + ')\n'; 98 | }; 99 | 100 | // Soundcloud player. 101 | exports.soundcloud = function(options) { 102 | return '[SOUNDCLOUD FIX ME](' + options.filepath + ')\n'; 103 | }; 104 | 105 | // Other file types. 106 | exports.file = includer.bind(function(options) { 107 | var method = exports.file[options.type]; 108 | if (method) { 109 | return method(options); 110 | } else { 111 | return 'Render error: File type "' + options.type + '" not found.'; 112 | } 113 | }); 114 | 115 | -------------------------------------------------------------------------------- /lib/page/render.js: -------------------------------------------------------------------------------- 1 | /* 2 | * a new benalman.com, maybe 3 | * 4 | * Copyright (c) 2012 "Cowboy" Ben Alman 5 | * Licensed under the MIT license. 6 | * http://benalman.com/about/license/ 7 | */ 8 | 9 | 'use strict'; 10 | 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | 14 | var ghm = require('github-flavored-markdown'); 15 | var ejs = require('ejs'); 16 | 17 | // Create a hyphenated slug from an arbitrary title string. (TODO: transliteration?) 18 | var slugify = function(str) { 19 | return str.replace(/\W+/g, '-').replace(/^-|-$/g, '').toLowerCase(); 20 | }; 21 | 22 | // Build the TOC from the page content markdown. 23 | var buildToc = function() { 24 | exports._toc = []; 25 | exports.raw = exports.raw.replace(/^(#+)\s+(.*)/gm, function(str, header, title) { 26 | exports._toc.push({title: title, depth: header.length}); 27 | return header + ' ' + title; 28 | }); 29 | }; 30 | 31 | exports.render = function(page, part) { 32 | exports.page = page; 33 | exports.meta = page.meta; 34 | exports.raw = page[part]; 35 | 36 | buildToc(); 37 | 38 | // Render page content as EJS template. 39 | var result = ejs.render(exports.raw, exports); 40 | // Parse result as GitHub-flavored-markdown. 41 | result = ghm.parse(result, exports.page.meta.github); 42 | 43 | return result; 44 | }; 45 | 46 | var define = Object.defineProperty.bind(null, exports); 47 | 48 | define('_toc', {writable: true, configurable: true}); 49 | 50 | // Render the TOC. 51 | define('toc', { 52 | enumerable: true, 53 | get: function() { 54 | var fn = function(options) { 55 | if (!options) { options = {}; } 56 | return exports._toc.map(function(item) { 57 | var md = ''; 58 | if (!options.flatten) { 59 | md += new Array(item.depth).join(' '); 60 | } 61 | md += '* [' + item.title + '](#' + slugify(item.title) + ')'; 62 | return md; 63 | }).join('\n'); 64 | }; 65 | // If the function is accessed as `toc` it will be coerced to a string 66 | // by calling `toc()`. 67 | fn.toString = fn; 68 | // But it can be also called as `toc({...})` with options! 69 | return fn; 70 | }, 71 | }); 72 | 73 | var includer = function(filepath, options) { 74 | if (!options) { options = {}; } 75 | if (!options.type) { 76 | options.type = path.extname(filepath).replace(/^\./, ''); 77 | } 78 | options.filepath = filepath; 79 | options.abspath = path.resolve(exports.page.basepath, filepath); 80 | return this(options); 81 | }; 82 | 83 | // Source code. 84 | exports.source = includer.bind(function(options) { 85 | var syntax = options.syntax || options.type; 86 | return '```' + syntax + '\n' + fs.readFileSync(options.abspath) + '\n```'; 87 | }); 88 | 89 | // Image. 90 | exports.image = includer.bind(function(options) { 91 | var alt = options.alt || ''; 92 | return '![' + alt + '](' + options.abspath + ')\n'; 93 | }); 94 | 95 | // Flickr photo. 96 | exports.flickr = function(url/*, context*/) { 97 | return '![](' + url + ')\n'; 98 | }; 99 | 100 | // Soundcloud player. 101 | exports.soundcloud = function(options) { 102 | return '[SOUNDCLOUD FIX ME](' + options.filepath + ')\n'; 103 | }; 104 | 105 | // Other file types. 106 | exports.file = includer.bind(function(options) { 107 | var method = exports.file[options.type]; 108 | if (method) { 109 | return method(options); 110 | } else { 111 | return 'Render error: File type "' + options.type + '" not found.'; 112 | } 113 | }); 114 | 115 | --------------------------------------------------------------------------------