node package manager


A system for including content references (conrefs) into Markdown files


I wrote technical documentation in my formative years using the DITA standard. Nowadays, I write documentation in Markdown, but one of the things I really miss is the ability to do content references—or conrefs—from one file to another.

The idea is simple: if you have a term, phrase, or block that you're reusing throughout the documentation, it's best to just define it once, tag it with a unique ID, then refer to that ID whenever you want to pull the content in. Simple, and much more efficient then search and replace.

I wrote a module for this functionality in Node.js, and it would be my dream of dreams if other Markdown packages for other languages implement similar functionality.


From npm, just do

npm install markdown_conrefs


The syntax follows the superior Maruku metadata format for Markdown. IDs are either attached at the block level, or inline. There are a few ways in Maruku to define attributes; this conref system supports all of them (with #blah, with id="blah", with id='blah', or just plain id=blah).

To attach to the block level, just create metadata for the last line of the block, with an id element, like so:

* Item 1
* Item 2
* Item 3
{: #aNiceList}
Here's a great, and final, paragraph.
{: id="myPara"}

To attach conrefs inline, you'll need to wrap the content with brackets ([ ]), and then continue with the same attribute format, like so:

[I don't want to have to rewrite this.]{: .class1 id=aTruth .class2}
[**A RELIEF**]{: key=value id='expression' foo=bar}
[Genesis]{: #newProj} 

Note that this format differs slightly from Maruku. Maruku metadata only applies to Markdown inline elements, like bold, or italic. This system allows you to conref even plain-text. In fact, namp, my Markdown processor written in Node.js, also supports this notation.

Remember: the syntax to define this metadata is curly brace-colon-space! Why is that important? Well...

In order to reference the conref, you'll just use this syntax wherever you want to do an insertion: {:id}. That's curly brace-colon-id-curly brace--nothing else! It's irrelevant if you defined your id with #, id="", or id=''--all you want to do is use the id name. To use the examples above, that'd be:


Pulling additional attributes

Any other attributes you've defined--class names, language identifiers for fenced code blocks, e.t.c.--get pulled into the resulting document as well, but the ID is stripped.

Why? Consider the following text:

I am working on [Project X]{: #product .secret}. I love being on {:product}. It's more rewarding to be a part of {:product}.

If IDs were kept, the block would resolve as:

I am working on [Project X]{: #product .secret}. I love being on [Project X]{: #product .secret}. It's more rewarding to be a part of [Project X]{: #product .secret}.

That's now three elements in the same document with the same ID, product. Instead, the resolve happens like this:

I am working on [Project X]{: #product .secret}. I love being on [Project X]{: .secret}. It's more rewarding to be a part of [Project X]{: .secret}.


First, add require('markdown_conrefs') to your code. This module only has the following functions:

init(source [, options])

This must always be called first! This creates the id-to-content hash. The parameters are:

  • source is a string of a directory or file name, or, an array of strings for directories and filenames. source can represent the file you want to parse, the files you want to parse, or the highest level directory you want to start searching content references for--this module will recursively find all conref IDs in files to keep track of them.
  • options is a JSON object to path that defines any options for the parser. These are the possible properties:
    • supportsAttributes: set this to true if you know that your Markdown parser supports the Maruku syntax of [ ] / {: } to pass attributes into text. When the conrefs are replaced, these attributes will be preserved; otherwise, they are wiped. Default is false.
    • type: the extension of the files you want to keep. If omitted, all files in source are parsed. You can either include the starting dot (.md) or omit it (md).
    • exclusions: an array of strings, indicating any files or directories you don't want to process when source is a directory.
    • blockPrefixChar: a string, indicating the starting line character in your files. This is used when parsing block conrefs. For example, you may be parsing Javascript comments, in which case blockPrefixChar: "*".
    • blockPrefixCharOptional: a boolean which, if true, means that the blockPrefixChar could, or could not, be there. In other (RegExp) words: options.blockPrefixCharOptional ? "?".

This function has no return value, and is synchronous/blocking.


When you're ready to apply the conrefs, use this function. This function takes one parameter:

  • data, the string containing the markdown text--it's usually the file you're reading.

This function returns data with the conrefs replaced; if no conref IDs are found, just plain old data is returned


Returns the entire hash list.

Check out the test/ directory for an example; just run cd test && node test/test.js from this directory.

Keep in mind that this module only replaces the references; you'll still need to run a Markdown parser in order to actually generate HTML.

Error Checking

The module automatically halts if:

  • You have more than one ID with the same reference
  • You refer to an ID that doesn't exist


I tested this module by creating a single file with 10,000 conref IDs, and 10,000 files with a single conref ID. Presumably, lookups would cost the same amount of time, so I made a single call to markdown_conref.init() on each set of files.

On average, the single file took 7ms to complete; the multiple files took 450ms. In other words, they're both damned fast, despite my mediocre programming. For the technical writer, a single file might be easier to manage.