'use strict'; var url = require('url'); var typeOf = require('kind-of'); var get = require('get-value'); var type = require('typeof-article'); var isSelfClosing = require('is-self-closing'); var diacritics = require('diacritics-map'); var stripColor = require('strip-color'); var wordRegex = require('word-regex'); var util = require('snapdragon-util'); /** * Wrapper for creating the handlers for compiling a tag that has `*.open` and * `*.close` nodes, in a single function call. * * ```js * breakdance.set('div', utils.block('', '')); * breakdance.set('address', block('\n
\n', '\n\n')); * // optionally pass a handler function to access the "parent" node * breakdance.set('abbr', block('', '', function(node) { * var attr = utils.toAttribs(node.attribs, ['title']); * if (attr) { * node.open = ''; * } * })) * ``` * @param {String} `open` The opening tag to render * @param {String} `close` The closing tag to render * @param {Object} `state` * @param {Function} `handler` Visitor function to modify the node * @return {undefined} * @api public */ exports.block = function(open, close, handler) { return function(node, nodes, i) { if (util.isEmptyNodes(node, open)) return; var state = this.state; var type = node.type; var isVoid = isSelfClosing(node.type); if (!isVoid && !this.compilers.hasOwnProperty(type + '.open')) { this.set(type + '.open', util.noop); this.set(type + '.close', util.noop); } // convert headings to bold see: edge-cases.md - headings #1 if (/^h[1-6]$/.test(type)) { if (util.isInside(state, node, ['a', 'li', 'table'])) { node.type = 'strong'; open = '**'; close = '**'; } } if (typeof handler === 'function') { handler.call(this, node, nodes, i); // allow handler to override opening tag if (node.open) { open = node.open; } } if (!/^(sup|sub)$/.test(node.type) && /[a-z0-9]/i.test(this.output.slice(-1))) { this.emit(' '); } this.emit(open, node); this.mapVisit(node); this.emit(close, node); }; }; /** * Stringify the `attribs` for a node. * * ```js * var str = utils.toAttribs(node.attribs); * ``` * @param {Object} `attribs` Object of attributes to stringify * @param {Array} `names` Array of `names` to only stringify attributes with those * names. * @return {String} Returns a string of attributes, e.g. `src="foo.jpg"` * @api public */ exports.toAttribs = function(attribs, names) { if (!attribs) return ''; var attr = ''; for (var key in attribs) { if (attribs.hasOwnProperty(key)) { if (names && names.indexOf(key) === -1) { continue; } var val = attribs[key]; attr = ' ' + key; if (typeof val !== 'boolean') { attr += '="' + val.trim() + '"'; } } } return attr; }; /** * Attempt to get a "language" value from the given `attribs`. * Used with code/pre tags. * * ```js * breakdance.set('code', function(node) { * var lang = utils.getLang(node.attribs); * // console.log(lang); * }); * ``` * @param {Object} `attribs` The `node.attribs` object * @return {String} * @api public */ exports.getLang = function(attribs) { attribs = attribs || {}; var lang = get(attribs, 'data-lang') || get(attribs, 'data-language') || ''; var classLang = get(attribs, 'class') || ''; var regex = /lang(?:uage)?-([^\s]+)/; var match = regex.exec(lang) || regex.exec(classLang); if (match) { return match[1]; } return lang; }; /** * Helper for handling spacing around emphasis tags. */ exports.emphasis = function(openTag) { return function(node) { if (this.options.whitespace === false) { return this.mapVisit(node); } if (util.isEmptyNodes(node, openTag)) return; if (/(^|\w)$/.test(this.output)) this.emit(' '); this.mapVisit(node); }; }; /** * Slugify the url part of a markdown link. * * ```js * var str = utils.toAttribs(node.attribs); * ``` * * @param {String} `str` The string to slugify * @param {Object} `options` Pass a custom slugify function on `options.slugify`. As an implementor, this allows you to just pass the breakdance options as the second argument to allow users to override this function. * @return {String} * @api {public} */ exports.slugify = function(str, options) { options = options || {}; if (options.slugify === false) return str; if (typeof options.slugify === 'function') { return options.slugify(str, options); } str = stripColor(str); str = str.toLowerCase(); // `.split()` is often (but not always) faster than `.replace()` str = str.split(/ /).join('-'); str = str.split(/\t/).join('--'); str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join(''); str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join(''); str = replaceDiacritics(str); if (options.num) { str += '-' + options.num; } return str; }; /** * Replace diacritics in the given string with english language * equivalents. This is necessary for slugifying headings to * be conformant with how github slugifies headings. */ function replaceDiacritics(str) { return str.replace(/[À-ž]/g, function(ch) { return diacritics[ch] || ch; }); } /** * Formats the link part of a "src" or "href" attribute on the given `node`. * * ```js * // this is how