Skip to content

Instantly share code, notes, and snippets.

@Widdershin
Forked from rauschma/js-in-md-checker.md
Last active August 11, 2017 17:01
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 Widdershin/182dbe75e7b8c0f3b7971a883d930c0e to your computer and use it in GitHub Desktop.
Save Widdershin/182dbe75e7b8c0f3b7971a883d930c0e to your computer and use it in GitHub Desktop.

Testing JavaScript embedded in Markdown

  • Use case: Blog posts and books, not documentation for software.
  • As much code as possible should be included within the Markdown file.
  • I’m currently using § as a meta-character.
    • I’m open to alternatives. Other characters I considered: ¡ ¿ Δ ≡
    • I wanted it to be a non-ASCII character, to minimize the risk of conflicts.

Checking is opt-in

JavaScript to be checked:

<!--§check-->

```js
Math.hypot(3, 4); //→ 5
```

Code that isn’t syntactically legal:

```js
class Foo {
    constructor(name) {
        this.name = name;
    }
    ···
}
```

markdown-doctest is currently opt out only. Every example is run by default, need to prepend <!-- skip-example --> to skip a code block. This means adoption is easier, as usually the majority of code blocks are runnable, and new code blocks should not require the user to opt in.

Defining and referring to pieces of code

<!--§id:point-->

```js
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}
```

<!--§include:point-->

```js
const point = new Point(5, 2);
```

markdown-doctest doesn't currently support includes like this, but could without too much work.

Including arbitrary JavaScript

Without ID: add to following code block.

<!--§js:
import {foo} from 'bar';
-->

```js
foo();
```

With ID: defines a named code block.

<!--§js:the-imports
import {foo} from 'bar';
-->

<!--§include:the-imports-->

```js
foo();
```

markdown-doctest doesn't currently support javascript in comments, but it's a good idea. Currently you would achieve this by adding foo to the globals object in your markdown doctest configuration.

Checking results

Single statements

```js
console.log(3 + 4); //→ 7

function add(x, y) {
    return x + y;
}
add(3, 4); //→ 7
```

Loops

```js
const obj = {
    foo: 1,
    bar: 2,
};
for (const [k,v] of Object.entries(obj)) {
    console.log(k, v); //
}
// Output:
// 'foo' 1
// 'bar' 2
```

markdown-doctest doesn't support checks like this, and they're actually quite complex to implement and support. There are a whole bunch of special cases that can be very brutal to support or workaround. My recommendation is to include assert calls in your examples when you want checks like this.

REPL interactions

```repl
> const x = 3;
> const y = 4;
> x + y
7
```

Configuring Babel

  • Once, at the end of a file. Is used for the whole file.
  • Always: babel-preset-env (means all latest features)
  • Explicitly: experimental plugins
<!--§babel:
{
    "plugins": ["transform-react-jsx"]
}
-->

this is achieved in the markdown-doctest config

Ideas

  • Also lint the code?

https://github.com/eslint/eslint-plugin-markdown

@rauschma
Copy link

rauschma commented Aug 11, 2017

For single-line checks, assertions are a good alternative to my style of console.log-based “testing”. However, for loops, I find my style too convenient (I’ve used it a few times in slides and books). Human readers have priority and this seems the approach that is easiest to understand.

Here is how I’d implement it (as basic and simple as possible...):

  • Override console.log (via shadowing – no global patching):
    // undefined means it can’t be checked right now
    let buffer = undefined;
    console = {
        log: (...args) => {
            if (buffer===undefined) {
                buffer = '';
            }
            const strs = args.map(a => JSON.stringify(a));
            buffer += strs.join(' ') + '\n';
        }
    };
  • //→ checks what’s currently in the buffer.
  • // Output: means that the remainder of the code block is text to be checked (after stripping line comments).

For nested data, I’d use Node’s assert.deepStrictEqual().

@rauschma
Copy link

rauschma commented Aug 11, 2017

For REPL blocks, I’d transform:

> foo()
{ firstVeryLongIdentifier: 1,
  anotherVeryLongIdentifier: 2,
  yetAnotherVeryLongIdentifier: 3 }

into:

assert.deepStrictEqual(
foo()
,
{ firstVeryLongIdentifier: 1,
  anotherVeryLongIdentifier: 2,
  yetAnotherVeryLongIdentifier: 3 }
);

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