You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

298 lines
7.8 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'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 nodes 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
}