// Enclose abbreviations in tags // 'use strict'; module.exports = function sub_plugin(md) { var escapeRE = md.utils.escapeRE, arrayReplaceAt = md.utils.arrayReplaceAt; // ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on; // you can check character classes here: // http://www.unicode.org/Public/UNIDATA/UnicodeData.txt var OTHER_CHARS = ' \r\n$+<=>^`|~'; var UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source; var UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source; function abbr_def(state, startLine, endLine, silent) { var label, title, ch, labelStart, labelEnd, pos = state.bMarks[startLine] + state.tShift[startLine], max = state.eMarks[startLine]; if (pos + 2 >= max) { return false; } if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; } if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; } labelStart = pos; for (; pos < max; pos++) { ch = state.src.charCodeAt(pos); if (ch === 0x5B /* [ */) { return false; } else if (ch === 0x5D /* ] */) { labelEnd = pos; break; } else if (ch === 0x5C /* \ */) { pos++; } } if (labelEnd < 0 || state.src.charCodeAt(labelEnd + 1) !== 0x3A/* : */) { return false; } if (silent) { return true; } label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, '$1'); title = state.src.slice(labelEnd + 2, max).trim(); if (label.length === 0) { return false; } if (title.length === 0) { return false; } if (!state.env.abbreviations) { state.env.abbreviations = {}; } // prepend ':' to avoid conflict with Object.prototype members if (typeof state.env.abbreviations[':' + label] === 'undefined') { state.env.abbreviations[':' + label] = title; } state.line = startLine + 1; return true; } function abbr_replace(state) { var i, j, l, tokens, token, text, nodes, pos, reg, m, regText, regSimple, currentToken, blockTokens = state.tokens; if (!state.env.abbreviations) { return; } regSimple = new RegExp('(?:' + Object.keys(state.env.abbreviations).map(function (x) { return x.substr(1); }).sort(function (a, b) { return b.length - a.length; }).map(escapeRE).join('|') + ')'); regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' + '(' + Object.keys(state.env.abbreviations).map(function (x) { return x.substr(1); }).sort(function (a, b) { return b.length - a.length; }).map(escapeRE).join('|') + ')' + '($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])'; reg = new RegExp(regText, 'g'); for (j = 0, l = blockTokens.length; j < l; j++) { if (blockTokens[j].type !== 'inline') { continue; } tokens = blockTokens[j].children; // We scan from the end, to keep position when new tags added. for (i = tokens.length - 1; i >= 0; i--) { currentToken = tokens[i]; if (currentToken.type !== 'text') { continue; } pos = 0; text = currentToken.content; reg.lastIndex = 0; nodes = []; // fast regexp run to determine whether there are any abbreviated words // in the current token if (!regSimple.test(text)) { continue; } while ((m = reg.exec(text))) { if (m.index > 0 || m[1].length > 0) { token = new state.Token('text', '', 0); token.content = text.slice(pos, m.index + m[1].length); nodes.push(token); } token = new state.Token('abbr_open', 'abbr', 1); token.attrs = [ [ 'title', state.env.abbreviations[':' + m[2]] ] ]; nodes.push(token); token = new state.Token('text', '', 0); token.content = m[2]; nodes.push(token); token = new state.Token('abbr_close', 'abbr', -1); nodes.push(token); reg.lastIndex -= m[3].length; pos = reg.lastIndex; } if (!nodes.length) { continue; } if (pos < text.length) { token = new state.Token('text', '', 0); token.content = text.slice(pos); nodes.push(token); } // replace current node blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes); } } } md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: [ 'paragraph', 'reference' ] }); md.core.ruler.after('linkify', 'abbr_replace', abbr_replace); };