├── .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 |
75 |
--------------------------------------------------------------------------------
/src/README_js.md:
--------------------------------------------------------------------------------
1 | #  
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 | #  
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 |
--------------------------------------------------------------------------------