├── .travis.yml ├── .npmignore ├── .gitignore ├── package.json ├── LICENSE ├── index.js ├── README.md └── test ├── main.js └── fixtures ├── journo.coffee.md ├── journo.litcoffee └── grammar.coffee /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | - 7 6 | - 8 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | *.sublime* 19 | package-lock.json 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | *.sublime* 19 | package-lock.json 20 | yarn.lock 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-coffee", 3 | "description": "Compile CoffeeScript files", 4 | "version": "3.0.3", 5 | "homepage": "http://github.com/gulp-community/gulp-coffee", 6 | "repository": "git://github.com/gulp-community/gulp-coffee.git", 7 | "author": "Contra (http://contra.io/)", 8 | "main": "./index.js", 9 | "keywords": [ 10 | "gulpplugin" 11 | ], 12 | "dependencies": { 13 | "coffeescript": "^2.1.0", 14 | "plugin-error": "^1.0.0", 15 | "replace-ext": "^1.0.0", 16 | "through2": "^2.0.1", 17 | "vinyl-sourcemaps-apply": "^0.2.1" 18 | }, 19 | "devDependencies": { 20 | "gulp-sourcemaps": "^2.6.2", 21 | "mocha": "^5.0.0", 22 | "should": "^13.0.0", 23 | "vinyl": "^2.1.0" 24 | }, 25 | "scripts": { 26 | "test": "mocha" 27 | }, 28 | "engines": { 29 | "node": ">= 6.0.0" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Contra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | var applySourceMap = require('vinyl-sourcemaps-apply'); 3 | var replaceExt = require('replace-ext'); 4 | var PluginError = require('plugin-error'); 5 | 6 | module.exports = function (opt) { 7 | function replaceExtension(path) { 8 | path = path.replace(/\.coffee\.md$/, '.litcoffee'); 9 | return replaceExt(path, '.js'); 10 | } 11 | 12 | function transform(file, enc, cb) { 13 | if (file.isNull()) return cb(null, file); 14 | if (file.isStream()) return cb(new PluginError('gulp-coffee', 'Streaming not supported')); 15 | 16 | var data; 17 | var str = file.contents.toString('utf8'); 18 | var dest = replaceExtension(file.path); 19 | 20 | var options = Object.assign({ 21 | bare: false, 22 | coffee: require('coffeescript'), 23 | header: false, 24 | sourceMap: Boolean(file.sourceMap), 25 | sourceRoot: false, 26 | literate: /\.(litcoffee|coffee\.md)$/.test(file.path), 27 | filename: file.path, 28 | sourceFiles: [file.relative], 29 | generatedFile: replaceExtension(file.relative) 30 | }, opt); 31 | 32 | try { 33 | data = options.coffee.compile(str, options); 34 | } catch (err) { 35 | return cb(new PluginError('gulp-coffee', err)); 36 | } 37 | 38 | if (data && data.v3SourceMap && file.sourceMap) { 39 | applySourceMap(file, data.v3SourceMap); 40 | file.contents = Buffer.from(data.js); 41 | } else { 42 | file.contents = Buffer.from(data); 43 | } 44 | 45 | file.path = dest; 46 | cb(null, file); 47 | } 48 | 49 | return through.obj(transform); 50 | }; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/gulp-community/gulp-coffee.png?branch=master)](https://travis-ci.org/gulp-community/gulp-coffee) 2 | 3 | ## Information 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Packagegulp-coffee
DescriptionCompiles CoffeeScript
Node Version>= 6
18 | 19 | ## Usage 20 | 21 | ```javascript 22 | var coffee = require('gulp-coffee'); 23 | 24 | gulp.task('coffee', function() { 25 | gulp.src('./src/*.coffee') 26 | .pipe(coffee({bare: true})) 27 | .pipe(gulp.dest('./public/')); 28 | }); 29 | ``` 30 | 31 | ## Options 32 | 33 | - `coffee` (optional): A reference to a custom CoffeeScript version/fork (eg. `coffee: require('my-name/coffeescript')`) 34 | 35 | Additionally, the options object supports all options that are supported by the standard CoffeeScript compiler. 36 | 37 | ## Source maps 38 | 39 | ### gulp 3.x 40 | 41 | gulp-coffee can be used in tandem with [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps) to generate source maps for the coffee to javascript transition. You will need to initialize [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps) prior to running the gulp-coffee compiler and write the source maps after. 42 | 43 | ```javascript 44 | var sourcemaps = require('gulp-sourcemaps'); 45 | 46 | gulp.src('./src/*.coffee') 47 | .pipe(sourcemaps.init()) 48 | .pipe(coffee()) 49 | .pipe(sourcemaps.write()) 50 | .pipe(gulp.dest('./dest/js')); 51 | 52 | // will write the source maps inline in the compiled javascript files 53 | ``` 54 | 55 | By default, [gulp-sourcemaps](https://github.com/floridoo/gulp-sourcemaps) writes the source maps inline in the compiled javascript files. To write them to a separate file, specify a relative file path in the `sourcemaps.write()` function. 56 | 57 | ```javascript 58 | var sourcemaps = require('gulp-sourcemaps'); 59 | 60 | gulp.src('./src/*.coffee') 61 | .pipe(sourcemaps.init()) 62 | .pipe(coffee({ bare: true })) 63 | .pipe(sourcemaps.write('./maps')) 64 | .pipe(gulp.dest('./dest/js')); 65 | 66 | // will write the source maps to ./dest/js/maps 67 | ``` 68 | 69 | ### gulp 4.x 70 | 71 | In gulp 4, sourcemaps are built-in by default. 72 | 73 | ```js 74 | gulp.src('./src/*.coffee', { sourcemaps: true }) 75 | .pipe(coffee({ bare: true })) 76 | .pipe(gulp.dest('./dest/js')); 77 | ``` 78 | 79 | ## LICENSE 80 | 81 | (MIT License) 82 | 83 | Copyright (c) 2015 Fractal 84 | 85 | Permission is hereby granted, free of charge, to any person obtaining 86 | a copy of this software and associated documentation files (the 87 | "Software"), to deal in the Software without restriction, including 88 | without limitation the rights to use, copy, modify, merge, publish, 89 | distribute, sublicense, and/or sell copies of the Software, and to 90 | permit persons to whom the Software is furnished to do so, subject to 91 | the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 98 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 99 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 100 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 101 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 102 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | var coffee = require('../'); 2 | var should = require('should'); 3 | var coffeescript = require('coffeescript'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var sourcemaps = require('gulp-sourcemaps'); 7 | var File = require('vinyl'); 8 | 9 | var createFile = function (filepath, contents) { 10 | var base = path.dirname(filepath); 11 | return new File({ 12 | path: filepath, 13 | base: base, 14 | cwd: path.dirname(base), 15 | contents: contents 16 | }); 17 | }; 18 | 19 | describe('gulp-coffee', function() { 20 | describe('coffee()', function() { 21 | before(function() { 22 | this.testData = function (expected, newPath, done) { 23 | var newPaths = [newPath], 24 | expectedSourceMap; 25 | 26 | if (expected.v3SourceMap) { 27 | expectedSourceMap = JSON.parse(expected.v3SourceMap); 28 | expected = [expected.js]; 29 | } else { 30 | expected = [expected]; 31 | } 32 | 33 | return function (newFile) { 34 | this.expected = expected.shift(); 35 | this.newPath = newPaths.shift(); 36 | 37 | should.exist(newFile); 38 | should.exist(newFile.path); 39 | should.exist(newFile.relative); 40 | should.exist(newFile.contents); 41 | newFile.path.should.equal(this.newPath); 42 | newFile.relative.should.equal(path.basename(this.newPath)); 43 | String(newFile.contents).should.equal(this.expected); 44 | 45 | if (expectedSourceMap) { 46 | // check whether the sources from the coffee have been 47 | // applied to the files source map 48 | newFile.sourceMap.sources 49 | .should.containDeep(expectedSourceMap.sources); 50 | } 51 | 52 | if (done && !expected.length) { 53 | done.call(this); 54 | } 55 | }; 56 | }; 57 | }); 58 | 59 | it('should concat two files', function(done) { 60 | var filepath = '/home/contra/test/file.coffee'; 61 | var contents = Buffer.from('a = 2'); 62 | var opts = {bare: true}; 63 | var expected = coffeescript.compile(String(contents), opts); 64 | 65 | coffee(opts) 66 | .on('error', done) 67 | .on('data', this.testData(expected, path.normalize('/home/contra/test/file.js'), done)) 68 | .write(createFile(filepath, contents)); 69 | }); 70 | 71 | it('should emit errors correctly', function(done) { 72 | var filepath = '/home/contra/test/file.coffee'; 73 | var contents = Buffer.from('if a()\r\n then huh'); 74 | 75 | coffee({bare: true}) 76 | .on('error', function(err) { 77 | err.message.should.equal('unexpected then'); 78 | done(); 79 | }) 80 | .on('data', function() { 81 | throw new Error('no file should have been emitted!'); 82 | }) 83 | .write(createFile(filepath, contents)); 84 | }); 85 | 86 | it('should compile a file (no bare)', function(done) { 87 | var filepath = 'test/fixtures/grammar.coffee'; 88 | var contents = Buffer.from(fs.readFileSync(filepath)); 89 | var expected = coffeescript.compile(String(contents)); 90 | 91 | coffee() 92 | .on('error', done) 93 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)) 94 | .write(createFile(filepath, contents)); 95 | }); 96 | 97 | it('should compile a file (with bare)', function(done) { 98 | var filepath = 'test/fixtures/grammar.coffee'; 99 | var contents = Buffer.from(fs.readFileSync(filepath)); 100 | var opts = {bare: true}; 101 | var expected = coffeescript.compile(String(contents), opts); 102 | 103 | coffee(opts) 104 | .on('error', done) 105 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)) 106 | .write(createFile(filepath, contents)); 107 | }); 108 | 109 | it('should compile a file with source map', function(done) { 110 | var filepath = 'test/fixtures/grammar.coffee'; 111 | var contents = Buffer.from(fs.readFileSync(filepath)); 112 | var expected = coffeescript.compile(String(contents), { 113 | sourceMap: true, 114 | sourceFiles: ['grammar.coffee'], 115 | generatedFile: 'grammar.js' 116 | }); 117 | 118 | 119 | var stream = sourcemaps.init(); 120 | stream.write(createFile(filepath, contents)); 121 | stream 122 | .pipe(coffee({})) 123 | .on('error', done) 124 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)); 125 | }); 126 | 127 | it('should compile a file with bare and with source map', function(done) { 128 | var filepath = 'test/fixtures/grammar.coffee'; 129 | var contents = Buffer.from(fs.readFileSync(filepath)); 130 | var expected = coffeescript.compile(String(contents), { 131 | bare: true, 132 | sourceMap: true, 133 | sourceFiles: ['grammar.coffee'], 134 | generatedFile: 'grammar.js' 135 | }); 136 | 137 | var stream = sourcemaps.init(); 138 | stream.write(createFile(filepath, contents)); 139 | stream 140 | .pipe(coffee({bare: true})) 141 | .on('error', done) 142 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)); 143 | }); 144 | 145 | it('should compile a file (no header)', function(done) { 146 | var filepath = 'test/fixtures/grammar.coffee'; 147 | var contents = Buffer.from(fs.readFileSync(filepath)); 148 | var expected = coffeescript.compile(String(contents), {header: false}); 149 | 150 | coffee() 151 | .on('error', done) 152 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)) 153 | .write(createFile(filepath, contents)); 154 | }); 155 | 156 | it('should compile a file (with header)', function(done) { 157 | var filepath = 'test/fixtures/grammar.coffee'; 158 | var contents = Buffer.from(fs.readFileSync(filepath)); 159 | var expected = coffeescript.compile(String(contents), {header: true}); 160 | 161 | coffee({header: true}) 162 | .on('error', done) 163 | .on('data', this.testData(expected, path.normalize('test/fixtures/grammar.js'), done)) 164 | .write(createFile(filepath, contents)); 165 | }); 166 | 167 | it('should compile a literate file', function(done) { 168 | var filepath = 'test/fixtures/journo.litcoffee'; 169 | var contents = Buffer.from(fs.readFileSync(filepath)); 170 | var opts = {literate: true}; 171 | var expected = coffeescript.compile(String(contents), opts); 172 | 173 | coffee(opts) 174 | .on('error', done) 175 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)) 176 | .write(createFile(filepath, contents)); 177 | }); 178 | 179 | it('should compile a literate file (implicit)', function(done) { 180 | var filepath = 'test/fixtures/journo.litcoffee'; 181 | var contents = Buffer.from(fs.readFileSync(filepath)); 182 | var expected = coffeescript.compile(String(contents), {literate: true}); 183 | 184 | coffee() 185 | .on('error', done) 186 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)) 187 | .write(createFile(filepath, contents)); 188 | }); 189 | 190 | it('should compile a literate file (with bare)', function(done) { 191 | var filepath = 'test/fixtures/journo.litcoffee'; 192 | var contents = Buffer.from(fs.readFileSync(filepath)); 193 | var opts = {literate: true, bare: true}; 194 | var expected = coffeescript.compile(String(contents), opts); 195 | 196 | coffee(opts) 197 | .on('error', done) 198 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)) 199 | .write(createFile(filepath, contents)); 200 | }); 201 | 202 | it('should compile a literate file with source map', function(done) { 203 | var filepath = 'test/fixtures/journo.litcoffee'; 204 | var contents = Buffer.from(fs.readFileSync(filepath)); 205 | var expected = coffeescript.compile(String(contents), { 206 | literate: true, 207 | sourceMap: true, 208 | sourceFiles: ['journo.litcoffee'], 209 | generatedFile: 'journo.js' 210 | }); 211 | 212 | var stream = sourcemaps.init(); 213 | stream.write(createFile(filepath, contents)); 214 | stream 215 | .pipe(coffee({literate: true})) 216 | .on('error', done) 217 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)); 218 | }); 219 | 220 | it('should compile a literate file with bare and with source map', function(done) { 221 | var filepath = 'test/fixtures/journo.litcoffee'; 222 | var contents = Buffer.from(fs.readFileSync(filepath)); 223 | var expected = coffeescript.compile(String(contents), { 224 | literate: true, 225 | bare: true, 226 | sourceMap: true, 227 | sourceFiles: ['journo.litcoffee'], 228 | generatedFile: 'journo.js' 229 | }); 230 | 231 | var stream = sourcemaps.init(); 232 | stream.write(createFile(filepath, contents)); 233 | stream 234 | .pipe(coffee({literate: true, bare: true})) 235 | .on('error', done) 236 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)); 237 | }); 238 | 239 | it('should rename a literate markdown file', function(done) { 240 | var filepath = 'test/fixtures/journo.coffee.md'; 241 | var contents = Buffer.from(fs.readFileSync(filepath)); 242 | var opts = {literate: true}; 243 | var expected = coffeescript.compile(String(contents), opts); 244 | 245 | coffee(opts) 246 | .on('error', done) 247 | .on('data', this.testData(expected, path.normalize('test/fixtures/journo.js'), done)) 248 | .write(createFile(filepath, contents)); 249 | }); 250 | 251 | it('should accept a custom coffeescript version', function(done) { 252 | var filepath = 'test/fixtures/grammar.coffee'; 253 | var contents = Buffer.from(fs.readFileSync(filepath)); 254 | var wasSpyCalled = false; 255 | var opts = { 256 | coffee: { 257 | compile() { 258 | wasSpyCalled = true; 259 | return ''; 260 | } 261 | } 262 | }; 263 | 264 | function assertSpy() { 265 | should(wasSpyCalled).equal(true); 266 | done(); 267 | } 268 | 269 | coffee(opts) 270 | .on('error', assertSpy) 271 | .on('data', this.testData('', path.normalize('test/fixtures/grammar.js'), assertSpy)) 272 | .write(createFile(filepath, contents)); 273 | 274 | }) 275 | }); 276 | }); 277 | -------------------------------------------------------------------------------- /test/fixtures/journo.coffee.md: -------------------------------------------------------------------------------- 1 | Journo 2 | ====== 3 | 4 | Journo = module.exports = {} 5 | 6 | Journo is a blogging program, with a few basic goals. To wit: 7 | 8 | * Write in Markdown. 9 | 10 | * Publish to flat files. 11 | 12 | * Publish via Rsync. 13 | 14 | * Maintain a manifest file (what's published and what isn't, pub dates). 15 | 16 | * Retina ready. 17 | 18 | * Syntax highlight code. 19 | 20 | * Publish a feed. 21 | 22 | * Quickly bootstrap a new blog. 23 | 24 | * Preview via a local server. 25 | 26 | * Work without JavaScript, but default to a fluid JavaScript-enabled UI. 27 | 28 | You can install and use the `journo` command via npm: `sudo npm install -g journo` 29 | 30 | ... now, let's go through those features one at a time: 31 | 32 | 33 | Getting Started 34 | --------------- 35 | 36 | 1. Create a folder for your blog, and `cd` into it. 37 | 38 | 2. Type `journo init` to bootstrap a new empty blog. 39 | 40 | 3. Edit the `config.json`, `layout.html`, and `posts/index.md` files to suit. 41 | 42 | 4. Type `journo` to start the preview server, and have at it. 43 | 44 | 45 | Write in Markdown 46 | ----------------- 47 | 48 | We'll use the excellent **marked** module to compile Markdown into HTML, and 49 | Underscore for many of its goodies later on. Up top, create a namespace for 50 | shared values needed by more than one function. 51 | 52 | marked = require 'marked' 53 | _ = require 'underscore' 54 | shared = {} 55 | 56 | To render a post, we take its raw `source`, treat it as both an Underscore 57 | template (for HTML generation) and as Markdown (for formatting), and insert it 58 | into the layout as `content`. 59 | 60 | Journo.render = (post, source) -> 61 | catchErrors -> 62 | do loadLayout 63 | source or= fs.readFileSync postPath post 64 | variables = renderVariables post 65 | markdown = _.template(source.toString()) variables 66 | title = detectTitle markdown 67 | content = marked.parser marked.lexer markdown 68 | shared.layout _.extend variables, {title, content} 69 | 70 | A Journo site has a layout file, stored in `layout.html`, which is used 71 | to wrap every page. 72 | 73 | loadLayout = (force) -> 74 | return layout if not force and layout = shared.layout 75 | shared.layout = _.template(fs.readFileSync('layout.html').toString()) 76 | 77 | Determine the appropriate command to "open" a url in the browser for the 78 | current platform. 79 | 80 | opener = switch process.platform 81 | when 'darwin' then 'open' 82 | when 'win32' then 'start' 83 | else 'xdg-open' 84 | 85 | 86 | Publish to Flat Files 87 | --------------------- 88 | 89 | A blog is a folder on your hard drive. Within the blog, you have a `posts` 90 | folder for blog posts, a `public` folder for static content, a `layout.html` 91 | file for the layout which wraps every page, and a `journo.json` file for 92 | configuration. During a `build`, a static version of the site is rendered 93 | into the `site` folder, by **rsync**ing over all static files, rendering and 94 | writing every post, and creating an RSS feed. 95 | 96 | fs = require 'fs' 97 | path = require 'path' 98 | {spawn, exec} = require 'child_process' 99 | 100 | Journo.build = -> 101 | do loadManifest 102 | fs.mkdirSync('site') unless fs.existsSync('site') 103 | 104 | exec "rsync -vur --delete public/ site", (err, stdout, stderr) -> 105 | throw err if err 106 | 107 | for post in folderContents('posts') 108 | html = Journo.render post 109 | file = htmlPath post 110 | fs.mkdirSync path.dirname(file) unless fs.existsSync path.dirname(file) 111 | fs.writeFileSync file, html 112 | 113 | fs.writeFileSync "site/feed.rss", Journo.feed() 114 | 115 | The `config.json` configuration file is where you keep the configuration 116 | details of your blog, and how to connect to the server you'd like to publish 117 | it on. The valid settings are: `title`, `description`, `author` (for RSS), `url 118 | `, `publish` (the `user@host:path` location to **rsync** to), and `publishPort` 119 | (if your server doesn't listen to SSH on the usual one). 120 | 121 | An example `config.json` will be bootstrapped for you when you initialize a blog, 122 | so you don't need to remember any of that. 123 | 124 | loadConfig = -> 125 | return if shared.config 126 | try 127 | shared.config = JSON.parse fs.readFileSync 'config.json' 128 | catch err 129 | fatal "Unable to read config.json" 130 | shared.siteUrl = shared.config.url.replace(/\/$/, '') 131 | 132 | 133 | Publish via rsync 134 | ----------------- 135 | 136 | Publishing is nice and rudimentary. We build out an entirely static version of 137 | the site and **rysnc** it up to the server. 138 | 139 | Journo.publish = -> 140 | do Journo.build 141 | rsync 'site/images/', path.join(shared.config.publish, 'images/'), -> 142 | rsync 'site/', shared.config.publish 143 | 144 | A helper function for **rsync**ing, with logging, and the ability to wait for 145 | the rsync to continue before proceeding. This is useful for ensuring that our 146 | any new photos have finished uploading (very slowly) before the update to the feed 147 | is syndicated out. 148 | 149 | rsync = (from, to, callback) -> 150 | port = "ssh -p #{shared.config.publishPort or 22}" 151 | child = spawn "rsync", ['-vurz', '--delete', '-e', port, from, to] 152 | child.stdout.on 'data', (out) -> console.log out.toString() 153 | child.stderr.on 'data', (err) -> console.error err.toString() 154 | child.on 'exit', callback if callback 155 | 156 | 157 | Maintain a Manifest File 158 | ------------------------ 159 | 160 | The "manifest" is where Journo keeps track of metadata -- the title, description, 161 | publications date and last modified time of each post. Everything you need to 162 | render out an RSS feed ... and everything you need to know if a post has been 163 | updated or removed. 164 | 165 | manifestPath = 'journo-manifest.json' 166 | 167 | loadManifest = -> 168 | do loadConfig 169 | 170 | shared.manifest = if fs.existsSync manifestPath 171 | JSON.parse fs.readFileSync manifestPath 172 | else 173 | {} 174 | 175 | do updateManifest 176 | fs.writeFileSync manifestPath, JSON.stringify shared.manifest 177 | 178 | We update the manifest by looping through every post and every entry in the 179 | existing manifest, looking for differences in `mtime`, and recording those 180 | along with the title and description of each post. 181 | 182 | updateManifest = -> 183 | manifest = shared.manifest 184 | posts = folderContents 'posts' 185 | 186 | delete manifest[post] for post of manifest when post not in posts 187 | 188 | for post in posts 189 | stat = fs.statSync postPath post 190 | entry = manifest[post] 191 | if not entry or entry.mtime isnt stat.mtime 192 | entry or= {pubtime: stat.ctime} 193 | entry.mtime = stat.mtime 194 | content = fs.readFileSync(postPath post).toString() 195 | entry.title = detectTitle content 196 | entry.description = detectDescription content, post 197 | manifest[post] = entry 198 | 199 | yes 200 | 201 | 202 | Retina Ready 203 | ------------ 204 | 205 | In the future, it may make sense for Journo to have some sort of built-in 206 | facility for automatically downsizing photos from retina to regular sizes ... 207 | But for now, this bit is up to you. 208 | 209 | 210 | Syntax Highlight Code 211 | --------------------- 212 | 213 | We syntax-highlight blocks of code with the nifty **highlight** package that 214 | includes heuristics for auto-language detection, so you don't have to specify 215 | what you're coding in. 216 | 217 | highlight = require 'highlight.js' 218 | 219 | marked.setOptions 220 | highlight: (code, lang) -> 221 | if highlight.LANGUAGES[lang]? 222 | highlight.highlight(lang, code, true).value 223 | else 224 | highlight.highlightAuto(code).value 225 | 226 | Publish a Feed 227 | -------------- 228 | 229 | We'll use the **rss** module to build a simple feed of recent posts. Start with 230 | the basic `author`, blog `title`, `description` and `url` configured in the 231 | `config.json`. Then, each post's `title` is the first header present in the 232 | post, the `description` is the first paragraph, and the date is the date you 233 | first created the post file. 234 | 235 | Journo.feed = -> 236 | RSS = require 'rss' 237 | do loadConfig 238 | config = shared.config 239 | 240 | feed = new RSS 241 | title: config.title 242 | description: config.description 243 | feed_url: "#{shared.siteUrl}/rss.xml" 244 | site_url: shared.siteUrl 245 | author: config.author 246 | 247 | for post in sortedPosts().reverse()[0...20] 248 | entry = shared.manifest[post] 249 | feed.item 250 | title: entry.title 251 | description: entry.description 252 | url: postUrl post 253 | date: entry.pubtime 254 | 255 | feed.xml() 256 | 257 | 258 | Quickly Bootstrap a New Blog 259 | ---------------------------- 260 | 261 | We **init** a new blog into the current directory by copying over the contents 262 | of a basic `bootstrap` folder. 263 | 264 | Journo.init = -> 265 | here = fs.realpathSync '.' 266 | if fs.existsSync 'posts' 267 | fatal "A blog already exists in #{here}" 268 | bootstrap = path.join(__dirname, 'bootstrap/*') 269 | exec "rsync -vur --delete #{bootstrap} .", (err, stdout, stderr) -> 270 | throw err if err 271 | console.log "Initialized new blog in #{here}" 272 | 273 | 274 | Preview via a Local Server 275 | -------------------------- 276 | 277 | Instead of constantly rebuilding a purely static version of the site, Journo 278 | provides a preview server (which you can start by just typing `journo` from 279 | within your blog). 280 | 281 | Journo.preview = -> 282 | http = require 'http' 283 | mime = require 'mime' 284 | url = require 'url' 285 | util = require 'util' 286 | do loadManifest 287 | 288 | server = http.createServer (req, res) -> 289 | rawPath = url.parse(req.url).pathname.replace(/(^\/|\/$)/g, '') or 'index' 290 | 291 | If the request is for a preview of the RSS feed... 292 | 293 | if rawPath is 'feed.rss' 294 | res.writeHead 200, 'Content-Type': mime.lookup('.rss') 295 | res.end Journo.feed() 296 | 297 | If the request is for a static file that exists in our `public` directory... 298 | 299 | else 300 | publicPath = "public/" + rawPath 301 | fs.exists publicPath, (exists) -> 302 | if exists 303 | res.writeHead 200, 'Content-Type': mime.lookup(publicPath) 304 | fs.createReadStream(publicPath).pipe res 305 | 306 | If the request is for the slug of a valid post, we reload the layout, and 307 | render it... 308 | 309 | else 310 | post = "posts/#{rawPath}.md" 311 | fs.exists post, (exists) -> 312 | if exists 313 | loadLayout true 314 | fs.readFile post, (err, content) -> 315 | res.writeHead 200, 'Content-Type': 'text/html' 316 | res.end Journo.render post, content 317 | 318 | Anything else is a 404. 319 | 320 | else 321 | res.writeHead 404 322 | res.end '404 Not Found' 323 | 324 | server.listen 1234 325 | console.log "Journo is previewing at http://localhost:1234" 326 | exec "#{opener} http://localhost:1234" 327 | 328 | 329 | Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI 330 | --------------------------------------------------------------------- 331 | 332 | The best way to handle this bit seems to be entirely on the client-side. For 333 | example, when rendering a JavaScript slideshow of photographs, instead of 334 | having the server spit out the slideshow code, simply have the blog detect 335 | the list of images during page load and move them into a slideshow right then 336 | and there -- using `alt` attributes for captions, for example. 337 | 338 | Since the blog is public, it's nice if search engines can see all of the pieces 339 | as well as readers. 340 | 341 | 342 | Finally, Putting it all Together. Run Journo From the Terminal 343 | -------------------------------------------------------------- 344 | 345 | We'll do the simplest possible command-line interface. If a public function 346 | exists on the `Journo` object, you can run it. *Note that this lets you do 347 | silly things, like* `journo toString` *but no big deal.* 348 | 349 | Journo.run = -> 350 | command = process.argv[2] or 'preview' 351 | return do Journo[command] if Journo[command] 352 | console.error "Journo doesn't know how to '#{command}'" 353 | 354 | Let's also provide a help page that lists the available commands. 355 | 356 | Journo.help = Journo['--help'] = -> 357 | console.log """ 358 | Usage: journo [command] 359 | 360 | If called without a command, `journo` will preview your blog. 361 | 362 | init start a new blog in the current folder 363 | build build a static version of the blog into 'site' 364 | preview live preview the blog via a local server 365 | publish publish the blog to your remote server 366 | """ 367 | 368 | And we might as well do the version number, for completeness' sake. 369 | 370 | Journo.version = Journo['--version'] = -> 371 | console.log "Journo 0.0.1" 372 | 373 | 374 | Miscellaneous Bits and Utilities 375 | -------------------------------- 376 | 377 | Little utility functions that are useful up above. 378 | 379 | The file path to the source of a given `post`. 380 | 381 | postPath = (post) -> "posts/#{post}" 382 | 383 | The server-side path to the HTML for a given `post`. 384 | 385 | htmlPath = (post) -> 386 | name = postName post 387 | if name is 'index' 388 | 'site/index.html' 389 | else 390 | "site/#{name}/index.html" 391 | 392 | The name (or slug) of a post, taken from the filename. 393 | 394 | postName = (post) -> path.basename post, '.md' 395 | 396 | The full, absolute URL for a published post. 397 | 398 | postUrl = (post) -> "#{shared.siteUrl}/#{postName(post)}/" 399 | 400 | Starting with the string contents of a post, detect the title -- 401 | the first heading. 402 | 403 | detectTitle = (content) -> 404 | _.find(marked.lexer(content), (token) -> token.type is 'heading')?.text 405 | 406 | Starting with the string contents of a post, detect the description -- 407 | the first paragraph. 408 | 409 | detectDescription = (content, post) -> 410 | desc = _.find(marked.lexer(content), (token) -> token.type is 'paragraph')?.text 411 | marked.parser marked.lexer _.template("#{desc}...")(renderVariables(post)) 412 | 413 | Helper function to read in the contents of a folder, ignoring hidden files 414 | and directories. 415 | 416 | folderContents = (folder) -> 417 | fs.readdirSync(folder).filter (f) -> f.charAt(0) isnt '.' 418 | 419 | Return the list of posts currently in the manifest, sorted by their date of 420 | publication. 421 | 422 | sortedPosts = -> 423 | _.sortBy _.without(_.keys(shared.manifest), 'index.md'), (post) -> 424 | shared.manifest[post].pubtime 425 | 426 | The shared variables we want to allow our templates (both posts, and layout) 427 | to use in their evaluations. In the future, it would be nice to determine 428 | exactly what best belongs here, and provide an easier way for the blog author 429 | to add functions to it. 430 | 431 | renderVariables = (post) -> 432 | { 433 | _ 434 | fs 435 | path 436 | mapLink 437 | postName 438 | folderContents 439 | posts: sortedPosts() 440 | post: path.basename(post) 441 | manifest: shared.manifest 442 | } 443 | 444 | Quick function which creates a link to a Google Map search for the name of the 445 | place. 446 | 447 | mapLink = (place, additional = '', zoom = 15) -> 448 | query = encodeURIComponent("#{place}, #{additional}") 449 | "#{place}" 450 | 451 | Convenience function for catching errors (keeping the preview server from 452 | crashing while testing code), and printing them out. 453 | 454 | catchErrors = (func) -> 455 | try do func 456 | catch err 457 | console.error err.stack 458 | "
#{err.stack}
" 459 | 460 | Finally, for errors that you want the app to die on -- things that should break 461 | the site build. 462 | 463 | fatal = (message) -> 464 | console.error message 465 | process.exit 1 466 | 467 | 468 | 469 | -------------------------------------------------------------------------------- /test/fixtures/journo.litcoffee: -------------------------------------------------------------------------------- 1 | Journo 2 | ====== 3 | 4 | Journo = module.exports = {} 5 | 6 | Journo is a blogging program, with a few basic goals. To wit: 7 | 8 | * Write in Markdown. 9 | 10 | * Publish to flat files. 11 | 12 | * Publish via Rsync. 13 | 14 | * Maintain a manifest file (what's published and what isn't, pub dates). 15 | 16 | * Retina ready. 17 | 18 | * Syntax highlight code. 19 | 20 | * Publish a feed. 21 | 22 | * Quickly bootstrap a new blog. 23 | 24 | * Preview via a local server. 25 | 26 | * Work without JavaScript, but default to a fluid JavaScript-enabled UI. 27 | 28 | You can install and use the `journo` command via npm: `sudo npm install -g journo` 29 | 30 | ... now, let's go through those features one at a time: 31 | 32 | 33 | Getting Started 34 | --------------- 35 | 36 | 1. Create a folder for your blog, and `cd` into it. 37 | 38 | 2. Type `journo init` to bootstrap a new empty blog. 39 | 40 | 3. Edit the `config.json`, `layout.html`, and `posts/index.md` files to suit. 41 | 42 | 4. Type `journo` to start the preview server, and have at it. 43 | 44 | 45 | Write in Markdown 46 | ----------------- 47 | 48 | We'll use the excellent **marked** module to compile Markdown into HTML, and 49 | Underscore for many of its goodies later on. Up top, create a namespace for 50 | shared values needed by more than one function. 51 | 52 | marked = require 'marked' 53 | _ = require 'underscore' 54 | shared = {} 55 | 56 | To render a post, we take its raw `source`, treat it as both an Underscore 57 | template (for HTML generation) and as Markdown (for formatting), and insert it 58 | into the layout as `content`. 59 | 60 | Journo.render = (post, source) -> 61 | catchErrors -> 62 | do loadLayout 63 | source or= fs.readFileSync postPath post 64 | variables = renderVariables post 65 | markdown = _.template(source.toString()) variables 66 | title = detectTitle markdown 67 | content = marked.parser marked.lexer markdown 68 | shared.layout _.extend variables, {title, content} 69 | 70 | A Journo site has a layout file, stored in `layout.html`, which is used 71 | to wrap every page. 72 | 73 | loadLayout = (force) -> 74 | return layout if not force and layout = shared.layout 75 | shared.layout = _.template(fs.readFileSync('layout.html').toString()) 76 | 77 | Determine the appropriate command to "open" a url in the browser for the 78 | current platform. 79 | 80 | opener = switch process.platform 81 | when 'darwin' then 'open' 82 | when 'win32' then 'start' 83 | else 'xdg-open' 84 | 85 | 86 | Publish to Flat Files 87 | --------------------- 88 | 89 | A blog is a folder on your hard drive. Within the blog, you have a `posts` 90 | folder for blog posts, a `public` folder for static content, a `layout.html` 91 | file for the layout which wraps every page, and a `journo.json` file for 92 | configuration. During a `build`, a static version of the site is rendered 93 | into the `site` folder, by **rsync**ing over all static files, rendering and 94 | writing every post, and creating an RSS feed. 95 | 96 | fs = require 'fs' 97 | path = require 'path' 98 | {spawn, exec} = require 'child_process' 99 | 100 | Journo.build = -> 101 | do loadManifest 102 | fs.mkdirSync('site') unless fs.existsSync('site') 103 | 104 | exec "rsync -vur --delete public/ site", (err, stdout, stderr) -> 105 | throw err if err 106 | 107 | for post in folderContents('posts') 108 | html = Journo.render post 109 | file = htmlPath post 110 | fs.mkdirSync path.dirname(file) unless fs.existsSync path.dirname(file) 111 | fs.writeFileSync file, html 112 | 113 | fs.writeFileSync "site/feed.rss", Journo.feed() 114 | 115 | The `config.json` configuration file is where you keep the configuration 116 | details of your blog, and how to connect to the server you'd like to publish 117 | it on. The valid settings are: `title`, `description`, `author` (for RSS), `url 118 | `, `publish` (the `user@host:path` location to **rsync** to), and `publishPort` 119 | (if your server doesn't listen to SSH on the usual one). 120 | 121 | An example `config.json` will be bootstrapped for you when you initialize a blog, 122 | so you don't need to remember any of that. 123 | 124 | loadConfig = -> 125 | return if shared.config 126 | try 127 | shared.config = JSON.parse fs.readFileSync 'config.json' 128 | catch err 129 | fatal "Unable to read config.json" 130 | shared.siteUrl = shared.config.url.replace(/\/$/, '') 131 | 132 | 133 | Publish via rsync 134 | ----------------- 135 | 136 | Publishing is nice and rudimentary. We build out an entirely static version of 137 | the site and **rysnc** it up to the server. 138 | 139 | Journo.publish = -> 140 | do Journo.build 141 | rsync 'site/images/', path.join(shared.config.publish, 'images/'), -> 142 | rsync 'site/', shared.config.publish 143 | 144 | A helper function for **rsync**ing, with logging, and the ability to wait for 145 | the rsync to continue before proceeding. This is useful for ensuring that our 146 | any new photos have finished uploading (very slowly) before the update to the feed 147 | is syndicated out. 148 | 149 | rsync = (from, to, callback) -> 150 | port = "ssh -p #{shared.config.publishPort or 22}" 151 | child = spawn "rsync", ['-vurz', '--delete', '-e', port, from, to] 152 | child.stdout.on 'data', (out) -> console.log out.toString() 153 | child.stderr.on 'data', (err) -> console.error err.toString() 154 | child.on 'exit', callback if callback 155 | 156 | 157 | Maintain a Manifest File 158 | ------------------------ 159 | 160 | The "manifest" is where Journo keeps track of metadata -- the title, description, 161 | publications date and last modified time of each post. Everything you need to 162 | render out an RSS feed ... and everything you need to know if a post has been 163 | updated or removed. 164 | 165 | manifestPath = 'journo-manifest.json' 166 | 167 | loadManifest = -> 168 | do loadConfig 169 | 170 | shared.manifest = if fs.existsSync manifestPath 171 | JSON.parse fs.readFileSync manifestPath 172 | else 173 | {} 174 | 175 | do updateManifest 176 | fs.writeFileSync manifestPath, JSON.stringify shared.manifest 177 | 178 | We update the manifest by looping through every post and every entry in the 179 | existing manifest, looking for differences in `mtime`, and recording those 180 | along with the title and description of each post. 181 | 182 | updateManifest = -> 183 | manifest = shared.manifest 184 | posts = folderContents 'posts' 185 | 186 | delete manifest[post] for post of manifest when post not in posts 187 | 188 | for post in posts 189 | stat = fs.statSync postPath post 190 | entry = manifest[post] 191 | if not entry or entry.mtime isnt stat.mtime 192 | entry or= {pubtime: stat.ctime} 193 | entry.mtime = stat.mtime 194 | content = fs.readFileSync(postPath post).toString() 195 | entry.title = detectTitle content 196 | entry.description = detectDescription content, post 197 | manifest[post] = entry 198 | 199 | yes 200 | 201 | 202 | Retina Ready 203 | ------------ 204 | 205 | In the future, it may make sense for Journo to have some sort of built-in 206 | facility for automatically downsizing photos from retina to regular sizes ... 207 | But for now, this bit is up to you. 208 | 209 | 210 | Syntax Highlight Code 211 | --------------------- 212 | 213 | We syntax-highlight blocks of code with the nifty **highlight** package that 214 | includes heuristics for auto-language detection, so you don't have to specify 215 | what you're coding in. 216 | 217 | highlight = require 'highlight.js' 218 | 219 | marked.setOptions 220 | highlight: (code, lang) -> 221 | if highlight.LANGUAGES[lang]? 222 | highlight.highlight(lang, code, true).value 223 | else 224 | highlight.highlightAuto(code).value 225 | 226 | Publish a Feed 227 | -------------- 228 | 229 | We'll use the **rss** module to build a simple feed of recent posts. Start with 230 | the basic `author`, blog `title`, `description` and `url` configured in the 231 | `config.json`. Then, each post's `title` is the first header present in the 232 | post, the `description` is the first paragraph, and the date is the date you 233 | first created the post file. 234 | 235 | Journo.feed = -> 236 | RSS = require 'rss' 237 | do loadConfig 238 | config = shared.config 239 | 240 | feed = new RSS 241 | title: config.title 242 | description: config.description 243 | feed_url: "#{shared.siteUrl}/rss.xml" 244 | site_url: shared.siteUrl 245 | author: config.author 246 | 247 | for post in sortedPosts().reverse()[0...20] 248 | entry = shared.manifest[post] 249 | feed.item 250 | title: entry.title 251 | description: entry.description 252 | url: postUrl post 253 | date: entry.pubtime 254 | 255 | feed.xml() 256 | 257 | 258 | Quickly Bootstrap a New Blog 259 | ---------------------------- 260 | 261 | We **init** a new blog into the current directory by copying over the contents 262 | of a basic `bootstrap` folder. 263 | 264 | Journo.init = -> 265 | here = fs.realpathSync '.' 266 | if fs.existsSync 'posts' 267 | fatal "A blog already exists in #{here}" 268 | bootstrap = path.join(__dirname, 'bootstrap/*') 269 | exec "rsync -vur --delete #{bootstrap} .", (err, stdout, stderr) -> 270 | throw err if err 271 | console.log "Initialized new blog in #{here}" 272 | 273 | 274 | Preview via a Local Server 275 | -------------------------- 276 | 277 | Instead of constantly rebuilding a purely static version of the site, Journo 278 | provides a preview server (which you can start by just typing `journo` from 279 | within your blog). 280 | 281 | Journo.preview = -> 282 | http = require 'http' 283 | mime = require 'mime' 284 | url = require 'url' 285 | util = require 'util' 286 | do loadManifest 287 | 288 | server = http.createServer (req, res) -> 289 | rawPath = url.parse(req.url).pathname.replace(/(^\/|\/$)/g, '') or 'index' 290 | 291 | If the request is for a preview of the RSS feed... 292 | 293 | if rawPath is 'feed.rss' 294 | res.writeHead 200, 'Content-Type': mime.lookup('.rss') 295 | res.end Journo.feed() 296 | 297 | If the request is for a static file that exists in our `public` directory... 298 | 299 | else 300 | publicPath = "public/" + rawPath 301 | fs.exists publicPath, (exists) -> 302 | if exists 303 | res.writeHead 200, 'Content-Type': mime.lookup(publicPath) 304 | fs.createReadStream(publicPath).pipe res 305 | 306 | If the request is for the slug of a valid post, we reload the layout, and 307 | render it... 308 | 309 | else 310 | post = "posts/#{rawPath}.md" 311 | fs.exists post, (exists) -> 312 | if exists 313 | loadLayout true 314 | fs.readFile post, (err, content) -> 315 | res.writeHead 200, 'Content-Type': 'text/html' 316 | res.end Journo.render post, content 317 | 318 | Anything else is a 404. 319 | 320 | else 321 | res.writeHead 404 322 | res.end '404 Not Found' 323 | 324 | server.listen 1234 325 | console.log "Journo is previewing at http://localhost:1234" 326 | exec "#{opener} http://localhost:1234" 327 | 328 | 329 | Work Without JavaScript, But Default to a Fluid JavaScript-Enabled UI 330 | --------------------------------------------------------------------- 331 | 332 | The best way to handle this bit seems to be entirely on the client-side. For 333 | example, when rendering a JavaScript slideshow of photographs, instead of 334 | having the server spit out the slideshow code, simply have the blog detect 335 | the list of images during page load and move them into a slideshow right then 336 | and there -- using `alt` attributes for captions, for example. 337 | 338 | Since the blog is public, it's nice if search engines can see all of the pieces 339 | as well as readers. 340 | 341 | 342 | Finally, Putting it all Together. Run Journo From the Terminal 343 | -------------------------------------------------------------- 344 | 345 | We'll do the simplest possible command-line interface. If a public function 346 | exists on the `Journo` object, you can run it. *Note that this lets you do 347 | silly things, like* `journo toString` *but no big deal.* 348 | 349 | Journo.run = -> 350 | command = process.argv[2] or 'preview' 351 | return do Journo[command] if Journo[command] 352 | console.error "Journo doesn't know how to '#{command}'" 353 | 354 | Let's also provide a help page that lists the available commands. 355 | 356 | Journo.help = Journo['--help'] = -> 357 | console.log """ 358 | Usage: journo [command] 359 | 360 | If called without a command, `journo` will preview your blog. 361 | 362 | init start a new blog in the current folder 363 | build build a static version of the blog into 'site' 364 | preview live preview the blog via a local server 365 | publish publish the blog to your remote server 366 | """ 367 | 368 | And we might as well do the version number, for completeness' sake. 369 | 370 | Journo.version = Journo['--version'] = -> 371 | console.log "Journo 0.0.1" 372 | 373 | 374 | Miscellaneous Bits and Utilities 375 | -------------------------------- 376 | 377 | Little utility functions that are useful up above. 378 | 379 | The file path to the source of a given `post`. 380 | 381 | postPath = (post) -> "posts/#{post}" 382 | 383 | The server-side path to the HTML for a given `post`. 384 | 385 | htmlPath = (post) -> 386 | name = postName post 387 | if name is 'index' 388 | 'site/index.html' 389 | else 390 | "site/#{name}/index.html" 391 | 392 | The name (or slug) of a post, taken from the filename. 393 | 394 | postName = (post) -> path.basename post, '.md' 395 | 396 | The full, absolute URL for a published post. 397 | 398 | postUrl = (post) -> "#{shared.siteUrl}/#{postName(post)}/" 399 | 400 | Starting with the string contents of a post, detect the title -- 401 | the first heading. 402 | 403 | detectTitle = (content) -> 404 | _.find(marked.lexer(content), (token) -> token.type is 'heading')?.text 405 | 406 | Starting with the string contents of a post, detect the description -- 407 | the first paragraph. 408 | 409 | detectDescription = (content, post) -> 410 | desc = _.find(marked.lexer(content), (token) -> token.type is 'paragraph')?.text 411 | marked.parser marked.lexer _.template("#{desc}...")(renderVariables(post)) 412 | 413 | Helper function to read in the contents of a folder, ignoring hidden files 414 | and directories. 415 | 416 | folderContents = (folder) -> 417 | fs.readdirSync(folder).filter (f) -> f.charAt(0) isnt '.' 418 | 419 | Return the list of posts currently in the manifest, sorted by their date of 420 | publication. 421 | 422 | sortedPosts = -> 423 | _.sortBy _.without(_.keys(shared.manifest), 'index.md'), (post) -> 424 | shared.manifest[post].pubtime 425 | 426 | The shared variables we want to allow our templates (both posts, and layout) 427 | to use in their evaluations. In the future, it would be nice to determine 428 | exactly what best belongs here, and provide an easier way for the blog author 429 | to add functions to it. 430 | 431 | renderVariables = (post) -> 432 | { 433 | _ 434 | fs 435 | path 436 | mapLink 437 | postName 438 | folderContents 439 | posts: sortedPosts() 440 | post: path.basename(post) 441 | manifest: shared.manifest 442 | } 443 | 444 | Quick function which creates a link to a Google Map search for the name of the 445 | place. 446 | 447 | mapLink = (place, additional = '', zoom = 15) -> 448 | query = encodeURIComponent("#{place}, #{additional}") 449 | "#{place}" 450 | 451 | Convenience function for catching errors (keeping the preview server from 452 | crashing while testing code), and printing them out. 453 | 454 | catchErrors = (func) -> 455 | try do func 456 | catch err 457 | console.error err.stack 458 | "
#{err.stack}
" 459 | 460 | Finally, for errors that you want the app to die on -- things that should break 461 | the site build. 462 | 463 | fatal = (message) -> 464 | console.error message 465 | process.exit 1 466 | 467 | 468 | 469 | -------------------------------------------------------------------------------- /test/fixtures/grammar.coffee: -------------------------------------------------------------------------------- 1 | # The CoffeeScript parser is generated by [Jison](http://github.com/zaach/jison) 2 | # from this grammar file. Jison is a bottom-up parser generator, similar in 3 | # style to [Bison](http://www.gnu.org/software/bison), implemented in JavaScript. 4 | # It can recognize [LALR(1), LR(0), SLR(1), and LR(1)](http://en.wikipedia.org/wiki/LR_grammar) 5 | # type grammars. To create the Jison parser, we list the pattern to match 6 | # on the left-hand side, and the action to take (usually the creation of syntax 7 | # tree nodes) on the right. As the parser runs, it 8 | # shifts tokens from our token stream, from left to right, and 9 | # [attempts to match](http://en.wikipedia.org/wiki/Bottom-up_parsing) 10 | # the token sequence against the rules below. When a match can be made, it 11 | # reduces into the [nonterminal](http://en.wikipedia.org/wiki/Terminal_and_nonterminal_symbols) 12 | # (the enclosing name at the top), and we proceed from there. 13 | # 14 | # If you run the `cake build:parser` command, Jison constructs a parse table 15 | # from our rules and saves it into `lib/parser.js`. 16 | 17 | # The only dependency is on the **Jison.Parser**. 18 | {Parser} = require 'jison' 19 | 20 | # Jison DSL 21 | # --------- 22 | 23 | # Since we're going to be wrapped in a function by Jison in any case, if our 24 | # action immediately returns a value, we can optimize by removing the function 25 | # wrapper and just returning the value directly. 26 | unwrap = /^function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/ 27 | 28 | # Our handy DSL for Jison grammar generation, thanks to 29 | # [Tim Caswell](http://github.com/creationix). For every rule in the grammar, 30 | # we pass the pattern-defining string, the action to run, and extra options, 31 | # optionally. If no action is specified, we simply pass the value of the 32 | # previous nonterminal. 33 | o = (patternString, action, options) -> 34 | patternString = patternString.replace /\s{2,}/g, ' ' 35 | patternCount = patternString.split(' ').length 36 | return [patternString, '$$ = $1;', options] unless action 37 | action = if match = unwrap.exec action then match[1] else "(#{action}())" 38 | 39 | # All runtime functions we need are defined on "yy" 40 | action = action.replace /\bnew /g, '$&yy.' 41 | action = action.replace /\b(?:Block\.wrap|extend)\b/g, 'yy.$&' 42 | 43 | # Returns a function which adds location data to the first parameter passed 44 | # in, and returns the parameter. If the parameter is not a node, it will 45 | # just be passed through unaffected. 46 | addLocationDataFn = (first, last) -> 47 | if not last 48 | "yy.addLocationDataFn(@#{first})" 49 | else 50 | "yy.addLocationDataFn(@#{first}, @#{last})" 51 | 52 | action = action.replace /LOC\(([0-9]*)\)/g, addLocationDataFn('$1') 53 | action = action.replace /LOC\(([0-9]*),\s*([0-9]*)\)/g, addLocationDataFn('$1', '$2') 54 | 55 | [patternString, "$$ = #{addLocationDataFn(1, patternCount)}(#{action});", options] 56 | 57 | # Grammatical Rules 58 | # ----------------- 59 | 60 | # In all of the rules that follow, you'll see the name of the nonterminal as 61 | # the key to a list of alternative matches. With each match's action, the 62 | # dollar-sign variables are provided by Jison as references to the value of 63 | # their numeric position, so in this rule: 64 | # 65 | # "Expression UNLESS Expression" 66 | # 67 | # `$1` would be the value of the first `Expression`, `$2` would be the token 68 | # for the `UNLESS` terminal, and `$3` would be the value of the second 69 | # `Expression`. 70 | grammar = 71 | 72 | # The **Root** is the top-level node in the syntax tree. Since we parse bottom-up, 73 | # all parsing must end here. 74 | Root: [ 75 | o '', -> new Block 76 | o 'Body' 77 | ] 78 | 79 | # Any list of statements and expressions, separated by line breaks or semicolons. 80 | Body: [ 81 | o 'Line', -> Block.wrap [$1] 82 | o 'Body TERMINATOR Line', -> $1.push $3 83 | o 'Body TERMINATOR' 84 | ] 85 | 86 | # Block and statements, which make up a line in a body. 87 | Line: [ 88 | o 'Expression' 89 | o 'Statement' 90 | ] 91 | 92 | # Pure statements which cannot be expressions. 93 | Statement: [ 94 | o 'Return' 95 | o 'Comment' 96 | o 'STATEMENT', -> new Literal $1 97 | ] 98 | 99 | # All the different types of expressions in our language. The basic unit of 100 | # CoffeeScript is the **Expression** -- everything that can be an expression 101 | # is one. Blocks serve as the building blocks of many other rules, making 102 | # them somewhat circular. 103 | Expression: [ 104 | o 'Value' 105 | o 'Invocation' 106 | o 'Code' 107 | o 'Operation' 108 | o 'Assign' 109 | o 'If' 110 | o 'Try' 111 | o 'While' 112 | o 'For' 113 | o 'Switch' 114 | o 'Class' 115 | o 'Throw' 116 | ] 117 | 118 | # An indented block of expressions. Note that the [Rewriter](rewriter.html) 119 | # will convert some postfix forms into blocks for us, by adjusting the 120 | # token stream. 121 | Block: [ 122 | o 'INDENT OUTDENT', -> new Block 123 | o 'INDENT Body OUTDENT', -> $2 124 | ] 125 | 126 | # A literal identifier, a variable name or property. 127 | Identifier: [ 128 | o 'IDENTIFIER', -> new Literal $1 129 | ] 130 | 131 | # Alphanumerics are separated from the other **Literal** matchers because 132 | # they can also serve as keys in object literals. 133 | AlphaNumeric: [ 134 | o 'NUMBER', -> new Literal $1 135 | o 'STRING', -> new Literal $1 136 | ] 137 | 138 | # All of our immediate values. Generally these can be passed straight 139 | # through and printed to JavaScript. 140 | Literal: [ 141 | o 'AlphaNumeric' 142 | o 'JS', -> new Literal $1 143 | o 'REGEX', -> new Literal $1 144 | o 'DEBUGGER', -> new Literal $1 145 | o 'UNDEFINED', -> new Undefined 146 | o 'NULL', -> new Null 147 | o 'BOOL', -> new Bool $1 148 | ] 149 | 150 | # Assignment of a variable, property, or index to a value. 151 | Assign: [ 152 | o 'Assignable = Expression', -> new Assign $1, $3 153 | o 'Assignable = TERMINATOR Expression', -> new Assign $1, $4 154 | o 'Assignable = INDENT Expression OUTDENT', -> new Assign $1, $4 155 | ] 156 | 157 | # Assignment when it happens within an object literal. The difference from 158 | # the ordinary **Assign** is that these allow numbers and strings as keys. 159 | AssignObj: [ 160 | o 'ObjAssignable', -> new Value $1 161 | o 'ObjAssignable : Expression', -> new Assign LOC(1)(new Value($1)), $3, 'object' 162 | o 'ObjAssignable : 163 | INDENT Expression OUTDENT', -> new Assign LOC(1)(new Value($1)), $4, 'object' 164 | o 'Comment' 165 | ] 166 | 167 | ObjAssignable: [ 168 | o 'Identifier' 169 | o 'AlphaNumeric' 170 | o 'ThisProperty' 171 | ] 172 | 173 | # A return statement from a function body. 174 | Return: [ 175 | o 'RETURN Expression', -> new Return $2 176 | o 'RETURN', -> new Return 177 | ] 178 | 179 | # A block comment. 180 | Comment: [ 181 | o 'HERECOMMENT', -> new Comment $1 182 | ] 183 | 184 | # The **Code** node is the function literal. It's defined by an indented block 185 | # of **Block** preceded by a function arrow, with an optional parameter 186 | # list. 187 | Code: [ 188 | o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4 189 | o 'FuncGlyph Block', -> new Code [], $2, $1 190 | ] 191 | 192 | # CoffeeScript has two different symbols for functions. `->` is for ordinary 193 | # functions, and `=>` is for functions bound to the current value of *this*. 194 | FuncGlyph: [ 195 | o '->', -> 'func' 196 | o '=>', -> 'boundfunc' 197 | ] 198 | 199 | # An optional, trailing comma. 200 | OptComma: [ 201 | o '' 202 | o ',' 203 | ] 204 | 205 | # The list of parameters that a function accepts can be of any length. 206 | ParamList: [ 207 | o '', -> [] 208 | o 'Param', -> [$1] 209 | o 'ParamList , Param', -> $1.concat $3 210 | o 'ParamList OptComma TERMINATOR Param', -> $1.concat $4 211 | o 'ParamList OptComma INDENT ParamList OptComma OUTDENT', -> $1.concat $4 212 | ] 213 | 214 | # A single parameter in a function definition can be ordinary, or a splat 215 | # that hoovers up the remaining arguments. 216 | Param: [ 217 | o 'ParamVar', -> new Param $1 218 | o 'ParamVar ...', -> new Param $1, null, on 219 | o 'ParamVar = Expression', -> new Param $1, $3 220 | ] 221 | 222 | # Function Parameters 223 | ParamVar: [ 224 | o 'Identifier' 225 | o 'ThisProperty' 226 | o 'Array' 227 | o 'Object' 228 | ] 229 | 230 | # A splat that occurs outside of a parameter list. 231 | Splat: [ 232 | o 'Expression ...', -> new Splat $1 233 | ] 234 | 235 | # Variables and properties that can be assigned to. 236 | SimpleAssignable: [ 237 | o 'Identifier', -> new Value $1 238 | o 'Value Accessor', -> $1.add $2 239 | o 'Invocation Accessor', -> new Value $1, [].concat $2 240 | o 'ThisProperty' 241 | ] 242 | 243 | # Everything that can be assigned to. 244 | Assignable: [ 245 | o 'SimpleAssignable' 246 | o 'Array', -> new Value $1 247 | o 'Object', -> new Value $1 248 | ] 249 | 250 | # The types of things that can be treated as values -- assigned to, invoked 251 | # as functions, indexed into, named as a class, etc. 252 | Value: [ 253 | o 'Assignable' 254 | o 'Literal', -> new Value $1 255 | o 'Parenthetical', -> new Value $1 256 | o 'Range', -> new Value $1 257 | o 'This' 258 | ] 259 | 260 | # The general group of accessors into an object, by property, by prototype 261 | # or by array index or slice. 262 | Accessor: [ 263 | o '. Identifier', -> new Access $2 264 | o '?. Identifier', -> new Access $2, 'soak' 265 | o ':: Identifier', -> [LOC(1)(new Access new Literal('prototype')), LOC(2)(new Access $2)] 266 | o '?:: Identifier', -> [LOC(1)(new Access new Literal('prototype'), 'soak'), LOC(2)(new Access $2)] 267 | o '::', -> new Access new Literal 'prototype' 268 | o 'Index' 269 | ] 270 | 271 | # Indexing into an object or array using bracket notation. 272 | Index: [ 273 | o 'INDEX_START IndexValue INDEX_END', -> $2 274 | o 'INDEX_SOAK Index', -> extend $2, soak : yes 275 | ] 276 | 277 | IndexValue: [ 278 | o 'Expression', -> new Index $1 279 | o 'Slice', -> new Slice $1 280 | ] 281 | 282 | # In CoffeeScript, an object literal is simply a list of assignments. 283 | Object: [ 284 | o '{ AssignList OptComma }', -> new Obj $2, $1.generated 285 | ] 286 | 287 | # Assignment of properties within an object literal can be separated by 288 | # comma, as in JavaScript, or simply by newline. 289 | AssignList: [ 290 | o '', -> [] 291 | o 'AssignObj', -> [$1] 292 | o 'AssignList , AssignObj', -> $1.concat $3 293 | o 'AssignList OptComma TERMINATOR AssignObj', -> $1.concat $4 294 | o 'AssignList OptComma INDENT AssignList OptComma OUTDENT', -> $1.concat $4 295 | ] 296 | 297 | # Class definitions have optional bodies of prototype property assignments, 298 | # and optional references to the superclass. 299 | Class: [ 300 | o 'CLASS', -> new Class 301 | o 'CLASS Block', -> new Class null, null, $2 302 | o 'CLASS EXTENDS Expression', -> new Class null, $3 303 | o 'CLASS EXTENDS Expression Block', -> new Class null, $3, $4 304 | o 'CLASS SimpleAssignable', -> new Class $2 305 | o 'CLASS SimpleAssignable Block', -> new Class $2, null, $3 306 | o 'CLASS SimpleAssignable EXTENDS Expression', -> new Class $2, $4 307 | o 'CLASS SimpleAssignable EXTENDS Expression Block', -> new Class $2, $4, $5 308 | ] 309 | 310 | # Ordinary function invocation, or a chained series of calls. 311 | Invocation: [ 312 | o 'Value OptFuncExist Arguments', -> new Call $1, $3, $2 313 | o 'Invocation OptFuncExist Arguments', -> new Call $1, $3, $2 314 | o 'SUPER', -> new Call 'super', [new Splat new Literal 'arguments'] 315 | o 'SUPER Arguments', -> new Call 'super', $2 316 | ] 317 | 318 | # An optional existence check on a function. 319 | OptFuncExist: [ 320 | o '', -> no 321 | o 'FUNC_EXIST', -> yes 322 | ] 323 | 324 | # The list of arguments to a function call. 325 | Arguments: [ 326 | o 'CALL_START CALL_END', -> [] 327 | o 'CALL_START ArgList OptComma CALL_END', -> $2 328 | ] 329 | 330 | # A reference to the *this* current object. 331 | This: [ 332 | o 'THIS', -> new Value new Literal 'this' 333 | o '@', -> new Value new Literal 'this' 334 | ] 335 | 336 | # A reference to a property on *this*. 337 | ThisProperty: [ 338 | o '@ Identifier', -> new Value LOC(1)(new Literal('this')), [LOC(2)(new Access($2))], 'this' 339 | ] 340 | 341 | # The array literal. 342 | Array: [ 343 | o '[ ]', -> new Arr [] 344 | o '[ ArgList OptComma ]', -> new Arr $2 345 | ] 346 | 347 | # Inclusive and exclusive range dots. 348 | RangeDots: [ 349 | o '..', -> 'inclusive' 350 | o '...', -> 'exclusive' 351 | ] 352 | 353 | # The CoffeeScript range literal. 354 | Range: [ 355 | o '[ Expression RangeDots Expression ]', -> new Range $2, $4, $3 356 | ] 357 | 358 | # Array slice literals. 359 | Slice: [ 360 | o 'Expression RangeDots Expression', -> new Range $1, $3, $2 361 | o 'Expression RangeDots', -> new Range $1, null, $2 362 | o 'RangeDots Expression', -> new Range null, $2, $1 363 | o 'RangeDots', -> new Range null, null, $1 364 | ] 365 | 366 | # The **ArgList** is both the list of objects passed into a function call, 367 | # as well as the contents of an array literal 368 | # (i.e. comma-separated expressions). Newlines work as well. 369 | ArgList: [ 370 | o 'Arg', -> [$1] 371 | o 'ArgList , Arg', -> $1.concat $3 372 | o 'ArgList OptComma TERMINATOR Arg', -> $1.concat $4 373 | o 'INDENT ArgList OptComma OUTDENT', -> $2 374 | o 'ArgList OptComma INDENT ArgList OptComma OUTDENT', -> $1.concat $4 375 | ] 376 | 377 | # Valid arguments are Blocks or Splats. 378 | Arg: [ 379 | o 'Expression' 380 | o 'Splat' 381 | ] 382 | 383 | # Just simple, comma-separated, required arguments (no fancy syntax). We need 384 | # this to be separate from the **ArgList** for use in **Switch** blocks, where 385 | # having the newlines wouldn't make sense. 386 | SimpleArgs: [ 387 | o 'Expression' 388 | o 'SimpleArgs , Expression', -> [].concat $1, $3 389 | ] 390 | 391 | # The variants of *try/catch/finally* exception handling blocks. 392 | Try: [ 393 | o 'TRY Block', -> new Try $2 394 | o 'TRY Block Catch', -> new Try $2, $3[0], $3[1] 395 | o 'TRY Block FINALLY Block', -> new Try $2, null, null, $4 396 | o 'TRY Block Catch FINALLY Block', -> new Try $2, $3[0], $3[1], $5 397 | ] 398 | 399 | # A catch clause names its error and runs a block of code. 400 | Catch: [ 401 | o 'CATCH Identifier Block', -> [$2, $3] 402 | o 'CATCH Object Block', -> [LOC(2)(new Value($2)), $3] 403 | o 'CATCH Block', -> [null, $2] 404 | ] 405 | 406 | # Throw an exception object. 407 | Throw: [ 408 | o 'THROW Expression', -> new Throw $2 409 | ] 410 | 411 | # Parenthetical expressions. Note that the **Parenthetical** is a **Value**, 412 | # not an **Expression**, so if you need to use an expression in a place 413 | # where only values are accepted, wrapping it in parentheses will always do 414 | # the trick. 415 | Parenthetical: [ 416 | o '( Body )', -> new Parens $2 417 | o '( INDENT Body OUTDENT )', -> new Parens $3 418 | ] 419 | 420 | # The condition portion of a while loop. 421 | WhileSource: [ 422 | o 'WHILE Expression', -> new While $2 423 | o 'WHILE Expression WHEN Expression', -> new While $2, guard: $4 424 | o 'UNTIL Expression', -> new While $2, invert: true 425 | o 'UNTIL Expression WHEN Expression', -> new While $2, invert: true, guard: $4 426 | ] 427 | 428 | # The while loop can either be normal, with a block of expressions to execute, 429 | # or postfix, with a single expression. There is no do..while. 430 | While: [ 431 | o 'WhileSource Block', -> $1.addBody $2 432 | o 'Statement WhileSource', -> $2.addBody LOC(1) Block.wrap([$1]) 433 | o 'Expression WhileSource', -> $2.addBody LOC(1) Block.wrap([$1]) 434 | o 'Loop', -> $1 435 | ] 436 | 437 | Loop: [ 438 | o 'LOOP Block', -> new While(LOC(1) new Literal 'true').addBody $2 439 | o 'LOOP Expression', -> new While(LOC(1) new Literal 'true').addBody LOC(2) Block.wrap [$2] 440 | ] 441 | 442 | # Array, object, and range comprehensions, at the most generic level. 443 | # Comprehensions can either be normal, with a block of expressions to execute, 444 | # or postfix, with a single expression. 445 | For: [ 446 | o 'Statement ForBody', -> new For $1, $2 447 | o 'Expression ForBody', -> new For $1, $2 448 | o 'ForBody Block', -> new For $2, $1 449 | ] 450 | 451 | ForBody: [ 452 | o 'FOR Range', -> source: LOC(2) new Value($2) 453 | o 'ForStart ForSource', -> $2.own = $1.own; $2.name = $1[0]; $2.index = $1[1]; $2 454 | ] 455 | 456 | ForStart: [ 457 | o 'FOR ForVariables', -> $2 458 | o 'FOR OWN ForVariables', -> $3.own = yes; $3 459 | ] 460 | 461 | # An array of all accepted values for a variable inside the loop. 462 | # This enables support for pattern matching. 463 | ForValue: [ 464 | o 'Identifier' 465 | o 'ThisProperty' 466 | o 'Array', -> new Value $1 467 | o 'Object', -> new Value $1 468 | ] 469 | 470 | # An array or range comprehension has variables for the current element 471 | # and (optional) reference to the current index. Or, *key, value*, in the case 472 | # of object comprehensions. 473 | ForVariables: [ 474 | o 'ForValue', -> [$1] 475 | o 'ForValue , ForValue', -> [$1, $3] 476 | ] 477 | 478 | # The source of a comprehension is an array or object with an optional guard 479 | # clause. If it's an array comprehension, you can also choose to step through 480 | # in fixed-size increments. 481 | ForSource: [ 482 | o 'FORIN Expression', -> source: $2 483 | o 'FOROF Expression', -> source: $2, object: yes 484 | o 'FORIN Expression WHEN Expression', -> source: $2, guard: $4 485 | o 'FOROF Expression WHEN Expression', -> source: $2, guard: $4, object: yes 486 | o 'FORIN Expression BY Expression', -> source: $2, step: $4 487 | o 'FORIN Expression WHEN Expression BY Expression', -> source: $2, guard: $4, step: $6 488 | o 'FORIN Expression BY Expression WHEN Expression', -> source: $2, step: $4, guard: $6 489 | ] 490 | 491 | Switch: [ 492 | o 'SWITCH Expression INDENT Whens OUTDENT', -> new Switch $2, $4 493 | o 'SWITCH Expression INDENT Whens ELSE Block OUTDENT', -> new Switch $2, $4, $6 494 | o 'SWITCH INDENT Whens OUTDENT', -> new Switch null, $3 495 | o 'SWITCH INDENT Whens ELSE Block OUTDENT', -> new Switch null, $3, $5 496 | ] 497 | 498 | Whens: [ 499 | o 'When' 500 | o 'Whens When', -> $1.concat $2 501 | ] 502 | 503 | # An individual **When** clause, with action. 504 | When: [ 505 | o 'LEADING_WHEN SimpleArgs Block', -> [[$2, $3]] 506 | o 'LEADING_WHEN SimpleArgs Block TERMINATOR', -> [[$2, $3]] 507 | ] 508 | 509 | # The most basic form of *if* is a condition and an action. The following 510 | # if-related rules are broken up along these lines in order to avoid 511 | # ambiguity. 512 | IfBlock: [ 513 | o 'IF Expression Block', -> new If $2, $3, type: $1 514 | o 'IfBlock ELSE IF Expression Block', -> $1.addElse LOC(3,5) new If $4, $5, type: $3 515 | ] 516 | 517 | # The full complement of *if* expressions, including postfix one-liner 518 | # *if* and *unless*. 519 | If: [ 520 | o 'IfBlock' 521 | o 'IfBlock ELSE Block', -> $1.addElse $3 522 | o 'Statement POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true 523 | o 'Expression POST_IF Expression', -> new If $3, LOC(1)(Block.wrap [$1]), type: $2, statement: true 524 | ] 525 | 526 | # Arithmetic and logical operators, working on one or more operands. 527 | # Here they are grouped by order of precedence. The actual precedence rules 528 | # are defined at the bottom of the page. It would be shorter if we could 529 | # combine most of these rules into a single generic *Operand OpSymbol Operand* 530 | # -type rule, but in order to make the precedence binding possible, separate 531 | # rules are necessary. 532 | Operation: [ 533 | o 'UNARY Expression', -> new Op $1 , $2 534 | o '- Expression', (-> new Op '-', $2), prec: 'UNARY' 535 | o '+ Expression', (-> new Op '+', $2), prec: 'UNARY' 536 | 537 | o '-- SimpleAssignable', -> new Op '--', $2 538 | o '++ SimpleAssignable', -> new Op '++', $2 539 | o 'SimpleAssignable --', -> new Op '--', $1, null, true 540 | o 'SimpleAssignable ++', -> new Op '++', $1, null, true 541 | 542 | # [The existential operator](http://jashkenas.github.com/coffee-script/#existence). 543 | o 'Expression ?', -> new Existence $1 544 | 545 | o 'Expression + Expression', -> new Op '+' , $1, $3 546 | o 'Expression - Expression', -> new Op '-' , $1, $3 547 | 548 | o 'Expression MATH Expression', -> new Op $2, $1, $3 549 | o 'Expression SHIFT Expression', -> new Op $2, $1, $3 550 | o 'Expression COMPARE Expression', -> new Op $2, $1, $3 551 | o 'Expression LOGIC Expression', -> new Op $2, $1, $3 552 | o 'Expression RELATION Expression', -> 553 | if $2.charAt(0) is '!' 554 | new Op($2[1..], $1, $3).invert() 555 | else 556 | new Op $2, $1, $3 557 | 558 | o 'SimpleAssignable COMPOUND_ASSIGN 559 | Expression', -> new Assign $1, $3, $2 560 | o 'SimpleAssignable COMPOUND_ASSIGN 561 | INDENT Expression OUTDENT', -> new Assign $1, $4, $2 562 | o 'SimpleAssignable COMPOUND_ASSIGN TERMINATOR 563 | Expression', -> new Assign $1, $4, $2 564 | o 'SimpleAssignable EXTENDS Expression', -> new Extends $1, $3 565 | ] 566 | 567 | 568 | # Precedence 569 | # ---------- 570 | 571 | # Operators at the top of this list have higher precedence than the ones lower 572 | # down. Following these rules is what makes `2 + 3 * 4` parse as: 573 | # 574 | # 2 + (3 * 4) 575 | # 576 | # And not: 577 | # 578 | # (2 + 3) * 4 579 | operators = [ 580 | ['left', '.', '?.', '::', '?::'] 581 | ['left', 'CALL_START', 'CALL_END'] 582 | ['nonassoc', '++', '--'] 583 | ['left', '?'] 584 | ['right', 'UNARY'] 585 | ['left', 'MATH'] 586 | ['left', '+', '-'] 587 | ['left', 'SHIFT'] 588 | ['left', 'RELATION'] 589 | ['left', 'COMPARE'] 590 | ['left', 'LOGIC'] 591 | ['nonassoc', 'INDENT', 'OUTDENT'] 592 | ['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS'] 593 | ['right', 'FORIN', 'FOROF', 'BY', 'WHEN'] 594 | ['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS'] 595 | ['left', 'POST_IF'] 596 | ] 597 | 598 | # Wrapping Up 599 | # ----------- 600 | 601 | # Finally, now that we have our **grammar** and our **operators**, we can create 602 | # our **Jison.Parser**. We do this by processing all of our rules, recording all 603 | # terminals (every symbol which does not appear as the name of a rule above) 604 | # as "tokens". 605 | tokens = [] 606 | for name, alternatives of grammar 607 | grammar[name] = for alt in alternatives 608 | for token in alt[0].split ' ' 609 | tokens.push token unless grammar[token] 610 | alt[1] = "return #{alt[1]}" if name is 'Root' 611 | alt 612 | 613 | # Initialize the **Parser** with our list of terminal **tokens**, our **grammar** 614 | # rules, and the name of the root. Reverse the operators because Jison orders 615 | # precedence from low to high, and we have it high to low 616 | # (as in [Yacc](http://dinosaur.compilertools.net/yacc/index.html)). 617 | exports.parser = new Parser 618 | tokens : tokens.join ' ' 619 | bnf : grammar 620 | operators : operators.reverse() 621 | startSymbol : 'Root' 622 | --------------------------------------------------------------------------------