Skip to content

Instantly share code, notes, and snippets.

@StefanoRausch
Last active December 25, 2015 11:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StefanoRausch/6966235 to your computer and use it in GitHub Desktop.
Save StefanoRausch/6966235 to your computer and use it in GitHub Desktop.
Literate Source Code : LSC

Gruntfile.js for Literate Source Code : LSC

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 :

  1. Not indented lines do author the documentation.
  2. Indented lines do list the executable code to extract.

This approach reflects Markdown's way of denoting code blocks.

Implementation

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' ] );
};

CoffeeScript

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' ]

Final Words

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.

package.json for Literate Source Code : LSC

A typical setup will involve adding two files to your project : package.json and the Gruntfile. Quoting Grunt :

The package.json file is used by npm to store metadata for projects published as npm modules. You will list grunt and the Grunt plugins your project needs as devDependencies.

Implementation

This is a barebone configuration only. Feel free to add any additional dependencies by running the command npm install <module> --save-dev. Alternatively add them manually and npm install them.

{
    "name": "Literate-Source-Code",
    "description": "Write the documentation and add the respective code.",
    "version": "0.1.0",
    "devDependencies": {
        "grunt": "~0.4.1",
        "grunt-contrib-watch": "~0.5.3"
    }
}

Happy literate coding!

Stefano F. Rausch — 13 October 2013

P.S. : No package.json can exist without a Gruntfile. Have a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment