diff --git a/index.js b/index.js index a4fc24b..b53d31f 100644 --- a/index.js +++ b/index.js @@ -4,14 +4,63 @@ * Cached method. */ -var splice = Array.prototype.splice; var has = Object.prototype.hasOwnProperty; /* * Hide process use from browserify. */ + var proc = typeof global !== 'undefined' && global.process; +/* + * Blacklist of SHAs which are also valid words. + * + * GitHub allows abbreviating SHAs up to 7 characters. + * These are ignored in text because they might just be + * ment as normal words. If you’d like these to link to + * their SHAs, just use more than 7 characters. + * + * Generated by: + * + * egrep -i "^[a-f0-9]{7,}$" /usr/share/dict/words + */ + +var BLACKLIST = [ + 'deedeed', + 'fabaceae' +]; + +/** + * Check if a value is a SHA. + * + * @param {string} sha + * @return {boolean} + */ +function isSHA(sha) { + return BLACKLIST.indexOf(sha.toLowerCase()) === -1; +} + +/** + * Abbreviate a SHA. + * + * @param {string} sha + * @return {string} + */ +function abbr(sha) { + return sha.slice(0, 7); +} + +/* + * Map of overwrites for at-mentions. + * GitHub does some fancy stuff with `@mention`, by linking + * it to their blog-post introducing the feature. + * To my knowledge, there are no other magical usernames. + */ + +var OVERWRITES = {}; + +OVERWRITES.mentions = OVERWRITES.mention = 'blog/821'; + /** * Return a URL to GitHub, relative to an optional * `repo` object, or `user` and `project`. @@ -41,48 +90,33 @@ function gh(repo, project) { * Username may only contain alphanumeric characters or * single hyphens, and cannot begin or end with a hyphen. * - * `PERSON` is either a user or a team, but also matches a team: - * https://github.com/blog/1121-introducing-team-mentions + * `PERSON` is either a user or an organization, but also + * matches a team: + * + * https://github.com/blog/1121-introducing-team-mentions */ var NAME = '(?:[a-z0-9]{1,2}|[a-z0-9][a-z0-9-]{1,37}[a-z0-9])'; var USER = '(' + NAME + ')'; var PERSON = '(' + NAME + '(?:\\/' + NAME + ')?)'; -var SHA = '([a-f0-9]{7,40})'; +var HASH = '([a-f0-9]{7,40})'; var NUMBER = '([0-9]+)'; var PROJECT = '((?:[a-z0-9-]|\\.git[a-z0-9-]|\\.(?!git))+)'; -var ISSUE = '(?:GH-|#)' + NUMBER; var REPO = USER + '\\/' + PROJECT; -var EXPRESSION_SHA = new RegExp( - '^' + SHA + '\\b', 'gi' -); - -var EXPRESSION_USER_SHA = new RegExp( - '^' + USER + '@' + SHA + '\\b', 'gi' -); - -var EXPRESSION_REPO_SHA = new RegExp( - '^' + REPO + '@' + SHA + '\\b', 'gi' -); - -var EXPRESSION_ISSUE = new RegExp( - '^' + ISSUE + '\\b', 'gi' -); - -var EXPRESSION_USER_ISSUE = new RegExp( - '^' + USER + '#' + NUMBER + '\\b', 'gi' -); +var SHA = new RegExp('^' + HASH + '\\b', 'i'); +var USER_SHA = new RegExp('^' + USER + '@' + HASH + '\\b', 'i'); +var REPO_SHA = new RegExp('^' + REPO + '@' + HASH + '\\b', 'i'); +var ISSUE = new RegExp('^(?:GH-|#)' + NUMBER + '\\b', 'i'); +var USER_ISSUE = new RegExp('^' + USER + '#' + NUMBER + '\\b', 'i'); +var REPO_ISSUE = new RegExp('^' + REPO + '#' + NUMBER + '\\b', 'i'); +var MENTION = new RegExp('^@' + PERSON + '\\b(?!-)', 'i'); -var EXPRESSION_REPO_ISSUE = new RegExp( - '^' + REPO + '#' + NUMBER + '\\b', 'gi' -); - -var EXPRESSION_MENTION = new RegExp( - '^@' + PERSON + '\\b(?!-)', 'gi' -); +/* + * Match a repo from a git / github URL. + */ -var EXPRESSIONS_REPO = new RegExp( +var REPOSITORY = new RegExp( '(?:^|/(?:repos/)?)' + REPO + '(?=\\.git|[\\/#@]|$)', 'i' ); @@ -91,238 +125,50 @@ var EXPRESSIONS_REPO = new RegExp( * references. */ -var EXPRESSIONS_NON_REFERENCE_LIKE = /[^/.@#_a-zA-Z0-9-]/; +var NON_GITHUB = /^[\s\S]+?(?:[^/.@#_a-zA-Z0-9-](?=[@#_a-zA-Z0-9-])|(?=$))/; /* * Expressions to use. */ var expressions = { - 'sha': EXPRESSION_SHA, - 'userSHA': EXPRESSION_USER_SHA, - 'repoSHA': EXPRESSION_REPO_SHA, - 'issue': EXPRESSION_ISSUE, - 'userIssue': EXPRESSION_USER_ISSUE, - 'repoIssue': EXPRESSION_REPO_ISSUE, - 'mention': EXPRESSION_MENTION + 'ghRepoSHA': REPO_SHA, + 'ghUserSHA': USER_SHA, + 'ghSha': SHA, + 'ghRepoIssue': REPO_ISSUE, + 'ghUserIssue': USER_ISSUE, + 'ghIssue': ISSUE, + 'ghMention': MENTION }; -var order = [ - 'repoSHA', - 'userSHA', - 'sha', - 'repoIssue', - 'userIssue', - 'issue', - 'mention' -]; - /* - * Blacklist of SHAs which are also valid words. - * - * GitHub allows abbreviating SHAs up to 7 characters. - * - * Generated by: - * - * egrep -i "^[a-f0-9]{7,}$" /usr/share/dict/words + * Order in which to use expressions. */ -var BLACKLIST = [ - 'deedeed', - 'fabaceae' +var order = [ + 'ghRepoSHA', + 'ghUserSHA', + 'ghSha', + 'ghRepoIssue', + 'ghUserIssue', + 'ghIssue', + 'ghMention' ]; -/** - * Check if a value is a SHA. - * - * @param {string} sha - * @return {boolean} - */ -function isSHA(sha) { - return BLACKLIST.indexOf(sha.toLowerCase()) === -1; -} - -/** - * Abbreviate a SHA. - * - * @param {string} sha - * @return {string} - */ -function abbr(sha) { - return sha.slice(0, 7); -} - -/** - * Check if a node is a text node. - * - * @param {Node} node - * @return {boolean} - */ -function isText(node) { - return node && node.type === 'text'; -} - -/** - * Render a link node. - * - * @param {Object} position - * @param {string} href - * @param {Array.} children - * @return {Node} - */ -function link(position, href, children) { - return { - 'type': 'link', - 'href': href, - 'title': null, - 'children': children, - 'position': position - }; -} - -/** - * Render a text node. - * - * @param {Object} position - * @param {string} value - * @return {Node} - */ -function text(position, value) { - return { - 'type': 'text', - 'value': value, - 'position': position - }; -} - -/** - * Find references in a text node, and return a list - * of replacement nodes. - * - * @param {Node} parent - * @param {Object} repo - * @return {Array.} - */ -function augment(parent, repo) { - var value = parent.value; - var valueLength = value.length; - var nodes = []; - var length = order.length; - var index = -1; - var offset = -1; - var node; - var name; - var match; - var subposition; - var start = 0; - var end = 0; - var position = parent.position ? parent.position.start : {}; - var line = position.line || 1; - var column = position.column || 1; - - /** - * Get the current position. - * - * @return {Object} - */ - function now() { - return { - 'line': line, - 'column': column - }; - } - - /** - * Location getter. - * - * @return {function(): Object} - */ - function location() { - var before = now(); - - /** - * Return a `position`. - * - * @return {Object} - */ - return function () { - return { - 'start': before, - 'end': now() - }; - }; - } - - position = location(); - - while (++offset < valueLength) { - index = -1; - - if ( - offset === 0 || - EXPRESSIONS_NON_REFERENCE_LIKE.test(value.charAt(offset - 1)) - ) { - while (++index < length) { - name = order[index]; - - match = expressions[name].exec(value.slice(offset)); - - expressions[name].lastIndex = 0; - - if (match) { - subposition = location(); - - end = offset; - - offset += match[0].length; - - node = augment[name].apply( - null, [subposition(), repo].concat(match) - ); - - if (node) { - if (end !== start) { - nodes.push(text( - position(), value.slice(start, end) - )); - - position = location(); - } - - start = offset; - nodes.push(node); - } - } - } - } - - if (value.charAt(index) === '\n') { - line++; - column = 0; - } - - column++; - } - - if (start < valueLength) { - nodes.push(text(position(), value.slice(start, offset))); - } - - return nodes; -} - /** * Render a SHA relative to a repo. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Project. * @param {Object} $3 - SHA. * @return {Node?} */ -augment.repoSHA = function (position, repo, $0, $1, $2, $3) { +function ghRepoSHA(eat, $0, $1, $2, $3) { + var now = eat.now(); var href; var value; @@ -330,186 +176,183 @@ augment.repoSHA = function (position, repo, $0, $1, $2, $3) { href = gh($1, $2) + 'commit/' + $3; value = $1 + '/' + $2 + '@' + abbr($3); - return link(position, href, [text(position, value)]); + return eat($0)(this.renderLink(true, href, value, null, now, eat)); } -}; +} + +ghRepoSHA.notInLink = true; /** * Render a SHA relative to a user. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - SHA. * @return {Node?} */ -augment.userSHA = function (position, repo, $0, $1, $2) { +function ghUserSHA(eat, $0, $1, $2) { + var now = eat.now(); var href; var value; if (isSHA($2)) { - href = gh($1, repo.project) + 'commit/' + $2; + href = gh($1, this.github.project) + 'commit/' + $2; value = $1 + '@' + abbr($2); - return link(position, href, [text(position, value)]); + return eat($0)(this.renderLink(true, href, value, null, now, eat)); } -}; +} + +ghUserSHA.notInLink = true; /** * Render a SHA. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - SHA. * @return {Node?} */ -augment.sha = function (position, repo, $0, $1) { +function ghSha(eat, $0, $1) { + var now = eat.now(); var href; if (isSHA($1)) { - href = gh(repo) + 'commit/' + $1; + href = gh(this.github) + 'commit/' + $1; - return link(position, href, [text(position, abbr($0))]); + return eat($0)(this.renderLink(true, href, abbr($0), null, now, eat)); } -}; +} + +ghSha.notInLink = true; /** * Render an issue relative to a repo. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Project. * @param {Object} $3 - Issue number. * @return {Node} */ -augment.repoIssue = function (position, repo, $0, $1, $2, $3) { +function ghRepoIssue(eat, $0, $1, $2, $3) { + var now = eat.now(); var href = gh($1, $2) + 'issues/' + $3; - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghRepoIssue.notInLink = true; /** * Render an issue relative to a user. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Issue number. * @return {Node} */ -augment.userIssue = function (position, repo, $0, $1, $2) { - var href = gh($1, repo.project) + 'issues/' + $2; +function ghUserIssue(eat, $0, $1, $2) { + var now = eat.now(); + var href = gh($1, this.github.project) + 'issues/' + $2; - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghUserIssue.notInLink = true; /** * Render an issue. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Issue number. * @return {Node} */ -augment.issue = function (position, repo, $0, $1) { - var href = gh(repo) + 'issues/' + $1; - - return link(position, href, [text(position, $0)]); -}; +function ghIssue(eat, $0, $1) { + var now = eat.now(); + var href = gh(this.github) + 'issues/' + $1; -var OVERWRITES = {}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} -OVERWRITES.mentions = OVERWRITES.mention = 'blog/821'; +ghIssue.notInLink = true; /** * Render a mention. * - * @param {Object} position - * @param {Object} repo + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @return {Node} */ -augment.mention = function (position, repo, $0, $1) { +function ghMention(eat, $0, $1) { + var now = eat.now(); var href = gh() + (has.call(OVERWRITES, $1) ? OVERWRITES[$1] : $1); - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghMention.notInLink = true; /** - * Construct a transformer + * Factory to parse plain-text, and look for github + * entities. * - * @param {Object} repo - * @return {function(node)} + * @param {Object} repo - User/project object. + * @return {Function} - Tokenizer. */ -function transformerFactory(repo) { +function inlineTextFactory(repo) { /** - * Adds an example section based on a valid example - * JavaScript document to a `Usage` section. + * Factory to parse plain-text, and look for github + * entities. * - * @param {Node} node + * @param {Function} eat + * @param {string} $0 - Content. + * @return {Array.} */ - return function (node) { - /** - * Replace a text node with results from `augment`. - * - * @param {Node} child - * @param {number} position - * @param {Array.} children - */ - function replace(child, position, children) { - splice.apply(children, [position, 1].concat( - augment(child, repo) - )); - } + function inlineText(eat, $0) { + var self = this; + var now = eat.now(); - var visit; - var visitAll; - - /** - * Visit `node`. Returns zero or more text blocks. - * - * @param {Node} child - */ - visit = function (child, position, children) { - if (isText(child)) { - replace(child, position, children); - } else if ('children' in child && child.type !== 'link') { - visitAll(child.children); - } - }; + self.github = repo; - /** - * Visit all `children`. Returns a single nested - * array. - * - * @param {Array.} children - */ - visitAll = function (children) { - children.map(visit); - }; + return eat($0)(self.augmentGitHub($0, now)); + } - visit(node); - }; + return inlineText; } /** * Attacher. * - * @param {MDAST} _ - * @param {Object?} options - * @return {function(node)} + * @param {MDAST} mdast + * @param {Object?} [options] */ -function attacher(_, options) { +function attacher(mdast, options) { var repo = (options || {}).repository; + var proto = mdast.Parser.prototype; + var scope = proto.inlineTokenizers; + var current = scope.inlineText; var pack; + /* + * Get the repo from `package.json`. + */ + if (!repo) { try { pack = require(require('path').resolve( @@ -522,18 +365,62 @@ function attacher(_, options) { repo = pack.repository ? pack.repository.url || pack.repository : ''; } - repo = EXPRESSIONS_REPO.exec(repo); + /* + * Parse the URL. + * See the tests for all possible URL kinds. + */ + + repo = REPOSITORY.exec(repo); - EXPRESSIONS_REPO.lastIndex = 0; + REPOSITORY.lastIndex = 0; if (!repo) { throw new Error('Missing `repository` field in `options`'); } - return transformerFactory({ + repo = { 'user': repo[1], 'project': repo[2] - }); + }; + + /* + * Add a tokenizer to the `Parser`. + */ + + proto.augmentGitHub = proto.tokenizeFactory('gh'); + + /* + * Copy tokenizers, expressions, and methods. + */ + + proto.ghMethods = order.concat(); + + proto.ghTokenizers = { + 'ghSha': ghSha, + 'ghUserSHA': ghUserSHA, + 'ghRepoSHA': ghRepoSHA, + 'ghRepoIssue': ghRepoIssue, + 'ghUserIssue': ghUserIssue, + 'ghIssue': ghIssue, + 'ghMention': ghMention + }; + + proto.expressions.gfm.ghSha = expressions.ghSha; + proto.expressions.gfm.ghUserSHA = expressions.ghUserSHA; + proto.expressions.gfm.ghRepoSHA = expressions.ghRepoSHA; + proto.expressions.gfm.ghIssue = expressions.ghIssue; + proto.expressions.gfm.ghUserIssue = expressions.ghUserIssue; + proto.expressions.gfm.ghRepoIssue = expressions.ghRepoIssue; + proto.expressions.gfm.ghMention = expressions.ghMention; + + /* + * Overwrite `inlineText`. + */ + + proto.ghMethods.push('ghText'); + proto.ghTokenizers.ghText = current; + proto.expressions.gfm.ghText = NON_GITHUB; + scope.inlineText = inlineTextFactory(repo); } /* diff --git a/mdast-github.js b/mdast-github.js index 34b76b8..fdd43bb 100644 --- a/mdast-github.js +++ b/mdast-github.js @@ -6,14 +6,63 @@ * Cached method. */ -var splice = Array.prototype.splice; var has = Object.prototype.hasOwnProperty; /* * Hide process use from browserify. */ + var proc = typeof global !== 'undefined' && global.process; +/* + * Blacklist of SHAs which are also valid words. + * + * GitHub allows abbreviating SHAs up to 7 characters. + * These are ignored in text because they might just be + * ment as normal words. If you’d like these to link to + * their SHAs, just use more than 7 characters. + * + * Generated by: + * + * egrep -i "^[a-f0-9]{7,}$" /usr/share/dict/words + */ + +var BLACKLIST = [ + 'deedeed', + 'fabaceae' +]; + +/** + * Check if a value is a SHA. + * + * @param {string} sha + * @return {boolean} + */ +function isSHA(sha) { + return BLACKLIST.indexOf(sha.toLowerCase()) === -1; +} + +/** + * Abbreviate a SHA. + * + * @param {string} sha + * @return {string} + */ +function abbr(sha) { + return sha.slice(0, 7); +} + +/* + * Map of overwrites for at-mentions. + * GitHub does some fancy stuff with `@mention`, by linking + * it to their blog-post introducing the feature. + * To my knowledge, there are no other magical usernames. + */ + +var OVERWRITES = {}; + +OVERWRITES.mentions = OVERWRITES.mention = 'blog/821'; + /** * Return a URL to GitHub, relative to an optional * `repo` object, or `user` and `project`. @@ -43,48 +92,33 @@ function gh(repo, project) { * Username may only contain alphanumeric characters or * single hyphens, and cannot begin or end with a hyphen. * - * `PERSON` is either a user or a team, but also matches a team: - * https://github.com/blog/1121-introducing-team-mentions + * `PERSON` is either a user or an organization, but also + * matches a team: + * + * https://github.com/blog/1121-introducing-team-mentions */ var NAME = '(?:[a-z0-9]{1,2}|[a-z0-9][a-z0-9-]{1,37}[a-z0-9])'; var USER = '(' + NAME + ')'; var PERSON = '(' + NAME + '(?:\\/' + NAME + ')?)'; -var SHA = '([a-f0-9]{7,40})'; +var HASH = '([a-f0-9]{7,40})'; var NUMBER = '([0-9]+)'; var PROJECT = '((?:[a-z0-9-]|\\.git[a-z0-9-]|\\.(?!git))+)'; -var ISSUE = '(?:GH-|#)' + NUMBER; var REPO = USER + '\\/' + PROJECT; -var EXPRESSION_SHA = new RegExp( - '^' + SHA + '\\b', 'gi' -); - -var EXPRESSION_USER_SHA = new RegExp( - '^' + USER + '@' + SHA + '\\b', 'gi' -); - -var EXPRESSION_REPO_SHA = new RegExp( - '^' + REPO + '@' + SHA + '\\b', 'gi' -); - -var EXPRESSION_ISSUE = new RegExp( - '^' + ISSUE + '\\b', 'gi' -); - -var EXPRESSION_USER_ISSUE = new RegExp( - '^' + USER + '#' + NUMBER + '\\b', 'gi' -); +var SHA = new RegExp('^' + HASH + '\\b', 'i'); +var USER_SHA = new RegExp('^' + USER + '@' + HASH + '\\b', 'i'); +var REPO_SHA = new RegExp('^' + REPO + '@' + HASH + '\\b', 'i'); +var ISSUE = new RegExp('^(?:GH-|#)' + NUMBER + '\\b', 'i'); +var USER_ISSUE = new RegExp('^' + USER + '#' + NUMBER + '\\b', 'i'); +var REPO_ISSUE = new RegExp('^' + REPO + '#' + NUMBER + '\\b', 'i'); +var MENTION = new RegExp('^@' + PERSON + '\\b(?!-)', 'i'); -var EXPRESSION_REPO_ISSUE = new RegExp( - '^' + REPO + '#' + NUMBER + '\\b', 'gi' -); - -var EXPRESSION_MENTION = new RegExp( - '^@' + PERSON + '\\b(?!-)', 'gi' -); +/* + * Match a repo from a git / github URL. + */ -var EXPRESSIONS_REPO = new RegExp( +var REPOSITORY = new RegExp( '(?:^|/(?:repos/)?)' + REPO + '(?=\\.git|[\\/#@]|$)', 'i' ); @@ -93,238 +127,50 @@ var EXPRESSIONS_REPO = new RegExp( * references. */ -var EXPRESSIONS_NON_REFERENCE_LIKE = /[^/.@#_a-zA-Z0-9-]/; +var NON_GITHUB = /^[\s\S]+?(?:[^/.@#_a-zA-Z0-9-](?=[@#_a-zA-Z0-9-])|(?=$))/; /* * Expressions to use. */ var expressions = { - 'sha': EXPRESSION_SHA, - 'userSHA': EXPRESSION_USER_SHA, - 'repoSHA': EXPRESSION_REPO_SHA, - 'issue': EXPRESSION_ISSUE, - 'userIssue': EXPRESSION_USER_ISSUE, - 'repoIssue': EXPRESSION_REPO_ISSUE, - 'mention': EXPRESSION_MENTION + 'ghRepoSHA': REPO_SHA, + 'ghUserSHA': USER_SHA, + 'ghSha': SHA, + 'ghRepoIssue': REPO_ISSUE, + 'ghUserIssue': USER_ISSUE, + 'ghIssue': ISSUE, + 'ghMention': MENTION }; -var order = [ - 'repoSHA', - 'userSHA', - 'sha', - 'repoIssue', - 'userIssue', - 'issue', - 'mention' -]; - /* - * Blacklist of SHAs which are also valid words. - * - * GitHub allows abbreviating SHAs up to 7 characters. - * - * Generated by: - * - * egrep -i "^[a-f0-9]{7,}$" /usr/share/dict/words + * Order in which to use expressions. */ -var BLACKLIST = [ - 'deedeed', - 'fabaceae' +var order = [ + 'ghRepoSHA', + 'ghUserSHA', + 'ghSha', + 'ghRepoIssue', + 'ghUserIssue', + 'ghIssue', + 'ghMention' ]; -/** - * Check if a value is a SHA. - * - * @param {string} sha - * @return {boolean} - */ -function isSHA(sha) { - return BLACKLIST.indexOf(sha.toLowerCase()) === -1; -} - -/** - * Abbreviate a SHA. - * - * @param {string} sha - * @return {string} - */ -function abbr(sha) { - return sha.slice(0, 7); -} - -/** - * Check if a node is a text node. - * - * @param {Node} node - * @return {boolean} - */ -function isText(node) { - return node && node.type === 'text'; -} - -/** - * Render a link node. - * - * @param {Object} position - * @param {string} href - * @param {Array.} children - * @return {Node} - */ -function link(position, href, children) { - return { - 'type': 'link', - 'href': href, - 'title': null, - 'children': children, - 'position': position - }; -} - -/** - * Render a text node. - * - * @param {Object} position - * @param {string} value - * @return {Node} - */ -function text(position, value) { - return { - 'type': 'text', - 'value': value, - 'position': position - }; -} - -/** - * Find references in a text node, and return a list - * of replacement nodes. - * - * @param {Node} parent - * @param {Object} repo - * @return {Array.} - */ -function augment(parent, repo) { - var value = parent.value; - var valueLength = value.length; - var nodes = []; - var length = order.length; - var index = -1; - var offset = -1; - var node; - var name; - var match; - var subposition; - var start = 0; - var end = 0; - var position = parent.position ? parent.position.start : {}; - var line = position.line || 1; - var column = position.column || 1; - - /** - * Get the current position. - * - * @return {Object} - */ - function now() { - return { - 'line': line, - 'column': column - }; - } - - /** - * Location getter. - * - * @return {function(): Object} - */ - function location() { - var before = now(); - - /** - * Return a `position`. - * - * @return {Object} - */ - return function () { - return { - 'start': before, - 'end': now() - }; - }; - } - - position = location(); - - while (++offset < valueLength) { - index = -1; - - if ( - offset === 0 || - EXPRESSIONS_NON_REFERENCE_LIKE.test(value.charAt(offset - 1)) - ) { - while (++index < length) { - name = order[index]; - - match = expressions[name].exec(value.slice(offset)); - - expressions[name].lastIndex = 0; - - if (match) { - subposition = location(); - - end = offset; - - offset += match[0].length; - - node = augment[name].apply( - null, [subposition(), repo].concat(match) - ); - - if (node) { - if (end !== start) { - nodes.push(text( - position(), value.slice(start, end) - )); - - position = location(); - } - - start = offset; - nodes.push(node); - } - } - } - } - - if (value.charAt(index) === '\n') { - line++; - column = 0; - } - - column++; - } - - if (start < valueLength) { - nodes.push(text(position(), value.slice(start, offset))); - } - - return nodes; -} - /** * Render a SHA relative to a repo. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Project. * @param {Object} $3 - SHA. * @return {Node?} */ -augment.repoSHA = function (position, repo, $0, $1, $2, $3) { +function ghRepoSHA(eat, $0, $1, $2, $3) { + var now = eat.now(); var href; var value; @@ -332,186 +178,183 @@ augment.repoSHA = function (position, repo, $0, $1, $2, $3) { href = gh($1, $2) + 'commit/' + $3; value = $1 + '/' + $2 + '@' + abbr($3); - return link(position, href, [text(position, value)]); + return eat($0)(this.renderLink(true, href, value, null, now, eat)); } -}; +} + +ghRepoSHA.notInLink = true; /** * Render a SHA relative to a user. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - SHA. * @return {Node?} */ -augment.userSHA = function (position, repo, $0, $1, $2) { +function ghUserSHA(eat, $0, $1, $2) { + var now = eat.now(); var href; var value; if (isSHA($2)) { - href = gh($1, repo.project) + 'commit/' + $2; + href = gh($1, this.github.project) + 'commit/' + $2; value = $1 + '@' + abbr($2); - return link(position, href, [text(position, value)]); + return eat($0)(this.renderLink(true, href, value, null, now, eat)); } -}; +} + +ghUserSHA.notInLink = true; /** * Render a SHA. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - SHA. * @return {Node?} */ -augment.sha = function (position, repo, $0, $1) { +function ghSha(eat, $0, $1) { + var now = eat.now(); var href; if (isSHA($1)) { - href = gh(repo) + 'commit/' + $1; + href = gh(this.github) + 'commit/' + $1; - return link(position, href, [text(position, abbr($0))]); + return eat($0)(this.renderLink(true, href, abbr($0), null, now, eat)); } -}; +} + +ghSha.notInLink = true; /** * Render an issue relative to a repo. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Project. * @param {Object} $3 - Issue number. * @return {Node} */ -augment.repoIssue = function (position, repo, $0, $1, $2, $3) { +function ghRepoIssue(eat, $0, $1, $2, $3) { + var now = eat.now(); var href = gh($1, $2) + 'issues/' + $3; - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghRepoIssue.notInLink = true; /** * Render an issue relative to a user. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @param {Object} $2 - Issue number. * @return {Node} */ -augment.userIssue = function (position, repo, $0, $1, $2) { - var href = gh($1, repo.project) + 'issues/' + $2; +function ghUserIssue(eat, $0, $1, $2) { + var now = eat.now(); + var href = gh($1, this.github.project) + 'issues/' + $2; - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghUserIssue.notInLink = true; /** * Render an issue. * - * @param {Object} position - * @param {Object} repo + * @property {boolean} notInLink + * @this {Parser} + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Issue number. * @return {Node} */ -augment.issue = function (position, repo, $0, $1) { - var href = gh(repo) + 'issues/' + $1; - - return link(position, href, [text(position, $0)]); -}; +function ghIssue(eat, $0, $1) { + var now = eat.now(); + var href = gh(this.github) + 'issues/' + $1; -var OVERWRITES = {}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} -OVERWRITES.mentions = OVERWRITES.mention = 'blog/821'; +ghIssue.notInLink = true; /** * Render a mention. * - * @param {Object} position - * @param {Object} repo + * @param {Function} eat * @param {string} $0 - Whole content. * @param {Object} $1 - Username. * @return {Node} */ -augment.mention = function (position, repo, $0, $1) { +function ghMention(eat, $0, $1) { + var now = eat.now(); var href = gh() + (has.call(OVERWRITES, $1) ? OVERWRITES[$1] : $1); - return link(position, href, [text(position, $0)]); -}; + return eat($0)(this.renderLink(true, href, $0, null, now, eat)); +} + +ghMention.notInLink = true; /** - * Construct a transformer + * Factory to parse plain-text, and look for github + * entities. * - * @param {Object} repo - * @return {function(node)} + * @param {Object} repo - User/project object. + * @return {Function} - Tokenizer. */ -function transformerFactory(repo) { +function inlineTextFactory(repo) { /** - * Adds an example section based on a valid example - * JavaScript document to a `Usage` section. + * Factory to parse plain-text, and look for github + * entities. * - * @param {Node} node + * @param {Function} eat + * @param {string} $0 - Content. + * @return {Array.} */ - return function (node) { - /** - * Replace a text node with results from `augment`. - * - * @param {Node} child - * @param {number} position - * @param {Array.} children - */ - function replace(child, position, children) { - splice.apply(children, [position, 1].concat( - augment(child, repo) - )); - } + function inlineText(eat, $0) { + var self = this; + var now = eat.now(); - var visit; - var visitAll; - - /** - * Visit `node`. Returns zero or more text blocks. - * - * @param {Node} child - */ - visit = function (child, position, children) { - if (isText(child)) { - replace(child, position, children); - } else if ('children' in child && child.type !== 'link') { - visitAll(child.children); - } - }; + self.github = repo; - /** - * Visit all `children`. Returns a single nested - * array. - * - * @param {Array.} children - */ - visitAll = function (children) { - children.map(visit); - }; + return eat($0)(self.augmentGitHub($0, now)); + } - visit(node); - }; + return inlineText; } /** * Attacher. * - * @param {MDAST} _ - * @param {Object?} options - * @return {function(node)} + * @param {MDAST} mdast + * @param {Object?} [options] */ -function attacher(_, options) { +function attacher(mdast, options) { var repo = (options || {}).repository; + var proto = mdast.Parser.prototype; + var scope = proto.inlineTokenizers; + var current = scope.inlineText; var pack; + /* + * Get the repo from `package.json`. + */ + if (!repo) { try { pack = require(require('path').resolve( @@ -524,18 +367,62 @@ function attacher(_, options) { repo = pack.repository ? pack.repository.url || pack.repository : ''; } - repo = EXPRESSIONS_REPO.exec(repo); + /* + * Parse the URL. + * See the tests for all possible URL kinds. + */ + + repo = REPOSITORY.exec(repo); - EXPRESSIONS_REPO.lastIndex = 0; + REPOSITORY.lastIndex = 0; if (!repo) { throw new Error('Missing `repository` field in `options`'); } - return transformerFactory({ + repo = { 'user': repo[1], 'project': repo[2] - }); + }; + + /* + * Add a tokenizer to the `Parser`. + */ + + proto.augmentGitHub = proto.tokenizeFactory('gh'); + + /* + * Copy tokenizers, expressions, and methods. + */ + + proto.ghMethods = order.concat(); + + proto.ghTokenizers = { + 'ghSha': ghSha, + 'ghUserSHA': ghUserSHA, + 'ghRepoSHA': ghRepoSHA, + 'ghRepoIssue': ghRepoIssue, + 'ghUserIssue': ghUserIssue, + 'ghIssue': ghIssue, + 'ghMention': ghMention + }; + + proto.expressions.gfm.ghSha = expressions.ghSha; + proto.expressions.gfm.ghUserSHA = expressions.ghUserSHA; + proto.expressions.gfm.ghRepoSHA = expressions.ghRepoSHA; + proto.expressions.gfm.ghIssue = expressions.ghIssue; + proto.expressions.gfm.ghUserIssue = expressions.ghUserIssue; + proto.expressions.gfm.ghRepoIssue = expressions.ghRepoIssue; + proto.expressions.gfm.ghMention = expressions.ghMention; + + /* + * Overwrite `inlineText`. + */ + + proto.ghMethods.push('ghText'); + proto.ghTokenizers.ghText = current; + proto.expressions.gfm.ghText = NON_GITHUB; + scope.inlineText = inlineTextFactory(repo); } /* diff --git a/mdast-github.min.js b/mdast-github.min.js index 4cb9743..4eb3b78 100644 --- a/mdast-github.min.js +++ b/mdast-github.min.js @@ -1 +1 @@ -!function(b,a){typeof exports==='object'&&typeof module!=='undefined'?module.exports=b():typeof define==='function'&&define.amd?define([],b):(typeof window!=='undefined'?a=window:typeof global!=='undefined'?a=global:typeof self!=='undefined'?a=self:a=this,a.mdastGitHub=b())}(function(){return function a(b,c,e){function f(d,k){if(!c[d]){if(!b[d]){var i=typeof require=='function'&&require;if(!k&&i)return i(d,!0);if(g)return g(d,!0);var j=new Error("Cannot find module '"+d+"'");throw j.code='MODULE_NOT_FOUND',j}var h=c[d]={exports:{}};b[d][0].call(h.exports,function(c){var a=b[d][1][c];return f(a?a:c)},h,h.exports,a,b,c,e)}return c[d].exports}var g=typeof require=='function'&&require;for(var d=0;d