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