mdast
Version:
Markdown processor powered by plugins
1,794 lines (1,615 loc) • 39.2 kB
JavaScript
/**
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
* @module mdast:stringify
* @version 2.2.2
* @fileoverview Compile an abstract syntax tree into
* a markdown document.
*/
'use strict';
/* eslint-env commonjs */
/*
* Dependencies.
*/
var he = require('he');
var table = require('markdown-table');
var repeat = require('repeat-string');
var extend = require('extend.js');
var ccount = require('ccount');
var longestStreak = require('longest-streak');
var utilities = require('./utilities.js');
var defaultOptions = require('./defaults.js').stringify;
/*
* Methods.
*/
var raise = utilities.raise;
var validate = utilities.validate;
/*
* Constants.
*/
var INDENT = 4;
var MINIMUM_CODE_FENCE_LENGTH = 3;
var YAML_FENCE_LENGTH = 3;
var MINIMUM_RULE_LENGTH = 3;
var MAILTO = 'mailto:';
var ERROR_LIST_ITEM_INDENT = 'Cannot indent code properly. See ' +
'http://git.io/mdast-lii';
/*
* Expressions.
*/
var EXPRESSIONS_WHITE_SPACE = /\s/;
/*
* Naive fence expression.
*/
var FENCE = /([`~])\1{2}/;
/*
* Expression for a protocol.
*
* @see http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
*/
var PROTOCOL = /^[a-z][a-z+.-]+:\/?/i;
/*
* Characters.
*/
var ANGLE_BRACKET_CLOSE = '>';
var ANGLE_BRACKET_OPEN = '<';
var ASTERISK = '*';
var CARET = '^';
var COLON = ':';
var DASH = '-';
var DOT = '.';
var EMPTY = '';
var EQUALS = '=';
var EXCLAMATION_MARK = '!';
var HASH = '#';
var LINE = '\n';
var PARENTHESIS_OPEN = '(';
var PARENTHESIS_CLOSE = ')';
var PIPE = '|';
var PLUS = '+';
var QUOTE_DOUBLE = '"';
var QUOTE_SINGLE = '\'';
var SPACE = ' ';
var SQUARE_BRACKET_OPEN = '[';
var SQUARE_BRACKET_CLOSE = ']';
var TICK = '`';
var TILDE = '~';
var UNDERSCORE = '_';
/*
* Character combinations.
*/
var BREAK = LINE + LINE;
var GAP = BREAK + LINE;
var DOUBLE_TILDE = TILDE + TILDE;
/*
* Allowed entity options.
*/
var ENTITY_OPTIONS = {};
ENTITY_OPTIONS.true = true;
ENTITY_OPTIONS.false = true;
ENTITY_OPTIONS.numbers = true;
ENTITY_OPTIONS.escape = true;
/*
* Allowed list-bullet characters.
*/
var LIST_BULLETS = {};
LIST_BULLETS[ASTERISK] = true;
LIST_BULLETS[DASH] = true;
LIST_BULLETS[PLUS] = true;
/*
* Allowed horizontal-rule bullet characters.
*/
var HORIZONTAL_RULE_BULLETS = {};
HORIZONTAL_RULE_BULLETS[ASTERISK] = true;
HORIZONTAL_RULE_BULLETS[DASH] = true;
HORIZONTAL_RULE_BULLETS[UNDERSCORE] = true;
/*
* Allowed emphasis characters.
*/
var EMPHASIS_MARKERS = {};
EMPHASIS_MARKERS[UNDERSCORE] = true;
EMPHASIS_MARKERS[ASTERISK] = true;
/*
* Allowed fence markers.
*/
var FENCE_MARKERS = {};
FENCE_MARKERS[TICK] = true;
FENCE_MARKERS[TILDE] = true;
/*
* Which method to use based on `list.ordered`.
*/
var ORDERED_MAP = {};
ORDERED_MAP.true = 'visitOrderedItems';
ORDERED_MAP.false = 'visitUnorderedItems';
/*
* Allowed list-item-indent's.
*/
var LIST_ITEM_INDENTS = {};
var LIST_ITEM_TAB = 'tab';
var LIST_ITEM_ONE = '1';
var LIST_ITEM_MIXED = 'mixed';
LIST_ITEM_INDENTS[LIST_ITEM_ONE] = true;
LIST_ITEM_INDENTS[LIST_ITEM_TAB] = true;
LIST_ITEM_INDENTS[LIST_ITEM_MIXED] = true;
/*
* Which checkbox to use.
*/
var CHECKBOX_MAP = {};
CHECKBOX_MAP.null = EMPTY;
CHECKBOX_MAP.undefined = EMPTY;
CHECKBOX_MAP.true = SQUARE_BRACKET_OPEN + 'x' + SQUARE_BRACKET_CLOSE + SPACE;
CHECKBOX_MAP.false = SQUARE_BRACKET_OPEN + SPACE + SQUARE_BRACKET_CLOSE +
SPACE;
/**
* Encode noop.
* Simply returns the given value.
*
* @example
* var encode = encodeNoop();
* encode('AT&T') // 'AT&T'
*
* @param {string} value - Content.
* @return {string} - Content, without any modifications.
*/
function encodeNoop(value) {
return value;
}
/**
* Factory to encode HTML entities.
* Creates a no-operation function when `type` is
* `'false'`, a function which encodes using named
* references when `type` is `'true'`, and a function
* which encodes using numbered references when `type` is
* `'numbers'`.
*
* By default this should not throw errors, but he does
* throw an error when in `strict` mode:
*
* he.encode.options.strict = true;
* encodeFactory('true')('\x01') // throws
*
* These are thrown on the currently compiled `File`.
*
* @example
* var file = new File();
*
* var encode = encodeFactory('false', file);
* encode('AT&T') // 'AT&T'
*
* encode = encodeFactory('true', file);
* encode('AT&T') // 'AT&T'
*
* encode = encodeFactory('numbers', file);
* encode('AT&T') // 'ATT&T'
*
* @param {string} type - Either `'true'`, `'false'`, or
* `numbers`.
* @param {File} file - Currently compiled virtual file.
* @return {function(string): string} - Function which
* takes a value and returns its encoded version.
*/
function encodeFactory(type, file) {
var options = {};
var fn;
if (type === 'false') {
return encodeNoop;
}
if (type === 'true') {
options.useNamedReferences = true;
}
fn = type === 'escape' ? 'escape' : 'encode';
/**
* Encode HTML entities using `he` using bound options.
*
* @see https://github.com/mathiasbynens/he#strict
*
* @example
* // When `type` is `'true'`.
* encode('AT&T'); // 'AT&T'
*
* // When `type` is `'numbers'`.
* encode('AT&T'); // 'ATT&T'
*
* @param {string} value - Content.
* @param {Object} [node] - Node which is compiled.
* @return {string} - Encoded content.
* @throws {Error} - When `file.quiet` is not `true`.
* However, by default `he` does not throw on
* parse errors, but when
* `he.encode.options.strict: true`, they occur on
* invalid HTML.
*/
function encode(value, node) {
/* istanbul ignore next - useful for other stringifiers */
var position = node ? node.position : null;
try {
return he[fn](value, options);
} catch (exception) {
file.fail(exception, position);
}
}
return encode;
}
/**
* Wrap `url` in angle brackets when needed, or when
* forced.
*
* In links, images, and definitions, the URL part needs
* to be enclosed when it:
*
* - has a length of `0`;
* - contains white-space;
* - has more or less opening than closing parentheses.
*
* @example
* encloseURI('foo bar') // '<foo bar>'
* encloseURI('foo(bar(baz)') // '<foo(bar(baz)>'
* encloseURI('') // '<>'
* encloseURI('example.com') // 'example.com'
* encloseURI('example.com', true) // '<example.com>'
*
* @param {string} uri
* @param {boolean?} [always] - Force enclosing.
* @return {boolean} - Properly enclosed `uri`.
*/
function encloseURI(uri, always) {
if (
always ||
!uri.length ||
EXPRESSIONS_WHITE_SPACE.test(uri) ||
ccount(uri, PARENTHESIS_OPEN) !== ccount(uri, PARENTHESIS_CLOSE)
) {
return ANGLE_BRACKET_OPEN + uri + ANGLE_BRACKET_CLOSE;
}
return uri;
}
/**
* There is currently no way to support nested delimiters
* across Markdown.pl, CommonMark, and GitHub (RedCarpet).
* The following code supports Markdown.pl and GitHub.
* CommonMark is not supported when mixing double- and
* single quotes inside a title.
*
* @see https://github.com/vmg/redcarpet/issues/473
* @see https://github.com/jgm/CommonMark/issues/308
*
* @example
* encloseTitle('foo') // '"foo"'
* encloseTitle('foo \'bar\' baz') // '"foo \'bar\' baz"'
* encloseTitle('foo "bar" baz') // '\'foo "bar" baz\''
* encloseTitle('foo "bar" \'baz\'') // '"foo "bar" \'baz\'"'
*
* @param {string} title - Content.
* @return {string} - Properly enclosed title.
*/
function encloseTitle(title) {
var delimiter = QUOTE_DOUBLE;
if (title.indexOf(delimiter) !== -1) {
delimiter = QUOTE_SINGLE;
}
return delimiter + title + delimiter;
}
/**
* Pad `value` with `level * INDENT` spaces. Respects
* lines. Ignores empty lines.
*
* @example
* pad('foo', 1) // ' foo'
*
* @param {string} value - Content.
* @param {number} level - Indentation level.
* @return {string} - Padded `value`.
*/
function pad(value, level) {
var index;
var padding;
value = value.split(LINE);
index = value.length;
padding = repeat(SPACE, level * INDENT);
while (index--) {
if (value[index].length !== 0) {
value[index] = padding + value[index];
}
}
return value.join(LINE);
}
/**
* Construct a new compiler.
*
* @example
* var compiler = new Compiler(new File('> foo.'));
*
* @constructor
* @class {Compiler}
* @param {File} file - Virtual file.
* @param {Object?} [options] - Passed to
* `Compiler#setOptions()`.
*/
function Compiler(file, options) {
var self = this;
self.file = file;
self.options = extend({}, self.options);
self.setOptions(options);
}
/*
* Cache prototype.
*/
var compilerPrototype = Compiler.prototype;
/*
* Expose defaults.
*/
compilerPrototype.options = defaultOptions;
/*
* Map of applicable enum's.
*/
var maps = {
'entities': ENTITY_OPTIONS,
'bullet': LIST_BULLETS,
'rule': HORIZONTAL_RULE_BULLETS,
'listItemIndent': LIST_ITEM_INDENTS,
'emphasis': EMPHASIS_MARKERS,
'strong': EMPHASIS_MARKERS,
'fence': FENCE_MARKERS
};
/**
* Set options. Does not overwrite previously set
* options.
*
* @example
* var compiler = new Compiler();
* compiler.setOptions({bullet: '*'});
*
* @this {Compiler}
* @throws {Error} - When an option is invalid.
* @param {Object?} [options] - Stringify settings.
* @return {Compiler} - `self`.
*/
compilerPrototype.setOptions = function (options) {
var self = this;
var current = self.options;
var ruleRepetition;
var key;
if (options === null || options === undefined) {
options = {};
} else if (typeof options === 'object') {
options = extend({}, options);
} else {
raise(options, 'options');
}
for (key in defaultOptions) {
validate[typeof current[key]](
options, key, current[key], maps[key]
);
}
ruleRepetition = options.ruleRepetition;
if (ruleRepetition && ruleRepetition < MINIMUM_RULE_LENGTH) {
raise(ruleRepetition, 'options.ruleRepetition');
}
self.encode = encodeFactory(String(options.entities), self.file);
self.options = options;
return self;
};
/**
* Visit a node.
*
* @example
* var compiler = new Compiler();
*
* compiler.visit({
* type: 'strong',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '**Foo**'
*
* @param {Object} node - Node.
* @param {Object?} [parent] - `node`s parent.
* @return {string} - Compiled `node`.
*/
compilerPrototype.visit = function (node, parent) {
var self = this;
/*
* Fail on unknown nodes.
*/
if (typeof self[node.type] !== 'function') {
self.file.fail(
'Missing compiler for node of type `' +
node.type + '`: `' + node + '`',
node
);
}
return self[node.type](node, parent);
};
/**
* Visit all children of `parent`.
*
* @example
* var compiler = new Compiler();
*
* compiler.all({
* type: 'strong',
* children: [{
* type: 'text',
* value: 'Foo'
* },
* {
* type: 'text',
* value: 'Bar'
* }]
* });
* // ['Foo', 'Bar']
*
* @param {Object} parent - Parent node of children.
* @return {Array.<string>} - List of compiled children.
*/
compilerPrototype.all = function (parent) {
var self = this;
var children = parent.children;
var values = [];
var index = -1;
var length = children.length;
while (++index < length) {
values[index] = self.visit(children[index], parent);
}
return values;
};
/**
* Visit ordered list items.
*
* Starts the list with
* `node.start` and increments each following list item
* bullet by one:
*
* 2. foo
* 3. bar
*
* In `incrementListMarker: false` mode, does not increment
* each marker and stays on `node.start`:
*
* 1. foo
* 1. bar
*
* Adds an extra line after an item if it has
* `loose: true`.
*
* @example
* var compiler = new Compiler();
*
* compiler.visitOrderedItems({
* type: 'list',
* ordered: true,
* children: [{
* type: 'listItem',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '1. bar'
*
* @param {Object} node - `list` node with
* `ordered: true`.
* @return {string} - Markdown list.
*/
compilerPrototype.visitOrderedItems = function (node) {
var self = this;
var increment = self.options.incrementListMarker;
var values = [];
var start = node.start;
var children = node.children;
var length = children.length;
var index = -1;
var bullet;
while (++index < length) {
bullet = (increment ? start + index : start) + DOT;
values[index] = self.listItem(children[index], node, index, bullet);
}
return values.join(LINE);
};
/**
* Visit unordered list items.
*
* Uses `options.bullet` as each item's bullet.
*
* Adds an extra line after an item if it has
* `loose: true`.
*
* @example
* var compiler = new Compiler();
*
* compiler.visitUnorderedItems({
* type: 'list',
* ordered: false,
* children: [{
* type: 'listItem',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '- bar'
*
* @param {Object} node - `list` node with
* `ordered: false`.
* @return {string} - Markdown list.
*/
compilerPrototype.visitUnorderedItems = function (node) {
var self = this;
var values = [];
var children = node.children;
var length = children.length;
var index = -1;
var bullet = self.options.bullet;
while (++index < length) {
values[index] = self.listItem(children[index], node, index, bullet);
}
return values.join(LINE);
};
/**
* Stringify a block node with block children (e.g., `root`
* or `blockquote`).
*
* Knows about code following a list, or adjacent lists
* with similar bullets, and places an extra newline
* between them.
*
* @example
* var compiler = new Compiler();
*
* compiler.block({
* type: 'root',
* children: [{
* type: 'paragraph',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // 'bar'
*
* @param {Object} node - `root` node.
* @return {string} - Markdown block content.
*/
compilerPrototype.block = function (node) {
var self = this;
var values = [];
var children = node.children;
var length = children.length;
var index = -1;
var child;
var prev;
while (++index < length) {
child = children[index];
if (prev) {
/*
* Duplicate nodes, such as a list
* directly following another list,
* often need multiple new lines.
*
* Additionally, code blocks following a list
* might easily be mistaken for a paragraph
* in the list itself.
*/
if (child.type === prev.type && prev.type === 'list') {
values.push(prev.ordered === child.ordered ? GAP : BREAK);
} else if (
prev.type === 'list' &&
child.type === 'code' &&
!child.lang
) {
values.push(GAP);
} else {
values.push(BREAK);
}
}
values.push(self.visit(child, node));
prev = child;
}
return values.join(EMPTY);
};
/**
* Stringify a root.
*
* Adds a final newline to ensure valid POSIX files.
*
* @example
* var compiler = new Compiler();
*
* compiler.root({
* type: 'root',
* children: [{
* type: 'paragraph',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // 'bar'
*
* @param {Object} node - `root` node.
* @return {string} - Markdown document.
*/
compilerPrototype.root = function (node) {
return this.block(node) + LINE;
};
/**
* Stringify a heading.
*
* In `setext: true` mode and when `depth` is smaller than
* three, creates a setext header:
*
* Foo
* ===
*
* Otherwise, an ATX header is generated:
*
* ### Foo
*
* In `closeAtx: true` mode, the header is closed with
* hashes:
*
* ### Foo ###
*
* @example
* var compiler = new Compiler();
*
* compiler.heading({
* type: 'heading',
* depth: 2,
* children: [{
* type: 'strong',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '## **bar**'
*
* @param {Object} node - `heading` node.
* @return {string} - Markdown heading.
*/
compilerPrototype.heading = function (node) {
var self = this;
var setext = self.options.setext;
var closeAtx = self.options.closeAtx;
var depth = node.depth;
var content = self.all(node).join(EMPTY);
var prefix;
if (setext && depth < 3) {
return content + LINE +
repeat(depth === 1 ? EQUALS : DASH, content.length);
}
prefix = repeat(HASH, node.depth);
content = prefix + SPACE + content;
if (closeAtx) {
content += SPACE + prefix;
}
return content;
};
/**
* Stringify text.
*
* Supports named entities in `settings.encode: true` mode:
*
* AT&T
*
* Supports numbered entities in `settings.encode: numbers`
* mode:
*
* AT&T
*
* @example
* var compiler = new Compiler();
*
* compiler.text({
* type: 'text',
* value: 'foo'
* });
* // 'foo'
*
* @param {Object} node - `text` node.
* @return {string} - Raw markdown text.
*/
compilerPrototype.text = function (node) {
return this.encode(node.value, node);
};
/**
* Stringify escaped text.
*
* @example
* var compiler = new Compiler();
*
* compiler.escape({
* type: 'escape',
* value: '\n'
* });
* // '\\\n'
*
* @param {Object} node - `escape` node.
* @return {string} - Markdown escape.
*/
compilerPrototype.escape = function (node) {
return '\\' + node.value;
};
/**
* Stringify a paragraph.
*
* @example
* var compiler = new Compiler();
*
* compiler.paragraph({
* type: 'paragraph',
* children: [{
* type: 'strong',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '**bar**'
*
* @param {Object} node - `paragraph` node.
* @return {string} - Markdown paragraph.
*/
compilerPrototype.paragraph = function (node) {
return this.all(node).join(EMPTY);
};
/**
* Stringify a block quote.
*
* @example
* var compiler = new Compiler();
*
* compiler.paragraph({
* type: 'blockquote',
* children: [{
* type: 'paragraph',
* children: [{
* type: 'strong',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* }]
* });
* // '> **bar**'
*
* @param {Object} node - `blockquote` node.
* @return {string} - Markdown block quote.
*/
compilerPrototype.blockquote = function (node) {
var indent = ANGLE_BRACKET_CLOSE + SPACE;
return indent + this.block(node).split(LINE).join(LINE + indent);
};
/**
* Stringify a list. See `Compiler#visitOrderedList()` and
* `Compiler#visitUnorderedList()` for internal working.
*
* @example
* var compiler = new Compiler();
*
* compiler.visitUnorderedItems({
* type: 'list',
* ordered: false,
* children: [{
* type: 'listItem',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '- bar'
*
* @param {Object} node - `list` node.
* @return {string} - Markdown list.
*/
compilerPrototype.list = function (node) {
return this[ORDERED_MAP[node.ordered]](node);
};
/**
* Stringify a list item.
*
* Prefixes the content with a checked checkbox when
* `checked: true`:
*
* [x] foo
*
* Prefixes the content with an unchecked checkbox when
* `checked: false`:
*
* [ ] foo
*
* @example
* var compiler = new Compiler();
*
* compiler.listItem({
* type: 'listItem',
* checked: true,
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }, {
* type: 'list',
* ordered: false,
* children: [{
* type: 'listItem',
* checked: true,
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* }, 0, '*');
* '- [x] bar'
*
* @param {Object} node - `listItem` node.
* @param {Object} parent - `list` node.
* @param {number} position - Index of `node` in `parent`.
* @param {string} bullet - Bullet to use. This, and the
* `listItemIndent` setting define the used indent.
* @return {string} - Markdown list item.
*/
compilerPrototype.listItem = function (node, parent, position, bullet) {
var self = this;
var style = self.options.listItemIndent;
var children = node.children;
var values = [];
var index = -1;
var length = children.length;
var loose = node.loose;
var value;
var indent;
var spacing;
while (++index < length) {
values[index] = self.visit(children[index], node);
}
value = CHECKBOX_MAP[node.checked] + values.join(loose ? BREAK : LINE);
if (
style === LIST_ITEM_ONE ||
(style === LIST_ITEM_MIXED && value.indexOf(LINE) === -1)
) {
indent = bullet.length + 1;
spacing = SPACE;
} else {
indent = Math.ceil((bullet.length + 1) / INDENT) * INDENT;
spacing = repeat(SPACE, indent - bullet.length);
}
value = bullet + spacing + pad(value, indent / INDENT).slice(indent);
if (loose && parent.children.length - 1 !== position) {
value += LINE;
}
return value;
};
/**
* Stringify inline code.
*
* Knows about internal ticks (`\``), and ensures one more
* tick is used to enclose the inline code:
*
* ```foo ``bar`` baz```
*
* Even knows about inital and final ticks:
*
* `` `foo ``
* `` foo` ``
*
* @example
* var compiler = new Compiler();
*
* compiler.inlineCode({
* type: 'inlineCode',
* value: 'foo(); `bar`; baz()'
* });
* // '``foo(); `bar`; baz()``'
*
* @param {Object} node - `inlineCode` node.
* @return {string} - Markdown inline code.
*/
compilerPrototype.inlineCode = function (node) {
var value = node.value;
var ticks = repeat(TICK, longestStreak(value, TICK) + 1);
var start = ticks;
var end = ticks;
if (value.charAt(0) === TICK) {
start += SPACE;
}
if (value.charAt(value.length - 1) === TICK) {
end = SPACE + end;
}
return start + node.value + end;
};
/**
* Stringify YAML front matter.
*
* @example
* var compiler = new Compiler();
*
* compiler.yaml({
* type: 'yaml',
* value: 'foo: bar'
* });
* // '---\nfoo: bar\n---'
*
* @param {Object} node - `yaml` node.
* @return {string} - Markdown YAML document.
*/
compilerPrototype.yaml = function (node) {
var delimiter = repeat(DASH, YAML_FENCE_LENGTH);
var value = node.value ? LINE + node.value : EMPTY;
return delimiter + value + LINE + delimiter;
};
/**
* Stringify a code block.
*
* Creates indented code when:
*
* - No language tag exists;
* - Not in `fences: true` mode;
* - A non-empty value exists.
*
* Otherwise, GFM fenced code is created:
*
* ```js
* foo();
* ```
*
* When in ``fence: `~` `` mode, uses tildes as fences:
*
* ~~~js
* foo();
* ~~~
*
* Knows about internal fences (Note: GitHub/Kramdown does
* not support this):
*
* ````javascript
* ```markdown
* foo
* ```
* ````
*
* Supports named entities in the language flag with
* `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.code({
* type: 'code',
* lang: 'js',
* value: 'fooo();'
* });
* // '```js\nfooo();\n```'
*
* @param {Object} node - `code` node.
* @return {string} - Markdown code block.
*/
compilerPrototype.code = function (node, parent) {
var self = this;
var value = node.value;
var options = self.options;
var marker = options.fence;
var language = self.encode(node.lang || EMPTY, node);
var fence;
/*
* Without (needed) fences.
*/
if (!language && !options.fences && value) {
/*
* Throw when pedantic, in a list item which
* isn’t compiled using a tab.
*/
if (
parent &&
parent.type === 'listItem' &&
options.listItemIndent !== LIST_ITEM_TAB &&
options.pedantic
) {
self.file.fail(ERROR_LIST_ITEM_INDENT, node.position);
}
return pad(value, 1);
}
fence = longestStreak(value, marker) + 1;
/*
* Fix GFM / RedCarpet bug, where fence-like characters
* inside fenced code can exit a code-block.
* Yes, even when the outer fence uses different
* characters, or is longer.
* Thus, we can only pad the code to make it work.
*/
if (FENCE.test(value)) {
value = pad(value, 1);
}
fence = repeat(marker, Math.max(fence, MINIMUM_CODE_FENCE_LENGTH));
return fence + language + LINE + value + LINE + fence;
};
/**
* Stringify HTML.
*
* @example
* var compiler = new Compiler();
*
* compiler.html({
* type: 'html',
* value: '<div>bar</div>'
* });
* // '<div>bar</div>'
*
* @param {Object} node - `html` node.
* @return {string} - Markdown HTML.
*/
compilerPrototype.html = function (node) {
return node.value;
};
/**
* Stringify a horizontal rule.
*
* The character used is configurable by `rule`: (`'_'`)
*
* ___
*
* The number of repititions is defined through
* `ruleRepetition`: (`6`)
*
* ******
*
* Whether spaces delimit each character, is configured
* through `ruleSpaces`: (`true`)
*
* * * *
*
* @example
* var compiler = new Compiler();
*
* compiler.horizontalRule({
* type: 'horizontalRule'
* });
* // '***'
*
* @return {string} - Markdown rule.
*/
compilerPrototype.horizontalRule = function () {
var options = this.options;
var rule = repeat(options.rule, options.ruleRepetition);
if (options.ruleSpaces) {
rule = rule.split(EMPTY).join(SPACE);
}
return rule;
};
/**
* Stringify a strong.
*
* The marker used is configurable by `strong`, which
* defaults to an asterisk (`'*'`) but also accepts an
* underscore (`'_'`):
*
* _foo_
*
* @example
* var compiler = new Compiler();
*
* compiler.strong({
* type: 'strong',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '**Foo**'
*
* @param {Object} node - `strong` node.
* @return {string} - Markdown strong-emphasised text.
*/
compilerPrototype.strong = function (node) {
var marker = this.options.strong;
marker = marker + marker;
return marker + this.all(node).join(EMPTY) + marker;
};
/**
* Stringify an emphasis.
*
* The marker used is configurable by `emphasis`, which
* defaults to an underscore (`'_'`) but also accepts an
* asterisk (`'*'`):
*
* *foo*
*
* @example
* var compiler = new Compiler();
*
* compiler.emphasis({
* type: 'emphasis',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '_Foo_'
*
* @param {Object} node - `emphasis` node.
* @return {string} - Markdown emphasised text.
*/
compilerPrototype.emphasis = function (node) {
var marker = this.options.emphasis;
return marker + this.all(node).join(EMPTY) + marker;
};
/**
* Stringify a hard break.
*
* @example
* var compiler = new Compiler();
*
* compiler.break({
* type: 'break'
* });
* // ' \n'
*
* @return {string} - Hard markdown break.
*/
compilerPrototype.break = function () {
return SPACE + SPACE + LINE;
};
/**
* Stringify a delete.
*
* @example
* var compiler = new Compiler();
*
* compiler.delete({
* type: 'delete',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '~~Foo~~'
*
* @param {Object} node - `delete` node.
* @return {string} - Markdown strike-through.
*/
compilerPrototype.delete = function (node) {
return DOUBLE_TILDE + this.all(node).join(EMPTY) + DOUBLE_TILDE;
};
/**
* Stringify a link.
*
* When no title exists, the compiled `children` equal
* `href`, and `href` starts with a protocol, an auto
* link is created:
*
* <http://example.com>
*
* Otherwise, is smart about enclosing `href` (see
* `encloseURI()`) and `title` (see `encloseTitle()`).
*
* [foo](<foo at bar dot com> 'An "example" e-mail')
*
* Supports named entities in the `href` and `title` when
* in `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.link({
* type: 'link',
* href: 'http://example.com',
* title: 'Example Domain',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '[Foo](http://example.com "Example Domain")'
*
* @param {Object} node - `link` node.
* @return {string} - Markdown link.
*/
compilerPrototype.link = function (node) {
var self = this;
var url = self.encode(node.href, node);
var value = self.all(node).join(EMPTY);
if (
node.title === null &&
PROTOCOL.test(url) &&
(url === value || url === MAILTO + value)
) {
return encloseURI(url, true);
}
url = encloseURI(url);
if (node.title) {
url += SPACE + encloseTitle(self.encode(node.title, node));
}
value = SQUARE_BRACKET_OPEN + value + SQUARE_BRACKET_CLOSE;
value += PARENTHESIS_OPEN + url + PARENTHESIS_CLOSE;
return value;
};
/**
* Stringify a link label.
*
* Because link references are easily, mistakingly,
* created (for example, `[foo]`), reference nodes have
* an extra property depicting how it looked in the
* original document, so stringification can cause minimal
* changes.
*
* @example
* label({
* type: 'referenceImage',
* referenceType: 'full',
* identifier: 'foo'
* });
* // '[foo]'
*
* label({
* type: 'referenceImage',
* referenceType: 'collapsed',
* identifier: 'foo'
* });
* // '[]'
*
* label({
* type: 'referenceImage',
* referenceType: 'shortcut',
* identifier: 'foo'
* });
* // ''
*
* @param {Object} node - `linkReference` or
* `imageReference` node.
* @return {string} - Markdown label reference.
*/
function label(node) {
var value = EMPTY;
var type = node.referenceType;
if (type === 'full') {
value = node.identifier;
}
if (type !== 'shortcut') {
value = SQUARE_BRACKET_OPEN + value + SQUARE_BRACKET_CLOSE;
}
return value;
}
/**
* Stringify a link reference.
*
* See `label()` on how reference labels are created.
*
* @example
* var compiler = new Compiler();
*
* compiler.linkReference({
* type: 'linkReference',
* referenceType: 'collapsed',
* identifier: 'foo',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '[Foo][]'
*
* @param {Object} node - `linkReference` node.
* @return {string} - Markdown link reference.
*/
compilerPrototype.linkReference = function (node) {
return SQUARE_BRACKET_OPEN +
this.all(node).join(EMPTY) + SQUARE_BRACKET_CLOSE +
label(node);
};
/**
* Stringify an image reference.
*
* See `label()` on how reference labels are created.
*
* Supports named entities in the `alt` when
* in `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.imageReference({
* type: 'imageReference',
* referenceType: 'full',
* identifier: 'foo',
* alt: 'Foo'
* });
* // '![Foo][foo]'
*
* @param {Object} node - `imageReference` node.
* @return {string} - Markdown image reference.
*/
compilerPrototype.imageReference = function (node) {
var alt = this.encode(node.alt, node);
return EXCLAMATION_MARK +
SQUARE_BRACKET_OPEN + alt + SQUARE_BRACKET_CLOSE +
label(node);
};
/**
* Stringify a footnote reference.
*
* @example
* var compiler = new Compiler();
*
* compiler.footnoteReference({
* type: 'footnoteReference',
* identifier: 'foo'
* });
* // '[^foo]'
*
* @param {Object} node - `footnoteReference` node.
* @return {string} - Markdown footnote reference.
*/
compilerPrototype.footnoteReference = function (node) {
return SQUARE_BRACKET_OPEN + CARET + node.identifier +
SQUARE_BRACKET_CLOSE;
};
/**
* Stringify an link- or image definition.
*
* Is smart about enclosing `href` (see `encloseURI()`) and
* `title` (see `encloseTitle()`).
*
* [foo]: <foo at bar dot com> 'An "example" e-mail'
*
* @example
* var compiler = new Compiler();
*
* compiler.definition({
* type: 'definition',
* link: 'http://example.com',
* title: 'Example Domain',
* identifier: 'foo'
* });
* // '[foo]: http://example.com "Example Domain"'
*
* @param {Object} node - `definition` node.
* @return {string} - Markdown link- or image definition.
*/
compilerPrototype.definition = function (node) {
var value = SQUARE_BRACKET_OPEN + node.identifier + SQUARE_BRACKET_CLOSE;
var url = encloseURI(node.link);
if (node.title) {
url += SPACE + encloseTitle(node.title);
}
return value + COLON + SPACE + url;
};
/**
* Stringify an image.
*
* Is smart about enclosing `href` (see `encloseURI()`) and
* `title` (see `encloseTitle()`).
*
* 
*
* Supports named entities in `src`, `alt`, and `title`
* when in `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.image({
* type: 'image',
* href: 'http://example.png/favicon.png',
* title: 'Example Icon',
* alt: 'Foo'
* });
* // ''
*
* @param {Object} node - `image` node.
* @return {string} - Markdown image.
*/
compilerPrototype.image = function (node) {
var encode = this.encode;
var url = encloseURI(encode(node.src, node));
var value;
if (node.title) {
url += SPACE + encloseTitle(encode(node.title, node));
}
value = EXCLAMATION_MARK +
SQUARE_BRACKET_OPEN + encode(node.alt || EMPTY, node) +
SQUARE_BRACKET_CLOSE;
value += PARENTHESIS_OPEN + url + PARENTHESIS_CLOSE;
return value;
};
/**
* Stringify a footnote.
*
* @example
* var compiler = new Compiler();
*
* compiler.footnote({
* type: 'footnote',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
* // '[^Foo]'
*
* @param {Object} node - `footnote` node.
* @return {string} - Markdown footnote.
*/
compilerPrototype.footnote = function (node) {
return SQUARE_BRACKET_OPEN + CARET + this.all(node).join(EMPTY) +
SQUARE_BRACKET_CLOSE;
};
/**
* Stringify a footnote definition.
*
* @example
* var compiler = new Compiler();
*
* compiler.footnoteDefinition({
* type: 'footnoteDefinition',
* identifier: 'foo',
* children: [{
* type: 'paragraph',
* children: [{
* type: 'text',
* value: 'bar'
* }]
* }]
* });
* // '[^foo]: bar'
*
* @param {Object} node - `footnoteDefinition` node.
* @return {string} - Markdown footnote definition.
*/
compilerPrototype.footnoteDefinition = function (node) {
var id = node.identifier.toLowerCase();
return SQUARE_BRACKET_OPEN + CARET + id +
SQUARE_BRACKET_CLOSE + COLON + SPACE +
this.all(node).join(BREAK + repeat(SPACE, INDENT));
};
/**
* Stringify table.
*
* Creates a fenced table by default, but not in
* `looseTable: true` mode:
*
* Foo | Bar
* :-: | ---
* Baz | Qux
*
* NOTE: Be careful with `looseTable: true` mode, as a
* loose table inside an indented code block on GitHub
* renders as an actual table!
*
* Creates a spaces table by default, but not in
* `spacedTable: false`:
*
* |Foo|Bar|
* |:-:|---|
* |Baz|Qux|
*
* @example
* var compiler = new Compiler();
*
* compiler.table({
* type: 'table',
* align: ['center', null],
* children: [
* {
* type: 'tableHeader',
* children: [
* {
* type: 'tableCell'
* children: [{
* type: 'text'
* value: 'Foo'
* }]
* },
* {
* type: 'tableCell'
* children: [{
* type: 'text'
* value: 'Bar'
* }]
* }
* ]
* },
* {
* type: 'tableRow',
* children: [
* {
* type: 'tableCell'
* children: [{
* type: 'text'
* value: 'Baz'
* }]
* },
* {
* type: 'tableCell'
* children: [{
* type: 'text'
* value: 'Qux'
* }]
* }
* ]
* }
* ]
* });
* // '| Foo | Bar |\n| :-: | --- |\n| Baz | Qux |'
*
* @param {Object} node - `table` node.
* @return {string} - Markdown table.
*/
compilerPrototype.table = function (node) {
var self = this;
var loose = self.options.looseTable;
var spaced = self.options.spacedTable;
var rows = node.children;
var index = rows.length;
var result = [];
var start;
while (index--) {
result[index] = self.all(rows[index]);
}
start = loose ? EMPTY : spaced ? PIPE + SPACE : PIPE;
return table(result, {
'align': node.align,
'start': start,
'end': start.split(EMPTY).reverse().join(EMPTY),
'delimiter': spaced ? SPACE + PIPE + SPACE : PIPE
});
};
/**
* Stringify a table cell.
*
* @example
* var compiler = new Compiler();
*
* compiler.tableCell({
* type: 'tableCell',
* children: [{
* type: 'text'
* value: 'Qux'
* }]
* });
* // 'Qux'
*
* @param {Object} node - `tableCell` node.
* @return {string} - Markdown table cell.
*/
compilerPrototype.tableCell = function (node) {
return this.all(node).join(EMPTY);
};
/**
* Stringify the bound file.
*
* @example
* var file = new VFile('__Foo__');
*
* file.namespace('mdast').tree = {
* type: 'strong',
* children: [{
* type: 'text',
* value: 'Foo'
* }]
* });
*
* new Compiler(file).compile();
* // '**Foo**'
*
* @this {Compiler}
* @return {string} - Markdown document.
*/
compilerPrototype.compile = function () {
return this.visit(this.file.namespace('mdast').tree);
};
/*
* Expose `stringify` on `module.exports`.
*/
module.exports = Compiler;