UNPKG

mdast

Version:

Markdown processor powered by plugins

1,794 lines (1,615 loc) 39.2 kB
/** * @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&amp;T' * * encode = encodeFactory('numbers', file); * encode('AT&T') // 'ATT&#x26;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&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. * @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&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. * @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()`). * * ![foo](</fav icon.png> 'My "favourite" icon') * * 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' * }); * // '![Foo](http://example.png/favicon.png "Example Icon")' * * @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;