(function() {
/**
* @module
*/
/**
* @typedef {Object} Block Parsed chunk of information about the Markdown text.
* @property {String} type Type of block - either `raw` or `fence`.
* @property {String} lang Language text for the codeblock.
* @property {String} symbol Character used to open and close the block.
* @property {String} open String used to open the block. e.g. "```" or "~~~".
* @property {String} close String used to close the block. e.g. "```" or "~~~".
* @property {String} content String inside the block.
*/
/**
* Simple String wrapper class to help navigate the characters within the String.
* @example
* var feed = new TextFeed('some random text here!');
* console.log(feed.peek()); // prints 's'
* console.log(feed.peek(1)); // prints 'o'
* console.log(feed.next()); // prints 's'
* console.log(feed.next()); // prints 'o'
* console.log(feed.next(2)); // prints 'me'
* console.log(feed.skip(1)); // skip ' '
* console.log(feed.next()); // prints 'r'
*/
class TextFeed {
/**
* @param {String} str input string.
*/
constructor(str) {
this.str = str;
this.ptr = 0;
}
/**
* Return the next `n` characters and update the internal pointer.
* @param {Number} [n=0] next `n` characters to return.
* @return {String}
*/
next(n = 0) {
if (n > 0) {
this.ptr += n;
return this.str.slice(this.ptr - n, this.ptr);
}
return this.str.charAt(this.ptr++);
}
/**
* Return the next `n` characters but do not update the internal pointer.
* @param {Number} [n=0] next `n` characters to peek.
* @return {String}
*/
peek(n = 0) {
return this.str.charAt(this.ptr + n);
}
/**
* Skip the next `n` characters and return a reference to itself.
* @param {Number} [n=1] number of characters to skip.
* @return {TextFeed}
*/
skip(n = 1) {
this.ptr += n;
return this;
}
/**
* Return the internal string.
* @return {String}
*/
toString() {
return this.str;
}
}
/**
* Super simple markdown parser just for `fence` only.
* @example
* var md = fs.readFileSync('README.md', {encoding: 'utf8'});
* var fence = new MdFence(['vg', 'vega', 'vega-lite', 'vl']);
* console.log(fence.eat(md).blocks); // prints parsed blocks
*/
class MdFence {
constructor() {
this._blocks = [];
this._current = null;
}
/**
* A internal list of `block` that has been parsed but not been transformed yet.
* @return {Block[]}
*/
get blocks() {
return this._blocks;
}
/**
* Consume a String and update the internal `Blocks`. You can call `eat`
* multiple times as the internal state will be updated. Return a reference
* to itself.
* @param {String} str Markdown string to parse.
* @return {MdFence}
*/
eat(str) {
if (!str || str.length === 0) return this;
var feed = new TextFeed(str);
while (feed.peek()) {
let res = MdFence.isFence(feed, this._current && this._current.symbol);
if (res) {
if (this._current) {
this._blocks.push(this._current);
}
if (this._current.type === 'fence') {
// close
this._current.close = res.bracket;
this._current = null;
} else {
// open
this._current = {
type: 'fence',
open: res.bracket,
symbol: res.symbol,
lang: MdFence.getFenceLang(feed),
content: ''
};
}
continue;
}
if (this._current) {
this._current.content += feed.next();
} else {
this._current = {type: 'raw', content: feed.next()};
}
}
return this;
}
/**
* Resolve all the current `Blocks` and transform them. Will empty the current
* `Block` queue. Return a `Promise` as the transform function can be async.
* @param {Function} [fn = ({content, lang}) => content] Transform function that takes in a `Block` object and output a String.
* @return {Promise}
*/
transform(fn = ({content, lang}) => content) {
var promises = this.blocks.map(block => {
var p = fn(block);
if (p) return Promise.resolve(p);
return Promise.resolve(MdFence.block2String(block));
});
this._blocks = [];
return Promise.all(promises);
}
/**
* Flush any unfinished blocks. Will close any `fence` if not closed.
* @return {MdFence}
*/
flush() {
if (this._current) {
if (this._current.type === 'fence' && !this._current.close) {
this._current.close = this._current.open;
}
this._blocks.push(this._current);
}
this._current = null;
return this;
}
/**
* Convert a `Block` back to its String form.
* @param {Block} block
* @return {String}
*/
static block2String(block) {
if (block.type === 'fence')
return `${block.open}${block.lang}\n${block.content}${block.close}`;
return block.content;
}
/**
* Peek and check if it is a fence. If so return the symbol used, and the
* String used to open or close the fence.
* Return false if not a fence.
* @param {TextFeed} feed
* @param {String} symbol
* @return {{symbol: String, bracket: String}}
*/
static isFence(feed, symbol) {
if (!symbol) {
return MdFence.isFence(feed, '`') || MdFence.isFence(feed, '~');
}
var i = 0;
while (feed.peek(i) === symbol) {
i += 1;
}
if (i >= 3) {
let bracket = feed.next(i);
return {symbol, bracket};
}
return false;
}
/**
* Extract the language string after the code block.
* @param {TextFeed} feed
* @return {String}
*/
static getFenceLang(feed) {
var lang = [];
while (feed.peek()) {
let c = feed.next();
if (/[\s\n\r\f\0]/.test(c)) {
feed.skip(-1);
break;
}
lang.push(c);
}
return lang.join('').toLowerCase();
}
}
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = MdFence;
} else if (typeof define === 'function' && define.amd) {
define(function() {
return MdFence;
});
} else {
this.MdFence = MdFence;
}
}.call(
(function() {
return this || (typeof window !== 'undefined' ? window : global);
})()
));