snippin

re-use snippets from text files in other contexts

Usage no npm install needed!

<script type="module">
  import snippin from 'https://cdn.skypack.dev/snippin';
</script>

README

Snippin lets you re-use snippets from text files in other contexts.

Like say you're writing some documentation for your code and you want to show some examples of how it behaves. You figure you'll grab the examples from your test suite, because 1. they're already written and 2. they're guaranteed to work --it's so annoying to find bugs in sample code! But there's some boring boilerplate at the top of your test file, and there are some tests that go into too much detail to serve as good examples, so you don't want to just include the whole file in your documentation. With Snippin, you can identify just the specific parts you're interested in:

// test.js
const tap = require('tap')

// #snip "example"
tap.equals(1 + 1, 2)
// #snip

Now you can load them with Snippin and use them however you need:

const snippin = new Snippin()
tap.same(snippin.getSnippetsSync('test.js'), {
  'example': 'tap.equals(1 + 1, 2)\n'
})

Writing snippets

Start a snippet by writing #snip <snippet name> by itself on a line. The snippet name must be a valid JSON string, e.g.

#snip "example snippet"

End a snippet by writing just "#snip" by itself on a line:

#snip

You're allowed as much whitespace as you need before "#snip", and you can also have comment markers on the same line. For example,

// prefixed.js
// #snip "inside a comment"
//
// That "// " at the start of these lines
// won't be part of the snippet
// #snip
tap.same(snippin.getSnippetsSync('prefixed.js'), {
  'inside a comment': `
That "// " at the start of these lines
won't be part of the snippet
` })

Specifically, here are the regex components that match whitespace and comment markers:

  • prefix: /^\s*(?://|\*|/\*)?\s*/
  • suffix: /\s*$/

Snippets can be nested. For example,

// nested.txt
#snip "outer"
Outer snippet
#snip "inner"
Inner snippet
#snip
Still outer snippet
#snip
tap.same(snippin.getSnippetsSync('nested.txt'), {
  'outer': 'Outer snippet\nInner snippet\nStill outer snippet\n',
  'inner': 'Inner snippet\n'
})

You can escape "#snip" directives by prefixing them with a "\". And of course if you need a literal "\#snip" you can write "\\#snip":

// escaped.txt
#snip "snippet"
\#snip "literal #snip"
\#snip
// \\#snip "literal backslash"
#snip
tap.same(snippin.getSnippetsSync('escaped.txt'), {
  'snippet': '#snip "literal #snip"\n#snip\n// \\#snip "literal backslash"\n'
})

API

new Snippin([options, [streamOptions]])

  • refDir string The directory relative to which snippets are loaded. Default: '.'.
  • errorLogger Function Function to use to log warnings; it will be called with a single argument: an error message string. Default: console.warn.

snippin.getSnippets(file)

  • file string The name of the file for which to get snippets.
  • Returns Promise Resolves to an object containing the snippets found in file; keys are the snippet names, values are their contents.

Gets snippets from the given file. Throws if file doesn't exist.

snippin.getSnippetsSync(file)

  • file string The name of the file for which to get snippets.
  • Returns Object The snippets found in file; keys are the snippet names, values are their contents.

Gets snippets from the given file synchronously. Throws Error if file doesn't exist.

snippin.setSnippets(file, snippets)

  • file string The name of the file for which to set snippets. This can be a path to a real file, or just a name.
  • snippets Object The snippets to set; properties are snippet names, and their value is the snippet contents.

Snippets set this way take precedence over snippets loaded from files. This function operates on the entire set of snippets for a file; it will replace the set of snippets for the given file if it's already been loaded, or prevent it from being loaded if it hasn't yet.

Including snippets

Once you've loaded your snippets, you can include them into your text by using any of the popular templating libraries, or template literals, or whatever.

Snippin also ships with a stream transform that can process "#pin" directives and render them as the named snippet. Include a snippet by writing #pin file snippet-name by itself on a line. Both the file and snippet name must be valid JSON strings.

Continuing the first example above, we can do

const doc = `We all know that one and one make two:
  #pin "test.js" "example"`
let output = ''
const pinTx = new PinTx()
pinTx.on('data', chunk => { output += chunk })
pinTx.on('end', () => t.equals(output,
  'We all know that one and one make two:\ntap.equals(1 + 1, 2)\n'))
pinTx.end(doc)

new PinTx([options, [streamOptions]])

  • options Object
    • refDir string The directory relative to which snippets are loaded. Default: '.'.
    • errorLogger Function Function to use to log warnings; it will be called with a single argument: an error message string. Default: console.warn.
  • streamOptions Object Passed as-is to stream.Transform's constructor. Default: {}.

This transform replaces each #pin directive with the snippet it references.

If a #pin directive is malformed or references a snippet which can't be loaded, a warning is logged, and the directive is output as-is.

Command line

There's a command line interface to the stream transform.

Usage: snippin [-d REFDIR] [-o OUTFILE] [--] [INFILE]

Print standard input (or INFILE) to standard output (or OUTFILE)
with #pins expanded relative to the current working directory
(or INFILE's directory, or REFDIR).

Options:

    -d, --refdir=DIRECTORY  load snippets relative to DIRECTORY
    -o, --outfile=FILE      write output to FILE
    -h, --help              show this usage message

Contributing

This project is deliberately left imperfect to encourage you to participate in its development. If you make a Pull Request that

  • explains and solves a problem,
  • follows standard style, and
  • maintains 100% test coverage

it will be merged: this project follows the C4 process.

To make sure your commits follow the style guide and pass all tests, you can add

./.pre-commit

to your git pre-commit hook.