248 lines
6.8 KiB
JavaScript
248 lines
6.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 BACKSLASH = '\\';
|
|
var BULLETS = ['*', '-', '+'];
|
|
var ALLIGNMENT = [':', '-', ' ', '|'];
|
|
var entities = {'<': '<', ':': ':', '&': '&', '|': '|', '~': '~'};
|
|
|
|
/* 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 ? ['.', ')'] : ['.'];
|
|
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) && /\n\s*$/.test(prev.value);
|
|
} else {
|
|
afterNewLine = !parent || parent.type === 'root' || parent.type === 'paragraph';
|
|
}
|
|
|
|
function one(character) {
|
|
return escapable.indexOf(character) === -1 ?
|
|
entities[character] : BACKSLASH + character;
|
|
}
|
|
|
|
while (++position < length) {
|
|
character = value.charAt(position);
|
|
replace = false;
|
|
|
|
if (character === '\n') {
|
|
afterNewLine = true;
|
|
} else if (
|
|
character === BACKSLASH ||
|
|
character === '`' ||
|
|
character === '*' ||
|
|
character === '[' ||
|
|
character === '<' ||
|
|
(character === '&' && prefix(value.slice(position)) > 0) ||
|
|
(character === ']' && self.inLink) ||
|
|
(gfm && character === '~' && value.charAt(position + 1) === '~') ||
|
|
(gfm && character === '|' && (self.inTable || alignment(value, position))) ||
|
|
(
|
|
character === '_' &&
|
|
/* 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 === ':' && protocol(queue.join('')))
|
|
) {
|
|
replace = true;
|
|
} else if (afterNewLine) {
|
|
if (
|
|
character === '>' ||
|
|
character === '#' ||
|
|
BULLETS.indexOf(character) !== -1
|
|
) {
|
|
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 === ' ' || next === '\t' || next === '\n') {
|
|
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 === ' ' || character === '\t') {
|
|
continue;
|
|
}
|
|
|
|
if (character === '(' || character === ':') {
|
|
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) === '('
|
|
) {
|
|
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) === ':' &&
|
|
protocol(prev.value.slice(-6))
|
|
) {
|
|
escaped[0] = one(':');
|
|
}
|
|
|
|
/* Escape ampersand if it would otherwise
|
|
* start an entity. */
|
|
if (
|
|
text(next) &&
|
|
value.charAt(length - 1) === '&' &&
|
|
prefix('&' + next.value) !== 0
|
|
) {
|
|
escaped[escaped.length - 1] = one('&');
|
|
}
|
|
|
|
/* Escape double tildes in GFM. */
|
|
if (
|
|
gfm &&
|
|
text(next) &&
|
|
value.charAt(length - 1) === '~' &&
|
|
next.value.charAt(0) === '~'
|
|
) {
|
|
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 === '_' && (pedantic || !wordCharBefore || !wordCharAfter)) {
|
|
escaped.unshift(BACKSLASH);
|
|
}
|
|
} else {
|
|
if (
|
|
value.charAt(0) === '_' &&
|
|
(pedantic || !wordCharBefore || !alphanumeric(value.charAt(1)))
|
|
) {
|
|
escaped.unshift(BACKSLASH);
|
|
}
|
|
|
|
if (
|
|
value.charAt(length - 1) === '_' &&
|
|
(pedantic || !wordCharAfter || !alphanumeric(value.charAt(length - 2)))
|
|
) {
|
|
escaped.splice(escaped.length - 1, 0, BACKSLASH);
|
|
}
|
|
}
|
|
}
|
|
|
|
return escaped.join('');
|
|
}
|
|
}
|
|
|
|
/* Check if `index` in `value` is inside an alignment row. */
|
|
function alignment(value, index) {
|
|
var start = value.lastIndexOf('\n', index);
|
|
var end = value.indexOf('\n', index);
|
|
|
|
start = start === -1 ? -1 : start;
|
|
end = end === -1 ? value.length : end;
|
|
|
|
while (++start < end) {
|
|
if (ALLIGNMENT.indexOf(value.charAt(start)) === -1) {
|
|
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';
|
|
}
|