├── .husky ├── pre-commit └── commit-msg ├── .prettierignore ├── test ├── examples │ ├── foo.js │ ├── basic.md │ ├── module.md │ ├── es6.md │ └── contexts.md └── test.spec.js ├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .github └── workflows │ ├── ci.yml │ └── release-please.yml ├── package.json ├── cli.js ├── CHANGELOG.md ├── src ├── logo.svg └── README_js.md ├── README.md └── index.js /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/examples/foo.js: -------------------------------------------------------------------------------- 1 | // Plain module 2 | exports.foo = 123; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.sw* 3 | .DS_Store 4 | node_modules 5 | npm-debug.* 6 | npm-debug.log 7 | .env 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | package-lock.json 3 | node_modules 4 | .github/ 5 | .husky/ 6 | test/ 7 | src/ 8 | *.md -------------------------------------------------------------------------------- /test/examples/basic.md: -------------------------------------------------------------------------------- 1 | # Header 2 | 3 | ```javascript --run 4 | const res = 1 + 1; // RESULT 5 | ``` 6 | 7 | # Footer 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /test/examples/module.md: -------------------------------------------------------------------------------- 1 | ```javascript --hide 2 | runmd.onRequire = (path) => (path == 'bar' ? './foo' : path); 3 | ``` 4 | 5 | ```javascript --run 6 | const aModule = require('bar'); 7 | const res = aModule.foo; // RESULT 8 | ``` 9 | -------------------------------------------------------------------------------- /test/examples/es6.md: -------------------------------------------------------------------------------- 1 | ```javascript --hide 2 | runmd.onRequire = (path) => (path == 'bar' ? './foo' : path); 3 | ``` 4 | 5 | ```javascript --run 6 | import aModule from 'bar'; 7 | import { foo } from 'bar'; 8 | import { foo as bar } from 'bar'; 9 | const fooRes = foo; // RESULT 10 | const barRes = bar; // RESULT 11 | ``` 12 | -------------------------------------------------------------------------------- /test/examples/contexts.md: -------------------------------------------------------------------------------- 1 | ```javascript --run 2 | const foo = 'def'; 3 | ``` 4 | 5 | ```javascript --run alpha 6 | const foo = 'alpha'; 7 | ``` 8 | 9 | ```javascript --run beta 10 | const foo = 'beta'; 11 | ``` 12 | 13 | ```javascript --run 14 | const def = typeof foo; // RESULT 15 | ``` 16 | 17 | ```javascript --run alpha 18 | const alpha = foo; // RESULT 19 | ``` 20 | 21 | ```javascript --run beta 22 | const beta = foo; // RESULT 23 | ``` 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | name: CI Tests 9 | 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x, 20.x, 22.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { render } = require('..'); 4 | const test = require('ava'); 5 | 6 | function transform(filename) { 7 | const filepath = path.join(__dirname, 'examples', filename); 8 | const inputText = fs.readFileSync(filepath, 'utf8'); 9 | return render(inputText, { 10 | lame: true, 11 | inputName: `test/examples/${path.basename(__filename)}` 12 | }); 13 | } 14 | 15 | test('basic', (t) => { 16 | const md = transform('basic.md'); 17 | t.regex(md, /^# Header/m, 'has header'); 18 | t.regex(md, /res =.*2$/m, 'has result'); 19 | t.regex(md, /^# Footer/m, 'has footer'); 20 | }); 21 | 22 | test('module', (t) => { 23 | const md = transform('module.md'); 24 | t.regex(md, /res =.*123$/m, 'has access to module exports'); 25 | }); 26 | 27 | test('es6', (t) => { 28 | const md = transform('es6.md'); 29 | t.regex(md, /fooRes =.*123$/m, 'has access to module exports'); 30 | t.regex(md, /barRes =.*123$/m, 'has access to module exports'); 31 | }); 32 | 33 | test('contexts', (t) => { 34 | const md = transform('contexts.md'); 35 | t.regex(md, /def =.*undefined/m, 'foo in unnamed context is not undefined'); 36 | t.regex(md, /alpha =.*alpha/m, 'foo in alpha context'); 37 | t.regex(md, /beta =.*beta/m, 'foo in beta context'); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | # Relevent docs: 2 | # - https://github.com/googleapis/release-please 3 | # - https://github.com/googleapis/release-please-action 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | push: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | 16 | name: release-please 17 | 18 | jobs: 19 | release-please: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: googleapis/release-please-action@v4 23 | id: release 24 | with: 25 | token: ${{ secrets.RUNMD_RELEASE_PLEASE_TOKEN }} 26 | release-type: node 27 | 28 | # Steps below handle publication to NPM 29 | 30 | - uses: actions/checkout@v4 31 | if: ${{ steps.release.outputs.release_created }} 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | registry-url: 'https://registry.npmjs.org' 36 | if: ${{ steps.release.outputs.release_created }} 37 | 38 | - run: npm ci 39 | if: ${{ steps.release.outputs.release_created }} 40 | 41 | - run: npm test 42 | if: ${{ steps.release.outputs.release_created }} 43 | 44 | - run: npm publish 45 | env: 46 | NODE_AUTH_TOKEN: ${{secrets.RUNMD_NPM_RELEASE_TOKEN}} 47 | if: ${{ steps.release.outputs.release_created }} 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runmd", 3 | "author": { 4 | "name": "Robert Kieffer", 5 | "email": "robert@broofa.com" 6 | }, 7 | "version": "1.4.1", 8 | "bin": { 9 | "runmd": "./cli.js" 10 | }, 11 | "main": "./index.js", 12 | "description": "Runnable README files", 13 | "keywords": [ 14 | "markdown", 15 | "readme" 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://github.com/broofa/runmd", 19 | "ava": { 20 | "files": [ 21 | "test/*.spec.js" 22 | ] 23 | }, 24 | "commitlint": { 25 | "extends": [ 26 | "@commitlint/config-conventional" 27 | ] 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/broofa/runmd.git" 32 | }, 33 | "dependencies": { 34 | "minimist": "1.2.8", 35 | "require-like": "0.1.2" 36 | }, 37 | "peerDependencies": { 38 | "prettier": "^3.4.2" 39 | }, 40 | "devDependencies": { 41 | "@commitlint/config-conventional": "19.6.0", 42 | "ava": "6.2.0", 43 | "commitlint": "19.6.1", 44 | "husky": "9.1.7", 45 | "prettier": "3.4.2", 46 | "npm-run-all": "4.1.5" 47 | }, 48 | "scripts": { 49 | "prepare": "husky", 50 | "docs": "./cli.js --output README.md src/README_js.md", 51 | "docs:test": "npm run docs && git diff --quiet README.md", 52 | "lint:fix": "prettier --write .", 53 | "lint:test": "prettier --check .", 54 | "release": "echo \"This project uses release-please for releases\"", 55 | "test:unit": "ava", 56 | "test": "npm run docs:test && npm run lint:test && npm run test:unit" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const minimist = require('minimist'); 6 | const { render } = require('.'); 7 | 8 | const argv = minimist(process.argv.slice(2)); 9 | 10 | function usage() { 11 | console.log( 12 | 'Usage: runmd input_file [--lame] [--watch] [--output=output_file]' 13 | ); 14 | process.exit(1); 15 | } 16 | 17 | const inputName = argv._[0]; 18 | const outputName = argv.output; 19 | 20 | // Check usage 21 | if (!argv._.length) usage(); 22 | 23 | // Full paths (input and output) 24 | const inputPath = path.resolve(process.cwd(), inputName); 25 | let outputPath; 26 | if (outputName) { 27 | if (!/\.md$/.test(outputName)) { 28 | throw new Error(`Output file ${outputName} must have .md extension`); 29 | } 30 | 31 | outputPath = outputName && path.resolve(process.cwd(), outputName); 32 | } 33 | 34 | /** 35 | * Render input file to output. (args passed by fs.watchFile) 36 | */ 37 | async function run(curr, prev) { 38 | // Do nothing if file not modified 39 | if (curr && prev && curr.mtime === prev.mtime) return; 40 | 41 | let markdown; 42 | try { 43 | // Read input file 44 | const inputText = fs.readFileSync(inputPath, 'utf8'); 45 | 46 | // Render it 47 | markdown = render(inputText, { 48 | inputName, 49 | outputName, 50 | lame: argv.lame 51 | }); 52 | 53 | // Format using prettier (if available) 54 | try { 55 | const prettier = require('prettier'); 56 | const config = await prettier.resolveConfig(outputPath); 57 | markdown = await prettier.format(markdown, { 58 | parser: 'markdown', 59 | ...config 60 | }); 61 | } catch (err) { 62 | console.log(`Formatting skipped (${err.message})`); 63 | } 64 | 65 | // Write to output (file or stdout) 66 | if (outputPath) { 67 | fs.writeFileSync(outputPath, markdown); 68 | console.log('Rendered', argv.output); 69 | } else { 70 | process.stdout.write(markdown); 71 | process.stdout.write('\n'); 72 | } 73 | } catch (err) { 74 | if (!argv.watch) throw err; 75 | console.error(err); 76 | } 77 | } 78 | 79 | run().catch(console.error); 80 | 81 | // If --watch, rerender when file changes 82 | if (argv.watch) fs.watchFile(inputPath, run); 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.4.1](https://github.com/broofa/runmd/compare/v1.4.0...v1.4.1) (2024-12-24) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * prettier dependencies ([48d7667](https://github.com/broofa/runmd/commit/48d7667355ca3d7c086a6a1d5b5d4ddddb50bd18)) 11 | * prettier dependencies ([#34](https://github.com/broofa/runmd/issues/34)) ([b0d8b10](https://github.com/broofa/runmd/commit/b0d8b1081a8db9723915e39c2eea286c53ecec07)) 12 | 13 | ## [1.4.0](https://github.com/broofa/runmd/compare/v1.3.9...v1.4.0) (2024-12-24) 14 | 15 | 16 | ### Features 17 | 18 | * format with prettier ([#31](https://github.com/broofa/runmd/issues/31)) ([2adf164](https://github.com/broofa/runmd/commit/2adf164273dc31dba2ca67ad28da312913524d27)) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * format with prettier ([bec8824](https://github.com/broofa/runmd/commit/bec88242140023d8613ba80c554d12b3eccb4fd9)) 24 | * tweak logo size ([c464f21](https://github.com/broofa/runmd/commit/c464f21de636ad0df9480fdd3eba02492565b0c6)) 25 | 26 | ## [1.3.9](https://github.com/broofa/runmd/compare/v1.3.8...v1.3.9) (2023-03-13) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * testing release-please ([8866b15](https://github.com/broofa/runmd/commit/8866b1553c59522c1dae8959699fc7ebd11004c1)) 32 | * testing release-please ([00a9937](https://github.com/broofa/runmd/commit/00a9937cc571961a7a88dec4714a4f3f0f33e905)) 33 | * testing release-please ([5b5a5b4](https://github.com/broofa/runmd/commit/5b5a5b462bcb0cd898f1a78efb647b49d0d351cb)) 34 | * testing release-please ([fd2fb03](https://github.com/broofa/runmd/commit/fd2fb0390fac5e5056b713b1c551d970cefe5af2)) 35 | 36 | 37 | ### Miscellaneous Chores 38 | 39 | * testing release-please ([973f2c6](https://github.com/broofa/runmd/commit/973f2c694a2d86ab2d1a603f112f59c4a170974f)) 40 | 41 | ### [1.3.8](https://github.com/broofa/runmd/compare/v1.3.6...v1.3.8) (2023-03-12) 42 | 43 | ### [1.3.6](https://github.com/broofa/runmd/compare/v1.3.5...v1.3.6) (2022-05-19) 44 | 45 | Version created during testing of `release-please`. 46 | 47 | ### [1.3.5](https://github.com/broofa/runmd/compare/v1.3.4...v1.3.5) (2022-05-19) 48 | 49 | Version created during testing of `release-please`. 50 | 51 | ### [1.3.4](https://github.com/broofa/runmd/compare/v1.3.3...v1.3.4) (2022-05-18) 52 | 53 | ### Bug Fixes 54 | 55 | * use https for logo link ([0526806](https://github.com/broofa/runmd/commit/0526806a05fcbb63efff24d9bf80699cf8bed3da)) 56 | 57 | ### [1.3.3](https://github.com/broofa/runmd/compare/v1.3.2...v1.3.3) (2022-05-18) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | Version created during testing of `release-please`. 63 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | RunMD 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/README_js.md: -------------------------------------------------------------------------------- 1 | # ![RunMD Logo](http://i.imgur.com/cJKo6bU.png) ![example workflow](https://github.com/broofa/runmd/actions/workflows/ci.yml/badge.svg) 2 | 3 | Run code blocks in your markdown and annotate them with the output. 4 | 5 | Creating README files is a pain, especially when it comes to writing code 6 | samples. Code gets out of date, authors get sloppy, details get omitted, etc. 7 | RunMD takes the pain out of this process. 8 | 9 | With RunMD, your readers can trust your code blocks are runnable and that code 10 | output will be as-claimed. 11 | 12 | ## Install 13 | 14 | ```shell 15 | npm install runmd 16 | ``` 17 | 18 | ## Usage 19 | 20 | `runmd [options] input_file` 21 | 22 | Where `options` may be zero or more of: 23 | 24 | - `--output=output_file` file to write specify an output file 25 | - `--watch` Watch `input_file` for changes and rerender 26 | - `--lame` Suppress attribution footer 27 | 28 | For example, to port an existing README.md file: 29 | 30 | cp README.md README_js.md 31 | 32 | Edit README_js.md to add Markdown Options (below) to your ````javascript` 33 | blocks, then ... 34 | 35 | runmd README_js.md --output README.md 36 | 37 | ## Limitations 38 | 39 | RunMD scripts are run using [Node.js' `vm` module](https://nodejs.org/api/vm.html). 40 | This environment is limited in "interesting" ways, and RunMD runs fast and loose with some APIs. Specifically: 41 | 42 | - `console.log()` works, but no other `console` methods are supported at this 43 | time 44 | - `setTimeout()` works, but all timers fire immediately at the end of script 45 | execution. `clearTimeout`, `setInterval`, and `clearInterval` are not 46 | supported 47 | 48 | [Note: PRs fleshing out these and other missing APIs would be "well received"] 49 | 50 | ### ES6 Imports 51 | 52 | **Some** ES6 import incantations will work, however this feature should be 53 | considered very experimental at this point. Read the source [for 54 | details](https://github.com/broofa/runmd/blob/master/index.js#L229-L246). 55 | 56 | ## NPM Integration 57 | 58 | To avoid publishing when compilation of your README file fails: 59 | 60 | "scripts": { 61 | "prepare": "runmd README_js.md --output README.md" 62 | } 63 | 64 | ## Markdown API 65 | 66 | ### --run 67 | 68 | Runs the script, appending any console.log output. E.g.: 69 | 70 | ```javascript --run 71 | console.log('Hello, World!'); 72 | ``` 73 | 74 | ... becomes: 75 | 76 | ```javascript 77 | console.log('Hello, World!'); 78 | 79 | ⇒ Hello, World! 80 | ```` 81 | 82 | `--run` may be omitted if other options are present. 83 | 84 | ### --run [context_name] 85 | 86 | If a `context_name` is provided, all blocks with that name will share the same 87 | runtime context. E.g. 88 | 89 | ```javascript --run sample 90 | let text = 'World'; 91 | ``` 92 | 93 | Continuing on ... 94 | 95 | ```javascript --run sample 96 | console.log(text); 97 | ``` 98 | 99 | ... becomes: 100 | 101 | ```javascript 102 | let text = 'Hello'; 103 | ``` 104 | 105 | Continuing on ... 106 | 107 | ```javascript 108 | console.log(text); 109 | 110 | ⇒ Hello 111 | ``` 112 | 113 | ... but trying to reference `text` in a new context or other named context will 114 | fail: 115 | 116 | ```javascript --run 117 | console.log(text); 118 | ``` 119 | 120 | (Results in `ReferenceError: text is not defined`.) 121 | 122 | ### --hide 123 | 124 | Run the script, but do not render the script source or output. This is useful 125 | for setting up context that's necessary for code, but not germane to 126 | documentation. 127 | 128 | Welcome! 129 | 130 | ```javascript --run foo --hide 131 | // Setup/utility code or whatever ... 132 | function hello() { 133 | console.log('Hello, World!'); 134 | } 135 | ``` 136 | 137 | Here is a code snippet: 138 | 139 | ```javascript --run foo 140 | hello(); 141 | ``` 142 | 143 | ... becomes: 144 | 145 | Welcome! 146 | 147 | Here's a code snippet: 148 | 149 | ```javascript 150 | hello(); 151 | 152 | ⇒ Hello, World! 153 | ``` 154 | 155 | ### "// RESULT" 156 | 157 | Inline values **_for single line expressions_** may be displayed by appending 158 | "// RESULT" to the end of a line. Note: RunMD will error if the line is not a 159 | self-contained, evaluate-able, expression. 160 | 161 | ```javascript --run 162 | ['Hello', ' World!'].join(','); // RESULT 163 | ``` 164 | 165 | ... becomes: 166 | 167 | ```javascript 168 | ['Hello', ' World!'].join(','); // ⇨ 'Hello, World!' 169 | ``` 170 | 171 | ## runmd Object 172 | 173 | A global `runmd` object is provided all contexts, and supports the following: 174 | 175 | ### runmd.onRequire 176 | 177 | The `onRequire` event gives pages the opportunity to transform module require 178 | paths. This is useful if the module context in which you render markdown is 179 | different from what your readers will typically encounter. (Often the case with 180 | npm-published modules). 181 | 182 | ```javascript --hide 183 | // Remap `require('uuid/*') to `require('./*') 184 | runmd.onRequire = function(path) { 185 | return path.replace(/^uuid\//, './'); 186 | } 187 | ``` 188 | 189 | ### runmd.onOutputLine 190 | 191 | The `onOutputLine` event gives pages the opportunity to transform markdown output. 192 | 193 | ```javascript --hide 194 | runmd.onOutputLine = function(line, isRunning) { 195 | return !isRunning ? line.toUpperCase() : line); 196 | } 197 | ``` 198 | 199 | The `isRunning` argument will be `true` for any lines that are interpreted as 200 | code by this module. Transformations do not affect interpreted source, only how 201 | source is rendered. 202 | 203 | Return `null` to omit the line from the rendered output. 204 | 205 | # Recommended workflow: RunMD + Chrome + Markdown Preview Plus 206 | 207 | There's more than one way to visualize changes to Markdown files as you edit 208 | them, but the following works pretty well for me: 209 | 210 | - Install [the Markdown Preview Plus](https://goo.gl/iDhAL) Chrome 211 | - ... Allow it to access file URLs" (in chrome://extensions tab) 212 | - ... Set Reload Frequency to "1 second" (in the extension options) 213 | - Launch `runmd` with the `--watch` option to have it continuously re-render your output file as you make changes 214 | - Open the output file in Chrome, and it will update in realtime as you make changes to your runmd input file(s) 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # ![RunMD Logo](http://i.imgur.com/cJKo6bU.png) ![example workflow](https://github.com/broofa/runmd/actions/workflows/ci.yml/badge.svg) 6 | 7 | Run code blocks in your markdown and annotate them with the output. 8 | 9 | Creating README files is a pain, especially when it comes to writing code 10 | samples. Code gets out of date, authors get sloppy, details get omitted, etc. 11 | RunMD takes the pain out of this process. 12 | 13 | With RunMD, your readers can trust your code blocks are runnable and that code 14 | output will be as-claimed. 15 | 16 | ## Install 17 | 18 | ```shell 19 | npm install runmd 20 | ``` 21 | 22 | ## Usage 23 | 24 | `runmd [options] input_file` 25 | 26 | Where `options` may be zero or more of: 27 | 28 | - `--output=output_file` file to write specify an output file 29 | - `--watch` Watch `input_file` for changes and rerender 30 | - `--lame` Suppress attribution footer 31 | 32 | For example, to port an existing README.md file: 33 | 34 | cp README.md README_js.md 35 | 36 | Edit README_js.md to add Markdown Options (below) to your ````javascript` 37 | blocks, then ... 38 | 39 | runmd README_js.md --output README.md 40 | 41 | ## Limitations 42 | 43 | RunMD scripts are run using [Node.js' `vm` module](https://nodejs.org/api/vm.html). 44 | This environment is limited in "interesting" ways, and RunMD runs fast and loose with some APIs. Specifically: 45 | 46 | - `console.log()` works, but no other `console` methods are supported at this 47 | time 48 | - `setTimeout()` works, but all timers fire immediately at the end of script 49 | execution. `clearTimeout`, `setInterval`, and `clearInterval` are not 50 | supported 51 | 52 | [Note: PRs fleshing out these and other missing APIs would be "well received"] 53 | 54 | ### ES6 Imports 55 | 56 | **Some** ES6 import incantations will work, however this feature should be 57 | considered very experimental at this point. Read the source [for 58 | details](https://github.com/broofa/runmd/blob/master/index.js#L229-L246). 59 | 60 | ## NPM Integration 61 | 62 | To avoid publishing when compilation of your README file fails: 63 | 64 | "scripts": { 65 | "prepare": "runmd README_js.md --output README.md" 66 | } 67 | 68 | ## Markdown API 69 | 70 | ### --run 71 | 72 | Runs the script, appending any console.log output. E.g.: 73 | 74 | ```javascript --run 75 | console.log('Hello, World!'); 76 | ``` 77 | 78 | ... becomes: 79 | 80 | ```javascript 81 | console.log('Hello, World!'); 82 | 83 | ⇒ Hello, World! 84 | ```` 85 | 86 | `--run` may be omitted if other options are present. 87 | 88 | ### --run [context_name] 89 | 90 | If a `context_name` is provided, all blocks with that name will share the same 91 | runtime context. E.g. 92 | 93 | ```javascript --run sample 94 | let text = 'World'; 95 | ``` 96 | 97 | Continuing on ... 98 | 99 | ```javascript --run sample 100 | console.log(text); 101 | ``` 102 | 103 | ... becomes: 104 | 105 | ```javascript 106 | let text = 'Hello'; 107 | ``` 108 | 109 | Continuing on ... 110 | 111 | ```javascript 112 | console.log(text); 113 | 114 | ⇒ Hello 115 | ``` 116 | 117 | ... but trying to reference `text` in a new context or other named context will 118 | fail: 119 | 120 | ```javascript --run 121 | console.log(text); 122 | ``` 123 | 124 | (Results in `ReferenceError: text is not defined`.) 125 | 126 | ### --hide 127 | 128 | Run the script, but do not render the script source or output. This is useful 129 | for setting up context that's necessary for code, but not germane to 130 | documentation. 131 | 132 | Welcome! 133 | 134 | ```javascript --run foo --hide 135 | // Setup/utility code or whatever ... 136 | function hello() { 137 | console.log('Hello, World!'); 138 | } 139 | ``` 140 | 141 | Here is a code snippet: 142 | 143 | ```javascript --run foo 144 | hello(); 145 | ``` 146 | 147 | ... becomes: 148 | 149 | Welcome! 150 | 151 | Here's a code snippet: 152 | 153 | ```javascript 154 | hello(); 155 | 156 | ⇒ Hello, World! 157 | ``` 158 | 159 | ### "// RESULT" 160 | 161 | Inline values **_for single line expressions_** may be displayed by appending 162 | "// RESULT" to the end of a line. Note: RunMD will error if the line is not a 163 | self-contained, evaluate-able, expression. 164 | 165 | ```javascript --run 166 | ['Hello', ' World!'].join(','); // RESULT 167 | ``` 168 | 169 | ... becomes: 170 | 171 | ```javascript 172 | ['Hello', ' World!'].join(','); // ⇨ 'Hello, World!' 173 | ``` 174 | 175 | ## runmd Object 176 | 177 | A global `runmd` object is provided all contexts, and supports the following: 178 | 179 | ### runmd.onRequire 180 | 181 | The `onRequire` event gives pages the opportunity to transform module require 182 | paths. This is useful if the module context in which you render markdown is 183 | different from what your readers will typically encounter. (Often the case with 184 | npm-published modules). 185 | 186 | ```javascript --hide 187 | // Remap `require('uuid/*') to `require('./*') 188 | runmd.onRequire = function(path) { 189 | return path.replace(/^uuid\//, './'); 190 | } 191 | ``` 192 | 193 | ### runmd.onOutputLine 194 | 195 | The `onOutputLine` event gives pages the opportunity to transform markdown output. 196 | 197 | ```javascript --hide 198 | runmd.onOutputLine = function(line, isRunning) { 199 | return !isRunning ? line.toUpperCase() : line); 200 | } 201 | ``` 202 | 203 | The `isRunning` argument will be `true` for any lines that are interpreted as 204 | code by this module. Transformations do not affect interpreted source, only how 205 | source is rendered. 206 | 207 | Return `null` to omit the line from the rendered output. 208 | 209 | # Recommended workflow: RunMD + Chrome + Markdown Preview Plus 210 | 211 | There's more than one way to visualize changes to Markdown files as you edit 212 | them, but the following works pretty well for me: 213 | 214 | - Install [the Markdown Preview Plus](https://goo.gl/iDhAL) Chrome 215 | - ... Allow it to access file URLs" (in chrome://extensions tab) 216 | - ... Set Reload Frequency to "1 second" (in the extension options) 217 | - Launch `runmd` with the `--watch` option to have it continuously re-render your output file as you make changes 218 | - Open the output file in Chrome, and it will update in realtime as you make changes to your runmd input file(s) 219 | 220 | --- 221 | 222 | Markdown generated from [src/README_js.md](src/README_js.md) by 223 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const util = require('util'); 3 | const vm = require('vm'); 4 | const minimist = require('minimist'); 5 | const requireLike = require('require-like'); 6 | 7 | const RESULT_RE = /\/\/\s*RESULT\s*$/; 8 | const LARGE_RESULT_LINES = 2; // Number of lines that constitute a "large" result 9 | 10 | const LINK_MARKDOWN = ``; 11 | 12 | /** 13 | * Represents a line of source showing an evaluated expression 14 | */ 15 | class ResultLine { 16 | constructor(id, line) { 17 | this.id = id; 18 | this.line = line; 19 | this._result = util.inspect(undefined); 20 | } 21 | 22 | /** Source w/out RESULT comment e */ 23 | get bare() { 24 | return this.line.replace(RESULT_RE, ''); 25 | } 26 | 27 | /** Source w/out indenting */ 28 | get undent() { 29 | return this.line.replace(/^\S.*/, ''); 30 | } 31 | 32 | /** String needed to line up with RESULT comment */ 33 | get prefix() { 34 | return this.line.replace(RESULT_RE, '').replace(/./g, ' ') + '// '; 35 | } 36 | 37 | /** Executable source needed to produce result */ 38 | get scriptLine() { 39 | // Trim string (including trailing ';') 40 | const trimmed = this.line 41 | .replace(RESULT_RE, '') 42 | .replace(/^\s+|[\s;]+$/g, ''); 43 | 44 | // You can't wrap an expression in ()'s if it also has a var declaration, so 45 | // we do a bit of regex hacking to wrap just the expression part, here 46 | /(^\s*(?:const|let|var)[\w\s,]*=\s)*(.*)/.test(trimmed); 47 | 48 | // Script line with code needed to set the result in the context-global 49 | // `__.results` object 50 | return `${RegExp.$1} __.results[${this.id}].result = (${RegExp.$2});`; 51 | } 52 | 53 | /** Set result of evaluating line's expression */ 54 | set result(val) { 55 | const MAX_LEN = 130; // Github horizontal scrollbars appear at ~140 chars 56 | 57 | this._result = util.inspect(val, { 58 | depth: null, 59 | breakLength: Math.max(40, MAX_LEN - this.prefix.length) 60 | }); 61 | 62 | // If result spans multiple lines, move it to the next line 63 | if (this._result.split('\n').length >= LARGE_RESULT_LINES) { 64 | this._result = util.inspect(val, { 65 | depth: null, 66 | breakLength: MAX_LEN - this.undent.length 67 | }); 68 | } 69 | } 70 | 71 | /** Fully rendered line output */ 72 | toString() { 73 | let lines = this._result.split('\n'); 74 | let prefix = this.prefix; 75 | 76 | if (lines.length >= LARGE_RESULT_LINES) { 77 | lines.unshift(''); 78 | prefix = this.undent + ' // '; 79 | } 80 | 81 | lines = lines.map((line, i) => 82 | i === 0 ? '// \u21e8 ' + line : prefix + line 83 | ); 84 | return this.bare + lines.join('\n'); 85 | } 86 | } 87 | 88 | /** 89 | * Create a VM context factory. Creates a function that creates / returns VM 90 | * contexts. 91 | */ 92 | function _createCache(runmd, inputName, write) { 93 | const contexts = new Map(); 94 | 95 | /** 96 | * Get a [named] VM execution context 97 | * 98 | * @param {String} [name] of context to reuse / create 99 | */ 100 | return function (name) { 101 | if (name && contexts.has(name)) return contexts.get(name); 102 | 103 | // require() function that operates relative to input file path, ignore 104 | // existing require() cache 105 | if (!inputName) inputName = path.join(process.cwd(), '(stdin)'); 106 | const contextRequire = requireLike(inputName, true); 107 | 108 | // Create console shim that renders to our output file 109 | const log = function (...args) { 110 | // Stringify args with util.inspect 111 | args = args.map((arg) => 112 | typeof arg === 'string' ? arg : util.inspect(arg, { depth: null }) 113 | ); 114 | const lines = args 115 | .join(' ') 116 | .split('\n') 117 | .map((line) => '\u21d2 ' + line); 118 | 119 | write(...lines); 120 | }; 121 | const consoleShim = { 122 | log, 123 | warn: log, 124 | error: log 125 | }; 126 | 127 | // Crude setTimeout shim. Callbacks are invoked immediately after script 128 | // block(s) run 129 | const _timers = []; 130 | let _time = Date.now(); 131 | 132 | const timeoutShim = function (callback, delay) { 133 | _timers.push({ callback, time: _time + delay }); 134 | _timers.sort((a, b) => (a.time < b.time ? -1 : a.time > b.time ? 1 : 0)); 135 | }; 136 | 137 | timeoutShim.flush = function () { 138 | let timer; 139 | while ((timer = _timers.shift())) { 140 | _time = timer.time; 141 | timer.callback(); 142 | } 143 | }; 144 | 145 | // Create VM context 146 | const context = vm.createContext({ 147 | console: consoleShim, 148 | 149 | __: { results: [] }, 150 | 151 | process, 152 | 153 | require: (ref) => { 154 | if (runmd.onRequire) ref = runmd.onRequire(ref); 155 | return contextRequire(ref); 156 | }, 157 | 158 | runmd, 159 | 160 | setTimeout: timeoutShim 161 | }); 162 | 163 | // Named contexts get cached 164 | if (name) contexts.set(name, context); 165 | 166 | return context; 167 | }; 168 | } 169 | 170 | /** 171 | * Render RunMD-compatible markdown file 172 | * 173 | * @param {String} text to transform 174 | * @param {Object} [options] 175 | * @param {String} [options.inputName] name of input file 176 | * @param {String} [options.outputName] name of output file 177 | * @param {Boolean} [options.lame] if true, disables RunMD footer 178 | */ 179 | function render(inputText, options = {}) { 180 | const { inputName, outputName, lame } = options; 181 | 182 | // Warn people against editing the output file (only helps about 50% of the 183 | // time but, hey, we tried!) 184 | const outputLines = inputName 185 | ? [ 186 | '', 189 | '' 190 | ] 191 | : []; 192 | 193 | const scriptLines = []; 194 | let lineOffset = 0; 195 | let runArgs; 196 | let runContext = {}; 197 | let hide = false; 198 | 199 | // Read input file, split into lines 200 | const lines = inputText.split('\n'); 201 | 202 | // Add line(s) to output (when --hide flag not active) 203 | function _write(...lines) { 204 | if (hide) return; 205 | outputLines.push(...lines.filter((line) => line != null)); 206 | } 207 | 208 | // State available to all contexts as `runmd` 209 | const runmd = { 210 | // Hook for transforming output lines 211 | onOutputLine: null, 212 | 213 | // Hook for transforming require()'s 214 | onRequire: null, 215 | 216 | // Make Date available for duck-patching 217 | Date 218 | }; 219 | 220 | // Clear VM context cache 221 | const getContext = _createCache(runmd, inputName, _write); 222 | 223 | // Process each line of input 224 | lines.forEach((line, lineNo) => { 225 | if (!runArgs) { 226 | // Look for start of script block with '--(arg)' arguments 227 | runArgs = /^```javascript\s+(--.*)?/i.test(line) && RegExp.$1; 228 | if (runArgs) { 229 | runArgs = minimist(runArgs.split(/\s+/)); 230 | 231 | // Get runContext for this script block 232 | runContext = getContext(runArgs.run === true ? '' : runArgs.run); 233 | 234 | hide = !!runArgs.hide; 235 | lineOffset = lineNo + 1; 236 | line = line.replace(/\s.*/, ''); 237 | } 238 | } else if (runArgs && /^```/.test(line)) { 239 | // End of script block. Process the script lines we gathered 240 | 241 | let script = scriptLines.join('\n'); 242 | 243 | // Limited support for ES6-style imports. Transforms some "import" 244 | // patterns into CommonJS require()'s via regex. 245 | script = script 246 | // "import { X as Y } from 'Z'" 247 | .replace( 248 | /import\s*\{\s*([^\s]+)\s+as\s+([^\s]+)\s*\}\s*from\s*['"]([^']+)['"]/g, 249 | 'const { $1: $2 } = require("$3")' 250 | ) 251 | // "import { X } from 'Z'" 252 | .replace( 253 | /import\s*\{\s*([^\s]+)\s*\}\s*from\s*['"]([^']+)['"]/g, 254 | 'const { $1 } = require("$2")' 255 | ) 256 | // ES6: "import X from 'Z'" 257 | .replace( 258 | /import\s([^\s]+)\sfrom\s*['"]([^']+)['"]/g, 259 | 'const $1 = require("$2")' 260 | ); 261 | 262 | // Clear out script lines 263 | scriptLines.length = 0; 264 | 265 | // Replace setTimeout globally 266 | // (is this necessary since we define it in each context?!? I've 267 | // forgotten :-p ) 268 | const _timeout = setTimeout; 269 | setTimeout = runContext.setTimeout; 270 | 271 | // Run the script (W00t!) 272 | try { 273 | vm.runInContext(script, runContext, { 274 | lineOffset, 275 | filename: inputName || '(input string)' 276 | }); 277 | } finally { 278 | // Flush pending timers 279 | runContext.setTimeout.flush(); 280 | 281 | // Restore system setTimeout 282 | setTimeout = _timeout; 283 | } 284 | runContext = null; 285 | 286 | runArgs = false; 287 | if (hide) line = null; 288 | hide = false; 289 | } else if (runArgs) { 290 | // Line is part of a script block 291 | 292 | // If line has a RESULT comment, turn it into a ResultLine that injects 293 | // the value of the expression into the comment 294 | if (!hide && RESULT_RE.test(line)) { 295 | const resultId = runContext.__.results.length; 296 | line = runContext.__.results[resultId] = new ResultLine(resultId, line); 297 | } 298 | 299 | scriptLines.push(line.scriptLine || line); 300 | } 301 | 302 | // Add line to output 303 | if (!hide) { 304 | // onOutputLine for user script transforms 305 | if (line != null && runmd.onOutputLine) 306 | line = runmd.onOutputLine(line, !!runArgs); 307 | 308 | _write(line); 309 | } 310 | }); 311 | 312 | if (!lame) { 313 | _write('---'); 314 | _write(''); 315 | _write( 316 | outputName 317 | ? `Markdown generated from [${inputName}](${inputName}) by ${LINK_MARKDOWN}` 318 | : `Markdown generated by ${LINK_MARKDOWN}` 319 | ); 320 | _write(''); 321 | } 322 | 323 | return outputLines.join('\n'); 324 | } 325 | 326 | module.exports = { render }; 327 | --------------------------------------------------------------------------------