blob: a0d966ebc5b6c3ec9d3b2bab1232f72ea3c1bf5e [file] [log] [blame]
'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 = {
'<': '&lt;',
':': '&#x3A;',
'&': '&amp;',
'|': '&#x7C;',
'~': '&#x7E;'
}
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 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) && blankExpression.test(prev.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 === exclamationMark &&
value.charAt(position + 1) === leftSquareBracket) ||
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 (prev && prev.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(prev) &&
value.charAt(0) === colon &&
protocol(prev.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(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 === 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(escaped.length - 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 val = value.slice(-6).toLowerCase()
return val === mailto || val.slice(-5) === https || val.slice(-4) === http
}