node/tools/eslint/node_modules/remark-stringify/lib/compiler.js
Rich Trott c5adb5f008
tools: update ESLint to 4.2.0
ESLint 4.2.0 contains a fix for a bug that is blocking us from moving to
the non-legacy stricter indentation linting. Update to 4.2.0 to remove
the blocking issue.

Backport-PR-URL: https://github.com/nodejs/node/pull/14859
PR-URL: https://github.com/nodejs/node/pull/14155
Ref: https://github.com/eslint/eslint/issues/8882
Ref: https://github.com/eslint/eslint/pull/8885
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Gibson Fahnestock <gibfahn@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
2017-09-05 12:49:53 -04:00

2629 lines
61 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @author Titus Wormer
* @copyright 2015-2016 Titus Wormer
* @license MIT
* @module remark:compiler
* @fileoverview Markdown compiler
*/
'use strict';
/* eslint-env commonjs */
/*
* Dependencies.
*/
var decode = require('parse-entities');
var encode = require('stringify-entities');
var table = require('markdown-table');
var repeat = require('repeat-string');
var extend = require('extend');
var ccount = require('ccount');
var longestStreak = require('longest-streak');
var defaultOptions = require('./defaults.js');
/*
* 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/vgFvT';
/*
* 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;
/*
* Punctuation characters.
*/
var PUNCTUATION = /[-!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~_]/;
/*
* Characters.
*/
var ANGLE_BRACKET_CLOSE = '>';
var ANGLE_BRACKET_OPEN = '<';
var ASTERISK = '*';
var BACKSLASH = '\\';
var CARET = '^';
var COLON = ':';
var SEMICOLON = ';';
var DASH = '-';
var DOT = '.';
var EMPTY = '';
var EQUALS = '=';
var EXCLAMATION_MARK = '!';
var HASH = '#';
var AMPERSAND = '&';
var LINE = '\n';
var CARRIAGE = '\r';
var FORM_FEED = '\f';
var PARENTHESIS_OPEN = '(';
var PARENTHESIS_CLOSE = ')';
var PIPE = '|';
var PLUS = '+';
var QUOTE_DOUBLE = '"';
var QUOTE_SINGLE = '\'';
var SPACE = ' ';
var TAB = '\t';
var VERTICAL_TAB = '\u000B';
var SQUARE_BRACKET_OPEN = '[';
var SQUARE_BRACKET_CLOSE = ']';
var TICK = '`';
var TILDE = '~';
var UNDERSCORE = '_';
/**
* Check whether `character` is numeric.
*
* @param {string} character - Single character to check.
* @return {boolean} - Whether `character` is numeric.
*/
function isNumeric(character) {
return /[0-9]/.test(character);
}
/**
* Check whether `character` is alphanumeric.
*
* @param {string} character - Single character to check.
* @return {boolean} - Whether `character` is alphanumeric.
*/
function isAlphanumeric(character) {
return /\w/.test(character) && character !== UNDERSCORE;
}
/*
* Entities.
*/
var ENTITY_AMPERSAND = AMPERSAND + 'amp' + SEMICOLON;
var ENTITY_ANGLE_BRACKET_OPEN = AMPERSAND + 'lt' + SEMICOLON;
var ENTITY_COLON = AMPERSAND + '#x3A' + SEMICOLON;
/*
* 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 THEMATIC_BREAK_BULLETS = {};
THEMATIC_BREAK_BULLETS[ASTERISK] = true;
THEMATIC_BREAK_BULLETS[DASH] = true;
THEMATIC_BREAK_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;
/**
* Throw an exception with in its `message` `value`
* and `name`.
*
* @param {*} value - Invalid value.
* @param {string} name - Setting name.
*/
function raise(value, name) {
throw new Error(
'Invalid value `' + value + '` ' +
'for setting `' + name + '`'
);
}
/**
* Validate a value to be boolean. Defaults to `def`.
* Raises an exception with `context[name]` when not
* a boolean.
*
* @example
* validateBoolean({foo: null}, 'foo', true) // true
* validateBoolean({foo: false}, 'foo', true) // false
* validateBoolean({foo: 'bar'}, 'foo', true) // Throws
*
* @throws {Error} - When a setting is neither omitted nor
* a boolean.
* @param {Object} context - Settings.
* @param {string} name - Setting name.
* @param {boolean} def - Default value.
*/
function validateBoolean(context, name, def) {
var value = context[name];
if (value === null || value === undefined) {
value = def;
}
if (typeof value !== 'boolean') {
raise(value, 'options.' + name);
}
context[name] = value;
}
/**
* Validate a value to be boolean. Defaults to `def`.
* Raises an exception with `context[name]` when not
* a boolean.
*
* @example
* validateNumber({foo: null}, 'foo', 1) // 1
* validateNumber({foo: 2}, 'foo', 1) // 2
* validateNumber({foo: 'bar'}, 'foo', 1) // Throws
*
* @throws {Error} - When a setting is neither omitted nor
* a number.
* @param {Object} context - Settings.
* @param {string} name - Setting name.
* @param {number} def - Default value.
*/
function validateNumber(context, name, def) {
var value = context[name];
if (value === null || value === undefined) {
value = def;
}
if (typeof value !== 'number' || value !== value) {
raise(value, 'options.' + name);
}
context[name] = value;
}
/**
* Validate a value to be in `map`. Defaults to `def`.
* Raises an exception with `context[name]` when not
* in `map`.
*
* @example
* var map = {bar: true, baz: true};
* validateString({foo: null}, 'foo', 'bar', map) // 'bar'
* validateString({foo: 'baz'}, 'foo', 'bar', map) // 'baz'
* validateString({foo: true}, 'foo', 'bar', map) // Throws
*
* @throws {Error} - When a setting is neither omitted nor
* in `map`.
* @param {Object} context - Settings.
* @param {string} name - Setting name.
* @param {string} def - Default value.
* @param {Object} map - Enum.
*/
function validateString(context, name, def, map) {
var value = context[name];
if (value === null || value === undefined) {
value = def;
}
if (!(value in map)) {
raise(value, 'options.' + name);
}
context[name] = value;
}
/*
* Expose `validate`.
*/
var validate = {
'boolean': validateBoolean,
'string': validateString,
'number': validateNumber
};
/**
* Construct a state `toggler`: a function which inverses
* `property` in context based on its current value.
* The by `toggler` returned function restores that value.
*
* @example
* var context = {};
* var key = 'foo';
* var val = true;
* context[key] = val;
* context.enter = toggler(key, val);
* context[key]; // true
* var exit = context.enter();
* context[key]; // false
* var nested = context.enter();
* context[key]; // false
* nested();
* context[key]; // false
* exit();
* context[key]; // true
*
* @param {string} key - Property to toggle.
* @param {boolean} state - It's default state.
* @return {function(): function()} - Enter.
*/
function toggler(key, state) {
/**
* Construct a toggler for the bound `key`.
*
* @return {Function} - Exit state.
*/
function enter() {
var self = this;
var current = self[key];
self[key] = !state;
/**
* State canceler, cancels the state, if allowed.
*/
function exit() {
self[key] = current;
}
return exit;
}
return enter;
}
/**
* Check whether a node is mergeable with adjacent nodes.
*
* @param {Object} node - Node to check.
* @return {boolean} - Whether `node` is mergable.
*/
function mergeable(node) {
var start;
var end;
if (node.type !== 'text' || !node.position) {
return true;
}
start = node.position.start;
end = node.position.end;
/*
* Only merge nodes which occupy the same size as their
* `value`.
*/
return start.line !== end.line ||
end.column - start.column === node.value.length;
}
/**
* 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'`.
*
* @example
* encodeFactory('false')('AT&T') // 'AT&T'
* encodeFactory('true')('AT&T') // 'AT&amp;T'
* encodeFactory('numbers')('AT&T') // 'ATT&#x26;T'
*
* @param {string} type - Either `'true'`, `'false'`, or
* `'numbers'`.
* @return {function(string): string} - Function which
* takes a value and returns its encoded version.
*/
function encodeFactory(type) {
var options = {};
if (type === 'false') {
return encodeNoop;
}
if (type === 'true') {
options.useNamedReferences = true;
}
if (type === 'escape') {
options.escapeOnly = options.useNamedReferences = true;
}
/**
* Encode HTML entities using the bound options.
*
* @example
* // When `type` is `'true'`.
* encode('AT&T'); // 'AT&amp;T'
*
* // When `type` is `'numbers'`.
* encode('AT&T'); // 'ATT&#x26;T'
*
* @param {string} value - Content.
* @param {Object} [node] - Node which is compiled.
* @return {string} - Encoded content.
*/
function encoder(value) {
return encode(value, options);
}
return encoder;
}
/**
* Returns the length of HTML entity that is a prefix of
* the given string (excluding the ampersand), 0 if it
* does not start with an entity.
*
* @example
* entityPrefixLength('&copycat') // 4
* entityPrefixLength('&foo &amp &bar') // 0
*
* @param {string} value - Input string.
* @return {number} - Length of an entity.
*/
function entityPrefixLength(value) {
var prefix;
/* istanbul ignore if - Currently also tested for at
* implemention, but we keep it here because thats
* proper. */
if (value.charAt(0) !== AMPERSAND) {
return 0;
}
prefix = value.split(AMPERSAND, 2).join(AMPERSAND);
return prefix.length - decode(prefix).length;
}
/**
* Checks if a string starts with HTML entity.
*
* @example
* startsWithEntity('&copycat') // true
* startsWithEntity('&foo &amp &bar') // false
*
* @param {string} value - Value to check.
* @return {number} - Whether `value` starts an entity.
*/
function startsWithEntity(value) {
return entityPrefixLength(value) > 0;
}
/**
* Check if `character` is a valid alignment row character.
*
* @example
* isAlignmentRowCharacter(':') // true
* isAlignmentRowCharacter('=') // false
*
* @param {string} character - Character to check.
* @return {boolean} - Whether `character` is a valid
* alignment row character.
*/
function isAlignmentRowCharacter(character) {
return character === COLON ||
character === DASH ||
character === SPACE ||
character === PIPE;
}
/**
* Check if `index` in `value` is inside an alignment row.
*
* @example
* isInAlignmentRow(':--:', 2) // true
* isInAlignmentRow(':--:\n:-*-:', 9) // false
*
* @param {string} value - Value to check.
* @param {number} index - Position in `value` to check.
* @return {boolean} - Whether `index` in `value` is in
* an alignment row.
*/
function isInAlignmentRow(value, index) {
var length = value.length;
var start = index;
var character;
while (++index < length) {
character = value.charAt(index);
if (character === LINE) {
break;
}
if (!isAlignmentRowCharacter(character)) {
return false;
}
}
index = start;
while (--index > -1) {
character = value.charAt(index);
if (character === LINE) {
break;
}
if (!isAlignmentRowCharacter(character)) {
return false;
}
}
return true;
}
/**
* Factory to escape characters.
*
* @example
* var escape = escapeFactory({ commonmark: true });
* escape('x*x', { type: 'text', value: 'x*x' }) // 'x\\*x'
*
* @param {Object} options - Compiler options.
* @return {function(value, node, parent): string} - Function which
* takes a value and a node and (optionally) its parent and returns
* its escaped value.
*/
function escapeFactory(options) {
/**
* Escape punctuation characters in a node's value.
*
* @param {string} value - Value to escape.
* @param {Object} node - Node in which `value` exists.
* @param {Object} [parent] - Parent of `node`.
* @return {string} - Escaped `value`.
*/
return function escape(value, node, parent) {
var self = this;
var gfm = options.gfm;
var commonmark = options.commonmark;
var pedantic = options.pedantic;
var siblings = parent && parent.children;
var index = siblings && siblings.indexOf(node);
var prev = siblings && siblings[index - 1];
var next = siblings && siblings[index + 1];
var length = value.length;
var position = -1;
var queue = [];
var escaped = queue;
var afterNewLine;
var character;
var wordCharBefore;
var wordCharAfter;
var offset;
if (prev) {
afterNewLine = prev.type === 'text' && /\n\s*$/.test(prev.value);
} else if (parent) {
afterNewLine = parent.type === 'paragraph';
}
while (++position < length) {
character = value.charAt(position);
if (
character === BACKSLASH ||
character === TICK ||
character === ASTERISK ||
character === SQUARE_BRACKET_OPEN ||
(
character === UNDERSCORE &&
/*
* Delegate leading/trailing underscores
* to the multinode version below.
*/
0 < position &&
position < length - 1 &&
(
pedantic ||
!isAlphanumeric(value.charAt(position - 1)) ||
!isAlphanumeric(value.charAt(position + 1))
)
) ||
(self.inLink && character === SQUARE_BRACKET_CLOSE) ||
(
gfm &&
character === PIPE &&
(
self.inTable ||
isInAlignmentRow(value, position)
)
)
) {
afterNewLine = false;
queue.push(BACKSLASH);
} else if (character === ANGLE_BRACKET_OPEN) {
afterNewLine = false;
if (commonmark) {
queue.push(BACKSLASH);
} else {
queue.push(ENTITY_ANGLE_BRACKET_OPEN);
continue;
}
} else if (
gfm &&
!self.inLink &&
character === COLON &&
(
queue.slice(-6).join(EMPTY) === 'mailto' ||
queue.slice(-5).join(EMPTY) === 'https' ||
queue.slice(-4).join(EMPTY) === 'http'
)
) {
afterNewLine = false;
if (commonmark) {
queue.push(BACKSLASH);
} else {
queue.push(ENTITY_COLON);
continue;
}
/* istanbul ignore if - Impossible to test with
* the current set-up. We need tests which try
* to force markdown content into the tree. */
} else if (
character === AMPERSAND &&
startsWithEntity(value.slice(position))
) {
afterNewLine = false;
if (commonmark) {
queue.push(BACKSLASH);
} else {
queue.push(ENTITY_AMPERSAND);
continue;
}
} else if (
gfm &&
character === TILDE &&
value.charAt(position + 1) === TILDE
) {
queue.push(BACKSLASH, TILDE);
afterNewLine = false;
position += 1;
} else if (character === LINE) {
afterNewLine = true;
} else if (afterNewLine) {
if (
character === ANGLE_BRACKET_CLOSE ||
character === HASH ||
LIST_BULLETS[character]
) {
queue.push(BACKSLASH);
afterNewLine = false;
} else if (isNumeric(character)) {
offset = position + 1;
while (offset < length) {
if (!isNumeric(value.charAt(offset))) {
break;
}
offset++;
}
if (
value.charAt(offset) === DOT ||
(
commonmark &&
/* istanbul ignore next - hard to test :( */
value.charAt(offset) === PARENTHESIS_CLOSE
)
) {
queue.push(value.slice(position, offset), BACKSLASH);
position = offset;
character = value.charAt(position);
}
afterNewLine = false;
} else if (
character !== SPACE &&
character !== TAB &&
character !== CARRIAGE &&
character !== VERTICAL_TAB &&
character !== FORM_FEED
) {
afterNewLine = false;
}
}
queue.push(character);
}
/*
* Multi-node versions.
*/
if (siblings && node.type === 'text') {
/*
* Check for an opening parentheses after a
* link-reference (which can be joined by
* white-space).
*/
if (
prev &&
prev.referenceType === 'shortcut'
) {
position = -1;
length = escaped.length;
while (++position < length) {
character = escaped[position];
if (character === SPACE || character === TAB) {
continue;
}
if (character === PARENTHESIS_OPEN) {
escaped[position] = BACKSLASH + character;
}
if (character === COLON) {
if (commonmark) {
escaped[position] = BACKSLASH + character;
} else {
escaped[position] = ENTITY_COLON;
}
}
break;
}
/*
* If the current node is all spaces / tabs,
* preceded by a shortcut, and followed by
* a text starting with `(`, escape it.
*/
if (
next &&
position === length &&
next.type === 'text' &&
next.value.charAt(0) === PARENTHESIS_OPEN
) {
escaped.push(BACKSLASH);
}
}
/*
* Ensure non-auto-links are not seen as links.
* This pattern needs to check the preceding
* nodes too.
*/
if (
gfm &&
!self.inLink &&
prev &&
prev.type === 'text' &&
value.charAt(0) === COLON
) {
queue = prev.value.slice(-6);
if (
queue === 'mailto' ||
queue.slice(-5) === 'https' ||
queue.slice(-4) === 'http'
) {
if (commonmark) {
escaped.unshift(BACKSLASH);
} else {
escaped.splice(0, 1, ENTITY_COLON);
}
}
}
/*
* Escape ampersand if it would otherwise
* start an entity.
*/
if (
next &&
next.type === 'text' &&
value.slice(-1) === AMPERSAND &&
startsWithEntity(AMPERSAND + next.value)
) {
if (commonmark) {
escaped.splice(escaped.length - 1, 0, BACKSLASH);
} else {
escaped.push('amp', SEMICOLON);
}
}
/*
* Escape double tildes in GFM.
*/
if (
gfm &&
next &&
next.type === 'text' &&
value.slice(-1) === TILDE &&
next.value.charAt(0) === TILDE
) {
escaped.splice(escaped.length - 1, 0, BACKSLASH);
}
/*
* Escape underscores, but not mid-word (unless
* in pedantic mode).
*/
wordCharBefore = prev &&
prev.type === 'text' &&
isAlphanumeric(prev.value.slice(-1))
wordCharAfter = next &&
next.type === 'text' &&
isAlphanumeric(next.value.charAt(0))
if (length <= 1) {
if (
value === UNDERSCORE &&
(
pedantic ||
!wordCharBefore ||
!wordCharAfter
)
) {
escaped.unshift(BACKSLASH);
}
} else {
if (
value.charAt(0) === UNDERSCORE &&
(
pedantic ||
!wordCharBefore ||
/* istanbul ignore next - only for trees */
!isAlphanumeric(value.charAt(1))
)
) {
escaped.unshift(BACKSLASH);
}
if (
value.slice(-1) === UNDERSCORE &&
(
pedantic ||
!wordCharAfter ||
/* istanbul ignore next - only for trees */
!isAlphanumeric(value.slice(-2).charAt(0))
)
) {
escaped.splice(escaped.length - 1, 0, BACKSLASH);
}
}
}
return escaped.join(EMPTY);
};
}
/**
* 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 - URI to enclose.
* @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;
/*
* Expose visitors.
*/
var visitors = compilerPrototype.visitors = {};
/*
* Map of applicable enum's.
*/
var maps = {
'entities': ENTITY_OPTIONS,
'bullet': LIST_BULLETS,
'rule': THEMATIC_BREAK_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 {
throw new Error(
'Invalid value `' + options + '` ' +
'for setting `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.escape = escapeFactory(options);
self.options = options;
return self;
};
/*
* Enter and exit helpers.
*/
compilerPrototype.enterLink = toggler('inLink', false);
compilerPrototype.enterTable = toggler('inTable', false);
/**
* Shortcut and collapsed link references need no escaping
* and encoding during the processing of child nodes (it
* must be implied from identifier).
*
* This toggler turns encoding and escaping off for shortcut
* and collapsed references.
*
* Implies `enterLink`.
*
* @param {Compiler} compiler - Compiler instance.
* @param {LinkReference} node - LinkReference node.
* @return {Function} - Exit state.
*/
compilerPrototype.enterLinkReference = function (compiler, node) {
var encode = compiler.encode;
var escape = compiler.escape;
var exitLink = compiler.enterLink();
if (
node.referenceType === 'shortcut' ||
node.referenceType === 'collapsed'
) {
compiler.encode = compiler.escape = encodeNoop;
return function () {
compiler.encode = encode;
compiler.escape = escape;
exitLink();
};
} else {
return exitLink;
}
};
/**
* 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.visitors[node.type] !== 'function') {
self.file.fail(
'Missing compiler for node of type `' +
node.type + '`: `' + node + '`',
node
);
}
return self.visitors[node.type].call(self, 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 results = [];
var length = children.length;
var index = 0;
var node = children[0];
var next;
if (!length) {
return values;
}
length++;
while (++index < length) {
next = children[index];
if (
next &&
node.type === 'text' &&
node.type === next.type &&
mergeable(node) &&
mergeable(next)
) {
node.value += next.value;
} else {
values.push(node);
node = next;
}
}
index = -1;
length = values.length;
parent.children = values;
while (++index < length) {
results[index] = self.visit(values[index], parent);
}
return results;
};
/**
* 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;
var fn = self.visitors.listItem;
while (++index < length) {
bullet = (increment ? start + index : start) + DOT;
values[index] = fn.call(self, 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;
var fn = self.visitors.listItem;
while (++index < length) {
values[index] = fn.call(self, 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.
*/
visitors.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.
*/
visitors.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&amp;T
*
* Supports numbered entities in `settings.encode: numbers`
* mode:
*
* AT&#x26;T
*
* @example
* var compiler = new Compiler();
*
* compiler.text({
* type: 'text',
* value: 'foo'
* });
* // 'foo'
*
* @param {Object} node - `text` node.
* @param {Object} parent - Parent of `node`.
* @return {string} - Raw markdown text.
*/
visitors.text = function (node, parent) {
return this.encode(this.escape(node.value, node, parent), node);
};
/**
* 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.
*/
visitors.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.
*/
visitors.blockquote = function (node) {
var values = this.block(node).split(LINE);
var result = [];
var length = values.length;
var index = -1;
var value;
while (++index < length) {
value = values[index];
result[index] = (value ? SPACE : EMPTY) + value;
}
return ANGLE_BRACKET_CLOSE + result.join(LINE + ANGLE_BRACKET_CLOSE);
};
/**
* 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.
*/
visitors.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.
*/
visitors.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.
*/
visitors.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.
*/
visitors.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.
* @param {Object} parent - Parent of `node`.
* @return {string} - Markdown code block.
*/
visitors.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
* isnt 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.
*/
visitors.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.thematicBreak({
* type: 'thematicBreak'
* });
* // '***'
*
* @return {string} - Markdown rule.
*/
visitors.thematicBreak = 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.
*/
visitors.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.
*/
visitors.emphasis = function (node) {
var marker = this.options.emphasis;
return marker + this.all(node).join(EMPTY) + marker;
};
/**
* Stringify a hard break.
*
* In Commonmark mode, trailing backslash form is used in order
* to preserve trailing whitespace that the line may end with,
* and also for better visibility.
*
* @example
* var compiler = new Compiler();
*
* compiler.break({
* type: 'break'
* });
* // ' \n'
*
* @return {string} - Hard markdown break.
*/
visitors.break = function () {
return this.options.commonmark ? BACKSLASH + LINE : 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.
*/
visitors.delete = function (node) {
return DOUBLE_TILDE + this.all(node).join(EMPTY) + DOUBLE_TILDE;
};
/**
* Stringify a link.
*
* When no title exists, the compiled `children` equal
* `url`, and `url` starts with a protocol, an auto
* link is created:
*
* <http://example.com>
*
* Otherwise, is smart about enclosing `url` (see
* `encloseURI()`) and `title` (see `encloseTitle()`).
*
* [foo](<foo at bar dot com> 'An "example" e-mail')
*
* Supports named entities in the `url` and `title` when
* in `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.link({
* type: 'link',
* url: '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.
*/
visitors.link = function (node) {
var self = this;
var url = self.encode(node.url, node);
var exit = self.enterLink();
var escapedURL = self.encode(self.escape(node.url, node));
var value = self.all(node).join(EMPTY);
exit();
if (
node.title === null &&
PROTOCOL.test(url) &&
(escapedURL === value || escapedURL === MAILTO + value)
) {
/*
* Backslash escapes do not work in autolinks,
* so we do not escape.
*/
return encloseURI(self.encode(node.url), true);
}
url = encloseURI(url);
if (node.title) {
url += SPACE + encloseTitle(self.encode(self.escape(
node.title, node
), 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;
}
/**
* For shortcut and collapsed reference links, the contents
* is also an identifier, so we need to restore the original
* encoding and escaping that were present in the source
* string.
*
* This function takes the unescaped & unencoded value from
* shortcut's child nodes and the identifier and encodes
* the former according to the latter.
*
* @example
* copyIdentifierEncoding('a*b', 'a\\*b*c')
* // 'a\\*b*c'
*
* @param {string} value - Unescaped and unencoded stringified
* link value.
* @param {string} identifier - Link identifier.
* @return {string} - Encoded link value.
*/
function copyIdentifierEncoding(value, identifier) {
var index = 0;
var position = 0;
var length = value.length;
var count = identifier.length;
var result = [];
var start;
while (index < length) {
/*
* Take next non-punctuation characters from `value`.
*/
start = index;
while (
index < length &&
!PUNCTUATION.test(value.charAt(index))
) {
index += 1;
}
result.push(value.slice(start, index));
/*
* Advance `position` to the next punctuation character.
*/
while (
position < count &&
!PUNCTUATION.test(identifier.charAt(position))
) {
position += 1;
}
/*
* Take next punctuation characters from `identifier`.
*/
start = position;
while (
position < count &&
PUNCTUATION.test(identifier.charAt(position))
) {
if (identifier.charAt(position) === AMPERSAND) {
position += entityPrefixLength(identifier.slice(position));
}
position += 1;
}
result.push(identifier.slice(start, position));
/*
* Advance `index` to the next non-punctuation character.
*/
while (index < length && PUNCTUATION.test(value.charAt(index))) {
index += 1;
}
}
return result.join(EMPTY);
}
/**
* 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.
*/
visitors.linkReference = function (node) {
var self = this;
var exitLinkReference = self.enterLinkReference(self, node);
var value = self.all(node).join(EMPTY);
exitLinkReference();
if (
node.referenceType === 'shortcut' ||
node.referenceType === 'collapsed'
) {
value = copyIdentifierEncoding(value, node.identifier);
}
return SQUARE_BRACKET_OPEN + value + 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.
*/
visitors.imageReference = function (node) {
var alt = this.encode(node.alt, node) || EMPTY;
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.
*/
visitors.footnoteReference = function (node) {
return SQUARE_BRACKET_OPEN + CARET + node.identifier +
SQUARE_BRACKET_CLOSE;
};
/**
* Stringify a link- or image definition.
*
* Is smart about enclosing `url` (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',
* url: '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.
*/
visitors.definition = function (node) {
var value = SQUARE_BRACKET_OPEN + node.identifier + SQUARE_BRACKET_CLOSE;
var url = encloseURI(node.url);
if (node.title) {
url += SPACE + encloseTitle(node.title);
}
return value + COLON + SPACE + url;
};
/**
* Stringify an image.
*
* Is smart about enclosing `url` (see `encloseURI()`) and
* `title` (see `encloseTitle()`).
*
* ![foo](</fav icon.png> 'My "favourite" icon')
*
* Supports named entities in `url`, `alt`, and `title`
* when in `settings.encode` mode.
*
* @example
* var compiler = new Compiler();
*
* compiler.image({
* type: 'image',
* url: 'http://example.png/favicon.png',
* title: 'Example Icon',
* alt: 'Foo'
* });
* // '![Foo](http://example.png/favicon.png "Example Icon")'
*
* @param {Object} node - `image` node.
* @return {string} - Markdown image.
*/
visitors.image = function (node) {
var url = encloseURI(this.encode(node.url, node));
var value;
if (node.title) {
url += SPACE + encloseTitle(this.encode(node.title, node));
}
value = EXCLAMATION_MARK +
SQUARE_BRACKET_OPEN + this.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.
*/
visitors.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.
*/
visitors.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: 'tableRow',
* 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.
*/
visitors.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 exit = self.enterTable();
var result = [];
var start;
while (index--) {
result[index] = self.all(rows[index]);
}
exit();
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.
*/
visitors.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}
* @param {Node} node - Syntax tree.
* @return {string} - Markdown document.
*/
compilerPrototype.compile = function (node) {
return this.visit(node);
};
/*
* Expose `stringify` on `module.exports`.
*/
module.exports = Compiler;