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