'use strict'; var decimal = require('is-decimal'); var alphanumeric = require('is-alphanumeric'); var whitespace = require('is-whitespace-character'); var escapes = require('markdown-escapes'); var prefix = require('./util/entity-prefix-length'); module.exports = factory; var BACKSLASH = '\\'; var BULLETS = ['*', '-', '+']; var ALLIGNMENT = [':', '-', ' ', '|']; var entities = {'<': '<', ':': ':', '&': '&', '|': '|', '~': '~'}; /* Factory to escape characters. */ function factory(options) { return escape; /* Escape punctuation characters in a node's value. */ function escape(value, node, parent) { var self = this; var gfm = options.gfm; var commonmark = options.commonmark; var pedantic = options.pedantic; var markers = commonmark ? ['.', ')'] : ['.']; 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 escapable = escapes(options); var position = -1; var queue = []; var escaped = queue; var afterNewLine; var character; var wordCharBefore; var wordCharAfter; var offset; var replace; if (prev) { afterNewLine = text(prev) && /\n\s*$/.test(prev.value); } else { afterNewLine = !parent || parent.type === 'root' || parent.type === 'paragraph'; } function one(character) { return escapable.indexOf(character) === -1 ? entities[character] : BACKSLASH + character; } while (++position < length) { character = value.charAt(position); replace = false; if (character === '\n') { afterNewLine = true; } else if ( character === BACKSLASH || character === '`' || character === '*' || character === '[' || character === '<' || (character === '&' && prefix(value.slice(position)) > 0) || (character === ']' && self.inLink) || (gfm && character === '~' && value.charAt(position + 1) === '~') || (gfm && character === '|' && (self.inTable || alignment(value, position))) || ( character === '_' && /* Delegate leading/trailing underscores * to the multinode version below. */ position > 0 && position < length - 1 && ( pedantic || !alphanumeric(value.charAt(position - 1)) || !alphanumeric(value.charAt(position + 1)) ) ) || (gfm && !self.inLink && character === ':' && protocol(queue.join(''))) ) { replace = true; } else if (afterNewLine) { if ( character === '>' || character === '#' || BULLETS.indexOf(character) !== -1 ) { replace = true; } else if (decimal(character)) { offset = position + 1; while (offset < length) { if (!decimal(value.charAt(offset))) { break; } offset++; } if (markers.indexOf(value.charAt(offset)) !== -1) { next = value.charAt(offset + 1); if (!next || next === ' ' || next === '\t' || next === '\n') { queue.push(value.slice(position, offset)); position = offset; character = value.charAt(position); replace = true; } } } } if (afterNewLine && !whitespace(character)) { afterNewLine = false; } queue.push(replace ? one(character) : character); } /* Multi-node versions. */ if (siblings && text(node)) { /* 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 === ' ' || character === '\t') { continue; } if (character === '(' || character === ':') { escaped[position] = one(character); } break; } /* If the current node is all spaces / tabs, * preceded by a shortcut, and followed by * a text starting with `(`, escape it. */ if ( text(next) && position === length && next.value.charAt(0) === '(' ) { 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 && text(prev) && value.charAt(0) === ':' && protocol(prev.value.slice(-6)) ) { escaped[0] = one(':'); } /* Escape ampersand if it would otherwise * start an entity. */ if ( text(next) && value.charAt(length - 1) === '&' && prefix('&' + next.value) !== 0 ) { escaped[escaped.length - 1] = one('&'); } /* Escape double tildes in GFM. */ if ( gfm && text(next) && value.charAt(length - 1) === '~' && next.value.charAt(0) === '~' ) { escaped.splice(escaped.length - 1, 0, BACKSLASH); } /* Escape underscores, but not mid-word (unless * in pedantic mode). */ wordCharBefore = text(prev) && alphanumeric(prev.value.slice(-1)); wordCharAfter = text(next) && alphanumeric(next.value.charAt(0)); if (length === 1) { if (value === '_' && (pedantic || !wordCharBefore || !wordCharAfter)) { escaped.unshift(BACKSLASH); } } else { if ( value.charAt(0) === '_' && (pedantic || !wordCharBefore || !alphanumeric(value.charAt(1))) ) { escaped.unshift(BACKSLASH); } if ( value.charAt(length - 1) === '_' && (pedantic || !wordCharAfter || !alphanumeric(value.charAt(length - 2))) ) { escaped.splice(escaped.length - 1, 0, BACKSLASH); } } } return escaped.join(''); } } /* Check if `index` in `value` is inside an alignment row. */ function alignment(value, index) { var start = value.lastIndexOf('\n', index); var end = value.indexOf('\n', index); start = start === -1 ? -1 : start; end = end === -1 ? value.length : end; while (++start < end) { if (ALLIGNMENT.indexOf(value.charAt(start)) === -1) { return false; } } return true; } /* Check if `node` is a text node. */ function text(node) { return node && node.type === 'text'; } /* Check if `value` ends in a protocol. */ function protocol(value) { var val = value.slice(-6).toLowerCase(); return val === 'mailto' || val.slice(-5) === 'https' || val.slice(-4) === 'http'; }