298 lines
7.8 KiB
JavaScript
298 lines
7.8 KiB
JavaScript
'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
|
||
}
|