Jeremy Ashkenas has introduced Literate CoffeeScript, having been inspired by Donald Knuth's early work on Literate Programming.
According to Knuth, literate programming provides higher-quality programs, since it forces programmers to explicitly state the thoughts behind the program, making poorly thought-out design decisions more obvious. Knuth also claims that literate programming provides a first-rate documentation system, which is not an add-on, but is grown naturally in the process of exposition of one's thoughts during a program's creation. The resulting documentation allows authors to restart their own thought processes at any later time, and allows other programmers to understand the construction of the program more easily.
Literate Source Code takes Jeremy's idea ( of using John Gruber's Markdown for writing the documentation ) and applies it — without any restrictions — to any programming language with a little help from Grunt.
There are only 2 rules to follow :
- Not indented lines do author the documentation.
- Indented lines do list the executable code to extract.
This approach reflects Markdown's way of denoting code blocks.
At this initial stage the implementation of a plugin would be an overkill and hence the simplest approach, as described below, has been chosen.
module.exports = function( grunt )
{
grunt.initConfig({
Kyle R. Young's Watch task ensures that the process of filtering out the executable code will happen on the fly, i.e. without any further manual intervention — just create / edit the LSC. Following the file extension convention of CoffeeScript, some of the popular mark up / pre-processor / scripting languages for web development and design have been marked as literate by appending .md
to the file extension and using the Minimal Matching Utility of Isaac Z. Schlueter for folder independent watching.
watch : {
literateCode : {
files : [
'**/*.haml.md', '**/*.html.md', '**/*.jade.md', // mark up
'**/*.less.md', '**/*.rool.md', '**/*.sass.md', '**/*.scss.md', '**/*.stylus.md', // style sheet
'**/*.js.md', '**/*.php.md', // script
'**/*.ini.md', '**/*.json.md' // configuration
]
}
}
});
Once the plugin has been installed, it should be enabled :)
grunt.loadNpmTasks( 'grunt-contrib-watch' );
Every time a file ( extension ) being watched is added
or changed
the watch task triggers the respective event. This is the hook used to extract the executable code and to write the target file — by removing the file extension .md
.
grunt.event.on( 'watch', function( event, literateCode ) {
var executableCode = literateCode.replace( /(.+?)\.md$/g, '$1' );
if( event === 'added' || event === 'changed' ) {
var content = grunt.file.read( literateCode );
content = content.replace( /^\S+.*?$/mg, '' ); // remove documentation lines
content = content.replace( /[ \t]*\/\/.*?\n/g, '\n'); // remove single-line comments
content = content.replace( /\n+([ \t]*\n){2,}/g, '\n\n' ); // remove multiple empty lines
content = content.replace( /^(\n*[ \t]*\n)+/, '' ); // remove empty lines at the beginning of the file
content = content.replace( /(\n*[ \t]*\n)+$/, '\n' ); // remove empty lines at the end of the file, except the last one
content = content.replace( /^( {4}|\t)/mg, '' ); // one level de-indentation of the executable code
grunt.file.write( executableCode, content );
In addition to the above stated the deleted
event is catched to clean up any orphaned target files too.
} else try {
grunt.file.delete( executableCode );
} catch ( exception ) {
// file does not exist ...
}
});
The final step for Grunt to successfully being able to process the watch task and the respective events is to register the task — for easy invocation purposes as the default.
grunt.registerTask( 'default', [ 'watch' ] );
};
module.exports = -> @initConfig watch : literateCode : files : [ '**/*.haml.md', '**/*.html.md', '**/*.jade.md', # mark up '**/*.less.md', '**/*.rool.md', '**/*.sass.md', '**/*.scss.md', '**/*.stylus.md', # style sheet '**/*.js.md', '**/*.php.md', # script '**/*.ini.md', '**/*.json.md' # configuration ] @loadNpmTasks 'grunt-contrib-watch' @event.on 'watch', ( event, literateCode ) -> executableCode = literateCode.replace /(.+?)\.md$/g, '$1' if event is 'added' or event is 'changed' content = @file.read literateCode content = content.replace /^\S+.*?$/mg, '' # remove documentation lines content = content.replace /[ \t]*#.*?\n/g, '\n' # remove single-line comments content = content.replace /\n+([ \t]*\n){2,}/g, '\n\n' # remove multiple empty lines content = content.replace /^(\n*[ \t]*\n)+/, '' # remove empty lines at the beginning of the file content = content.replace /(\n*[ \t]*\n)+$/, '\n' # remove empty lines at the end of the file, except the last one content = content.replace /^( {4}|\t)/mg, '' # one level de-indentation of the executable code @file.write executableCode, content else try @file.delete executableCode catch exception # file does not exist ... @registerTask 'default', [ 'watch' ]
As with Jeremy I am very excited about this new way of documenting source code. It feels more natural and most importantly it is easy, flexible and fun again! Say goodbye to the rigid organisation and / or project documentation guidelines — sure, a minimal handbook is good to have. Tell your story while you are coding. Enjoy it.
Food for thoutght and happy literate coding!
Stefano F. Rausch — 13 October 2013
P.S. : No Gruntfile.js can exist without a package.json. Have a look.