'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 tab = '\t' var lineFeed = '\n' var space = ' ' var numberSign = '#' var ampersand = '&' var leftParenthesis = '(' var rightParenthesis = ')' var asterisk = '*' var plusSign = '+' var dash = '-' var dot = '.' var colon = ':' var lessThan = '<' var greaterThan = '>' var leftSquareBracket = '[' var backslash = '\\' var rightSquareBracket = ']' var underscore = '_' var graveAccent = '`' var verticalBar = '|' var tilde = '~' var exclamationMark = '!' var entities = { '<': '<', ':': ':', '&': '&', '|': '|', '~': '~' } var shortcut = 'shortcut' var mailto = 'mailto' var https = 'https' var http = 'http' var blankExpression = /\n\s*$/ // 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 ? [dot, rightParenthesis] : [dot] var siblings = parent && parent.children var index = siblings && siblings.indexOf(node) var previous = 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 (previous) { afterNewLine = text(previous) && blankExpression.test(previous.value) } else { afterNewLine = !parent || parent.type === 'root' || parent.type === 'paragraph' } while (++position < length) { character = value.charAt(position) replace = false if (character === '\n') { afterNewLine = true } else if ( character === backslash || character === graveAccent || character === asterisk || character === leftSquareBracket || character === lessThan || (character === ampersand && prefix(value.slice(position)) > 0) || (character === rightSquareBracket && self.inLink) || (gfm && character === tilde && value.charAt(position + 1) === tilde) || (gfm && character === verticalBar && (self.inTable || alignment(value, position))) || (character === underscore && // 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 === colon && protocol(queue.join(''))) ) { replace = true } else if (afterNewLine) { if ( character === greaterThan || character === numberSign || character === asterisk || character === dash || character === plusSign ) { 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 === space || next === tab || next === lineFeed) { 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 (previous && previous.referenceType === shortcut) { position = -1 length = escaped.length while (++position < length) { character = escaped[position] if (character === space || character === tab) { continue } if (character === leftParenthesis || character === colon) { 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) === leftParenthesis ) { 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(previous) && value.charAt(0) === colon && protocol(previous.value.slice(-6)) ) { escaped[0] = one(colon) } // Escape ampersand if it would otherwise start an entity. if ( text(next) && value.charAt(length - 1) === ampersand && prefix(ampersand + next.value) !== 0 ) { escaped[escaped.length - 1] = one(ampersand) } // Escape exclamation marks immediately followed by links. if ( next && next.type === 'link' && value.charAt(length - 1) === exclamationMark ) { escaped[escaped.length - 1] = one(exclamationMark) } // Escape double tildes in GFM. if ( gfm && text(next) && value.charAt(length - 1) === tilde && next.value.charAt(0) === tilde ) { escaped.splice(-1, 0, backslash) } // Escape underscores, but not mid-word (unless in pedantic mode). wordCharBefore = text(previous) && alphanumeric(previous.value.slice(-1)) wordCharAfter = text(next) && alphanumeric(next.value.charAt(0)) if (length === 1) { if ( value === underscore && (pedantic || !wordCharBefore || !wordCharAfter) ) { escaped.unshift(backslash) } } else { if ( value.charAt(0) === underscore && (pedantic || !wordCharBefore || !alphanumeric(value.charAt(1))) ) { escaped.unshift(backslash) } if ( value.charAt(length - 1) === underscore && (pedantic || !wordCharAfter || !alphanumeric(value.charAt(length - 2))) ) { escaped.splice(-1, 0, backslash) } } } return escaped.join('') function one(character) { return escapable.indexOf(character) === -1 ? entities[character] : backslash + character } } } // Check if `index` in `value` is inside an alignment row. function alignment(value, index) { var start = value.lastIndexOf(lineFeed, index) var end = value.indexOf(lineFeed, index) var char end = end === -1 ? value.length : end while (++start < end) { char = value.charAt(start) if ( char !== colon && char !== dash && char !== space && char !== verticalBar ) { 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 tail = value.slice(-6).toLowerCase() return tail === mailto || tail.slice(-5) === https || tail.slice(-4) === http }