├── .gitignore ├── LICENSE ├── README.md ├── compile.js ├── index.js ├── lit-node ├── package-lock.json ├── package.json ├── register.js └── test ├── problems.md ├── test.md └── the-answer.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 [these people](https://github.com/Rich-Harris/lit-node/graphs/contributors) 2 | 3 | Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions: 4 | 5 | This license, or a link to its text, must be included with all copies of the software and any derivative works. 6 | 7 | Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license. 8 | 9 | The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lit-node 2 | 3 | Load Markdown files as Node modules and run the code blocks. Self-documenting Node scripts through literate programming! Based on [lit](https://github.com/vijithassar/lit) by [Vijith Assar](https://twitter.com/vijithassar). 4 | 5 | ## Overview 6 | 7 | `lit-node` is a lightweight wrapper for [Node.js](https://nodejs.org/en/) which allows you to import code blocks from [Markdown](https://daringfireball.net/projects/markdown/syntax) documents using the `require()` function. This enables first-class support for simple [literate programming](https://en.wikipedia.org/wiki/Literate_programming), a software development technique which emphasizes clear written documentation. It's sort of like [Jupyter](http://jupyter.org/) notebooks for Node. Jupyter... nodebooks? *[todo: should we delete this pun? it's really stupid.]* 8 | 9 | ## Quick Start 10 | 11 | ```bash 12 | # install 13 | $ npm install lit-node 14 | 15 | # load module from command line to enable 16 | # execution of literate Markdown with Node 17 | $ node --require lit-node/register program.md 18 | 19 | # lit-node alias is just node with direct 20 | # support for importing modules from 21 | # Markdown code blocks 22 | $ lit-node program.md 23 | ``` 24 | 25 | ## Instructions 26 | 27 | First, install `lit-node`. 28 | 29 | ```bash 30 | # install 31 | $ npm install lit-node 32 | ``` 33 | 34 | You'll probably want a **global** install if you intend to use either the REPL or the alias for Node which automatically loads the lit-node module (more on these in a moment): 35 | 36 | ```bash 37 | # install 38 | $ npm install --global lit-node 39 | ``` 40 | 41 | Create a Markdown file into which to save your code and its Markdown documentation: 42 | 43 | ```bash 44 | # create a file 45 | $ touch test.md 46 | ``` 47 | 48 | Add some Markdown content to the file, including at least one code block demarcated by triple-backtick ["fenced code blocks"](https://help.github.com/articles/creating-and-highlighting-code-blocks/) as specified by [GitHub-Flavored Markdown](https://github.github.com/gfm/). 49 | 50 | ~~~ 51 | # this is a markdown file! 52 | 53 | It can have *all the usual markdown stuff*, but only the JavaScript code blocks will run: 54 | 55 | ```javascript 56 | // log a message 57 | console.log('hello world'); 58 | ``` 59 | ~~~ 60 | 61 | Now you can **execute your Markdown file**! 62 | 63 | Using the regular Node interpreter: 64 | 65 | ```bash 66 | # execute literate Markdown files with Node, 67 | # loading lit-node module from command line 68 | $ node --require lit-node/register ./test.md 69 | ``` 70 | 71 | Alternately, the same thing using the Node alias that automatically loads the lit-node module: 72 | 73 | ```bash 74 | # execute literate Markdown files with Node alias; 75 | # lit-node module is automatically loaded 76 | $ lit-node ./test.md 77 | ``` 78 | 79 | You *must* include `js` or `javascript` as a language specifier after opening up a fenced code block. Fenced code blocks that specify any other language and fenced code blocks that do not specify a language at all will be ignored. This makes it possible for you to include other code in your Markdown file without that code being executed. This is particularly useful for including Bash installation commands. 80 | 81 | ## Miscellaneous 82 | 83 | ### require() 84 | 85 | Any script that has previously loaded `lit-node` with `require()` can then `require()` other Markdown files, which will be parsed and executed just like any other module. The `.md` file extension is optional, but recommended. 86 | 87 | ```javascript 88 | // load lit-node module 89 | require('lit-node/register.js') 90 | 91 | // scripts can load code from literate Markdown files 92 | const thing = require('thing.md') 93 | console.log(typeof thing) 94 | ``` 95 | 96 | ### REPL 97 | 98 | Assuming you've installed globally as mentioned above, you can also use the Node alias installed by lit-node to launch an interactive [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) which will support Markdown imports. 99 | 100 | ```bash 101 | # launch Node alias REPL with 102 | # lit-node module already loaded 103 | $ lit-node 104 | 105 | # the REPL can load code from literate Markdown files 106 | > const thing = require('./thing.md'); 107 | > typeof thing 108 | ``` 109 | 110 | 111 | ## Other Tools 112 | 113 | - **IRONCLAD MONEY-BACK GUARANTEE**: If you later decide literate programming in Markdown isn't for you, you can quickly and painlessly convert all your Markdown documents into regular JavaScript files by running them through [lit](https://github.com/vijithassar/lit) to strip out the prose. Try it today! There is no risk! 114 | - [lit-web](https://github.com/vijithassar/lit-web) is a script that lets a browser execute the code blocks from a single Markdown document as JavaScript 115 | - To interpret literate code for languages other than JavaScript, you can use either lit [with subshells](https://github.com/vijithassar/lit#logging) or [Blaze](https://github.com/0atman/blaze/), which is a drop-in replacement for `usr/bin/env` 116 | - `lit-node` is just running Node.js internally and for a whole slew of complicated reasons Node.js doesn't yet support ES modules, so for now `lit-node` likewise only supports [CommonJS](http://www.commonjs.org/) exports. To write literate JavaScript source code using ES module syntax, either bundle with [Rollup](https://rollupjs.org/) and the [rollup-plugin-markdown](https://www.npmjs.com/package/rollup-plugin-markdown) plugin, or else process the Markdown files with [lit](https://github.com/vijithassar/lit) and then do whatever else you want with other ES module tools. 117 | - [Docco](http://ashkenas.com/docco/) and its many variants render literate source code into beautiful browsable HTML 118 | 119 | ## License 120 | 121 | [LIL](LICENSE) 122 | -------------------------------------------------------------------------------- /compile.js: -------------------------------------------------------------------------------- 1 | // test whether a fenced code block specifies JavaScript 2 | function isJavaScript(chunk) { 3 | return /^(js|javascript)\s*\n/.test(chunk); 4 | } 5 | 6 | // extract JavaScript code blocks from a Markdown string 7 | function compile(markdown) { 8 | 9 | // split along backtick fences 10 | const chunks = markdown.split(/^```/gm); 11 | 12 | // filter down to only code blocks 13 | const code = chunks 14 | .map((chunk, i) => { 15 | const odd = i % 2; 16 | if (odd && isJavaScript(chunk)) { 17 | return chunk.replace(/^.+/, ''); 18 | } else { 19 | return chunk.replace(/^.+$/gm, ''); 20 | } 21 | }); 22 | 23 | // output an async function 24 | return code.join(''); 25 | 26 | }; 27 | 28 | // export 29 | module.exports = compile; 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./compile.js'); 2 | -------------------------------------------------------------------------------- /lit-node: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const child_process = require('child_process'); 5 | const compile = require('./compile.js'); 6 | 7 | const args = process.argv.slice(2); 8 | 9 | args.unshift('-r', path.resolve(__dirname, 'register.js')); 10 | 11 | const inline = { 12 | '-e': true, 13 | '-p': true, 14 | '--eval': true, 15 | '--print': true 16 | }; 17 | 18 | for (let i = 0; i < args.length; i += 1) { 19 | if (inline[args[i]]) { 20 | args[i + 1] = compile(args[i + 1]); 21 | } 22 | } 23 | 24 | const proc = child_process.spawn(process.argv[0], args, { 25 | stdio: 'inherit' 26 | }); 27 | 28 | proc.on('exit', function(code, signal) { 29 | process.on('exit', function() { 30 | if (signal) { 31 | process.kill(process.pid, signal); 32 | } else { 33 | process.exit(code); 34 | } 35 | }); 36 | }); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-node", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-styles": { 8 | "version": "3.2.0", 9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", 10 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", 11 | "dev": true, 12 | "requires": { 13 | "color-convert": "^1.9.0" 14 | } 15 | }, 16 | "chalk": { 17 | "version": "2.3.0", 18 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", 19 | "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", 20 | "dev": true, 21 | "requires": { 22 | "ansi-styles": "^3.1.0", 23 | "escape-string-regexp": "^1.0.5", 24 | "supports-color": "^4.0.0" 25 | } 26 | }, 27 | "color-convert": { 28 | "version": "1.9.1", 29 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", 30 | "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", 31 | "dev": true, 32 | "requires": { 33 | "color-name": "^1.1.1" 34 | } 35 | }, 36 | "color-name": { 37 | "version": "1.1.3", 38 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 39 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 40 | "dev": true 41 | }, 42 | "escape-string-regexp": { 43 | "version": "1.0.5", 44 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 45 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 46 | "dev": true 47 | }, 48 | "has-flag": { 49 | "version": "2.0.0", 50 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 51 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", 52 | "dev": true 53 | }, 54 | "supports-color": { 55 | "version": "4.5.0", 56 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", 57 | "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", 58 | "dev": true, 59 | "requires": { 60 | "has-flag": "^2.0.0" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lit-node", 3 | "description": "Self-documenting Node scripts through literate programming", 4 | "version": "0.1.1", 5 | "license": "LIL", 6 | "main": "index.js", 7 | "bin": { 8 | "lit-node": "lit-node" 9 | }, 10 | "scripts": { 11 | "test-path": "./lit-node test/test.md", 12 | "test-module": "node --require ./register.js test/test.md", 13 | "test": "npm run test-path && npm run test-module", 14 | "prepublishOnly": "npm run test" 15 | }, 16 | "devDependencies": { 17 | "chalk": "^2.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const compile = require('./compile.js'); 3 | 4 | // register .md extension with Node.js to enable imports 5 | require.extensions['.md'] = function(module, filename) { 6 | const code = compile(fs.readFileSync(filename, 'utf-8')); 7 | return module._compile(code, filename); 8 | }; 9 | -------------------------------------------------------------------------------- /test/problems.md: -------------------------------------------------------------------------------- 1 | I got 2 | 3 | ```js 4 | module.exports = 99; 5 | ``` 6 | 7 | problems but self-documenting code ain't one -------------------------------------------------------------------------------- /test/test.md: -------------------------------------------------------------------------------- 1 | # lit-node tests 2 | 3 | This will test the behaviour of lit-node. 4 | 5 | ## Setup 6 | 7 | First, we'll require a couple of modules: 8 | 9 | ```js 10 | const assert = require('assert'); 11 | const chalk = require('chalk'); 12 | 13 | function success(msg) { 14 | console.log(chalk.green(`✓ ${msg}`)); 15 | } 16 | 17 | function fail(msg) { 18 | console.log(chalk.red(`✗ ${msg}`)); 19 | } 20 | ``` 21 | 22 | ## The actual tests 23 | 24 | Check that `js` code blocks run... 25 | 26 | ```js 27 | success('runs `js` code blocks'); 28 | ``` 29 | 30 | ...as do `javascript` ones... 31 | 32 | ```javascript 33 | success('runs `javascript` code blocks'); 34 | ``` 35 | 36 | ...but that code blocks that specify an unsupported language *don't* run... 37 | 38 | ```bash 39 | fail('should not run this'); 40 | ``` 41 | 42 | ...and neither do those with no language: 43 | 44 | ``` 45 | fail('should not run this'); 46 | ``` 47 | 48 | For debugging, stack traces should point to the correct line: 49 | 50 | ```js 51 | const err = new Error('something went wrong'); 52 | const line = err.stack.split('\n')[1]; 53 | const match = /test.md:(\d+):(\d+)/.exec(line); 54 | 55 | assert.equal(match[1], '51'); // line 56 | assert.equal(match[2], '13'); // column 57 | 58 | success('preserves line/column in stack traces'); 59 | ``` 60 | 61 | What about requiring other `.md` files? 62 | 63 | ```js 64 | const answer = require('./the-answer.md'); 65 | assert.equal(answer, 42); 66 | 67 | success('imports a `.md` file'); 68 | ``` 69 | 70 | Even if the extension isn't specified? 71 | 72 | ```js 73 | const problems = require('./problems'); 74 | assert.equal(problems, 99); 75 | 76 | success('imports a `.md` file without specifying the extension'); 77 | ``` 78 | 79 | 80 | ## Did it work? 81 | 82 | If everything worked, we'll print a message to say as much: 83 | 84 | ```js 85 | console.log(chalk.bold('🎉 everything worked! 🎉\n')); 86 | ``` 87 | -------------------------------------------------------------------------------- /test/the-answer.md: -------------------------------------------------------------------------------- 1 | "Good Morning," said Deep Thought at last. 2 | 3 | "Er..good morning, O Deep Thought" said Loonquawl nervously, "do you have...er, that is..." 4 | 5 | "An Answer for you?" interrupted Deep Thought majestically. "Yes, I have." 6 | 7 | The two men shivered with expectancy. Their waiting had not been in vain. 8 | 9 | "There really is one?" breathed Phouchg. 10 | 11 | "There really is one," confirmed Deep Thought. 12 | 13 | "To Everything? To the great Question of Life, the Universe and everything?" 14 | 15 | "Yes." 16 | 17 | Both of the men had been trained for this moment, their lives had been a preparation for it, they had been selected at birth as those who would witness the answer, but even so they found themselves gasping and squirming like excited children. 18 | 19 | "And you're ready to give it to us?" urged Loonsuawl. 20 | 21 | "I am." 22 | 23 | "Now?" 24 | 25 | "Now," said Deep Thought. 26 | 27 | They both licked their dry lips. 28 | 29 | "Though I don't think," added Deep Thought. "that you're going to like it." 30 | 31 | "Doesn't matter!" said Phouchg. "We must know it! Now!" 32 | 33 | "Now?" inquired Deep Thought. 34 | 35 | "Yes! Now..." 36 | 37 | "All right," said the computer, and settled into silence again. The two men fidgeted. The tension was unbearable. 38 | 39 | "You're really not going to like it," observed Deep Thought. 40 | 41 | "Tell us!" 42 | 43 | "All right," said Deep Thought. "The Answer to the Great Question..." 44 | 45 | "Yes..!" 46 | 47 | "Of Life, the Universe and Everything..." said Deep Thought. 48 | 49 | "Yes...!" 50 | 51 | "Is..." said Deep Thought, and paused. 52 | 53 | "Yes...!" 54 | 55 | "Is..." 56 | 57 | "Yes...!!!...?" 58 | 59 | ```js 60 | module.exports = 42; 61 | ``` 62 | 63 | , said Deep Thought, with infinite majesty and calm.” --------------------------------------------------------------------------------