/** * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT * @module remark:parse:tokenize:link * @fileoverview Tokenise a link. */ 'use strict'; var has = require('has'); var whitespace = require('is-whitespace-character'); var locate = require('../locate/link'); module.exports = link; link.locator = locate; var C_BACKSLASH = '\\'; var C_BRACKET_OPEN = '['; var C_BRACKET_CLOSE = ']'; var C_PAREN_OPEN = '('; var C_PAREN_CLOSE = ')'; var C_LT = '<'; var C_GT = '>'; var C_TICK = '`'; var C_DOUBLE_QUOTE = '"'; var C_SINGLE_QUOTE = '\''; /* Map of characters, which can be used to mark link * and image titles. */ var LINK_MARKERS = {}; LINK_MARKERS[C_DOUBLE_QUOTE] = C_DOUBLE_QUOTE; LINK_MARKERS[C_SINGLE_QUOTE] = C_SINGLE_QUOTE; /* Map of characters, which can be used to mark link * and image titles in commonmark-mode. */ var COMMONMARK_LINK_MARKERS = {}; COMMONMARK_LINK_MARKERS[C_DOUBLE_QUOTE] = C_DOUBLE_QUOTE; COMMONMARK_LINK_MARKERS[C_SINGLE_QUOTE] = C_SINGLE_QUOTE; COMMONMARK_LINK_MARKERS[C_PAREN_OPEN] = C_PAREN_CLOSE; /* Tokenise a link. */ function link(eat, value, silent) { var self = this; var subvalue = ''; var index = 0; var character = value.charAt(0); var commonmark = self.options.commonmark; var gfm = self.options.gfm; var closed; var count; var opening; var beforeURL; var beforeTitle; var subqueue; var hasMarker; var markers; var isImage; var content; var marker; var length; var title; var depth; var queue; var url; var now; var exit; var node; /* Detect whether this is an image. */ if (character === '!') { isImage = true; subvalue = character; character = value.charAt(++index); } /* Eat the opening. */ if (character !== C_BRACKET_OPEN) { return; } /* Exit when this is a link and we’re already inside * a link. */ if (!isImage && self.inLink) { return; } subvalue += character; queue = ''; index++; /* Eat the content. */ length = value.length; now = eat.now(); depth = 0; now.column += index; now.offset += index; while (index < length) { character = value.charAt(index); subqueue = character; if (character === C_TICK) { /* Inline-code in link content. */ count = 1; while (value.charAt(index + 1) === C_TICK) { subqueue += character; index++; count++; } if (!opening) { opening = count; } else if (count >= opening) { opening = 0; } } else if (character === C_BACKSLASH) { /* Allow brackets to be escaped. */ index++; subqueue += value.charAt(index); /* In GFM mode, brackets in code still count. * In all other modes, they don’t. This empty * block prevents the next statements are * entered. */ } else if ((!opening || gfm) && character === C_BRACKET_OPEN) { depth++; } else if ((!opening || gfm) && character === C_BRACKET_CLOSE) { if (depth) { depth--; } else { /* Allow white-space between content and * url in GFM mode. */ if (gfm) { while (index < length) { character = value.charAt(index + 1); if (!whitespace(character)) { break; } subqueue += character; index++; } } if (value.charAt(index + 1) !== C_PAREN_OPEN) { return; } subqueue += C_PAREN_OPEN; closed = true; index++; break; } } queue += subqueue; subqueue = ''; index++; } /* Eat the content closing. */ if (!closed) { return; } content = queue; subvalue += queue + subqueue; index++; /* Eat white-space. */ while (index < length) { character = value.charAt(index); if (!whitespace(character)) { break; } subvalue += character; index++; } /* Eat the URL. */ character = value.charAt(index); markers = commonmark ? COMMONMARK_LINK_MARKERS : LINK_MARKERS; queue = ''; beforeURL = subvalue; if (character === C_LT) { index++; beforeURL += C_LT; while (index < length) { character = value.charAt(index); if (character === C_GT) { break; } if (commonmark && character === '\n') { return; } queue += character; index++; } if (value.charAt(index) !== C_GT) { return; } subvalue += C_LT + queue + C_GT; url = queue; index++; } else { character = null; subqueue = ''; while (index < length) { character = value.charAt(index); if (subqueue && has(markers, character)) { break; } if (whitespace(character)) { if (commonmark) { break; } subqueue += character; } else { if (character === C_PAREN_OPEN) { depth++; } else if (character === C_PAREN_CLOSE) { if (depth === 0) { break; } depth--; } queue += subqueue; subqueue = ''; if (character === C_BACKSLASH) { queue += C_BACKSLASH; character = value.charAt(++index); } queue += character; } index++; } subvalue += queue; url = queue; index = subvalue.length; } /* Eat white-space. */ queue = ''; while (index < length) { character = value.charAt(index); if (!whitespace(character)) { break; } queue += character; index++; } character = value.charAt(index); subvalue += queue; /* Eat the title. */ if (queue && has(markers, character)) { index++; subvalue += character; queue = ''; marker = markers[character]; beforeTitle = subvalue; /* In commonmark-mode, things are pretty easy: the * marker cannot occur inside the title. * * Non-commonmark does, however, support nested * delimiters. */ if (commonmark) { while (index < length) { character = value.charAt(index); if (character === marker) { break; } if (character === C_BACKSLASH) { queue += C_BACKSLASH; character = value.charAt(++index); } index++; queue += character; } character = value.charAt(index); if (character !== marker) { return; } title = queue; subvalue += queue + character; index++; while (index < length) { character = value.charAt(index); if (!whitespace(character)) { break; } subvalue += character; index++; } } else { subqueue = ''; while (index < length) { character = value.charAt(index); if (character === marker) { if (hasMarker) { queue += marker + subqueue; subqueue = ''; } hasMarker = true; } else if (!hasMarker) { queue += character; } else if (character === C_PAREN_CLOSE) { subvalue += queue + marker + subqueue; title = queue; break; } else if (whitespace(character)) { subqueue += character; } else { queue += marker + subqueue + character; subqueue = ''; hasMarker = false; } index++; } } } if (value.charAt(index) !== C_PAREN_CLOSE) { return; } /* istanbul ignore if - never used (yet) */ if (silent) { return true; } subvalue += C_PAREN_CLOSE; url = self.decode.raw(self.unescape(url), eat(beforeURL).test().end); if (title) { beforeTitle = eat(beforeTitle).test().end; title = self.decode.raw(self.unescape(title), beforeTitle); } node = { type: isImage ? 'image' : 'link', title: title || null, url: url }; if (isImage) { node.alt = self.decode.raw(self.unescape(content), now) || null; } else { exit = self.enterLink(); node.children = self.tokenizeInline(content, now); exit(); } return eat(subvalue)(node); }