From 66a92d2e08218c80f69708d4e544a0d3e1040f6e Mon Sep 17 00:00:00 2001 From: nfrasser Date: Thu, 1 Jan 2015 22:00:49 -0500 Subject: [PATCH 01/67] Initial commit of basic Linkify 2.0 Current API just includes the `find` method, which takes a string of text and returns the kinds of links available in it. More methods to come soon. Also included are Mocha unit tests for the major lexical analysis components. --- .gitignore | 34 ++++ .jshintrc | 32 ++-- Gruntfile.js | 181 ------------------ bower.json | 38 ++-- dist/jquery.linkify.js | 88 --------- dist/jquery.linkify.min.js | 9 - gulpfile.js | 70 +++++++ index.js | 1 + package.json | 41 ++-- src/jquery.linkify.js | 73 ------- src/linkified.js | 346 ---------------------------------- src/linkify.js | 40 ++++ src/parser/index.js | 291 ++++++++++++++++++++++++++++ src/parser/state.js | 28 +++ src/scanner/index.js | 179 ++++++++++++++++++ src/scanner/state.js | 31 +++ src/scanner/stateify.js | 54 ++++++ src/scanner/tlds.js | 12 ++ src/state/base.js | 116 ++++++++++++ src/tokens/multi.js | 214 +++++++++++++++++++++ src/tokens/text.js | 204 ++++++++++++++++++++ test/benchmarks.js | 36 ++++ test/index.js | 5 + test/spec/parser/index.js | 113 +++++++++++ test/spec/parser/state.js | 28 +++ test/spec/scanner/index.js | 85 +++++++++ test/spec/scanner/state.js | 67 +++++++ test/spec/scanner/stateify.js | 54 ++++++ test/spec/tokens/multi.js | 200 ++++++++++++++++++++ test/spec/tokens/text.js | 47 +++++ tests/js/linkified.js | 94 --------- tests/linkified.html | 18 -- 32 files changed, 1954 insertions(+), 875 deletions(-) delete mode 100644 Gruntfile.js delete mode 100644 dist/jquery.linkify.js delete mode 100644 dist/jquery.linkify.min.js create mode 100644 gulpfile.js create mode 100644 index.js delete mode 100644 src/jquery.linkify.js delete mode 100644 src/linkified.js create mode 100644 src/linkify.js create mode 100644 src/parser/index.js create mode 100644 src/parser/state.js create mode 100644 src/scanner/index.js create mode 100644 src/scanner/state.js create mode 100644 src/scanner/stateify.js create mode 100644 src/scanner/tlds.js create mode 100644 src/state/base.js create mode 100644 src/tokens/multi.js create mode 100644 src/tokens/text.js create mode 100644 test/benchmarks.js create mode 100644 test/index.js create mode 100644 test/spec/parser/index.js create mode 100644 test/spec/parser/state.js create mode 100644 test/spec/scanner/index.js create mode 100644 test/spec/scanner/state.js create mode 100644 test/spec/scanner/stateify.js create mode 100644 test/spec/tokens/multi.js create mode 100644 test/spec/tokens/text.js delete mode 100644 tests/js/linkified.js delete mode 100644 tests/linkified.html diff --git a/.gitignore b/.gitignore index f605c5fc..062b2cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,38 @@ Thumbs.DB node_modules bower_components build/* +dist/* demo/dist/* + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Build files +build +dist + diff --git a/.jshintrc b/.jshintrc index cad9eb97..5aede48e 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,21 +1,13 @@ { - "boss": true, - "curly": true, - "eqeqeq": true, - "eqnull": true, - "expr": true, - "immed": false, - "noarg": true, - "smarttabs": true, - "trailing": true, - "unused": true, - "strict": false, - "node": true, - "browser": true, - "jquery": true, - "globals": { - "prettyPrint": false, - "jQuery": false, - "Linkified": false - } -} \ No newline at end of file + "esnext": true, + "globalstrict": false, + "node": true, + "globals": { + "describe": false, + "it": false, + "before": false, + "beforeEach": false, + "after": false, + "afterEach": false + } +} diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index a283140d..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,181 +0,0 @@ -module.exports = function (grunt) { - - "use strict"; - - grunt.initConfig({ - - // Import package manifest - pkg: grunt.file.readJSON("package.json"), - - // Banner definitions - meta: { - banner: "/*\n" + - " * <%= pkg.title || pkg.name %> - v<%= pkg.version %>\n" + - " * <%= pkg.description %>\n" + - " * <%= pkg.homepage %>\n" + - " *\n" + - " * Made by <%= pkg.author %>\n" + - " * Under <%= pkg.license %> License\n" + - " */\n" - }, - - // Concat definitions - concat: { - jquery: { - files: { - "build/jquery.linkify.js": [ - "src/linkified.js", - "src/jquery.linkify.js" - ] - } - } - }, - - // Wrap files - wrap: { - jquery: { - expand: true, - src: ['build/jquery.linkify.js'], - dest: 'build/', - options: { - wrapper: [ - '<%= meta.banner %>\n;(function ($, window, document, undefined) {\n"use strict";', - '})(jQuery, window, document)\n' - ] - } - } - }, - - // Lint definitions - jshint: { - files: ["src/*"], - options: { - jshintrc: ".jshintrc" - } - }, - - // Run test suite - qunit: { - all: { - options: { - urls: [ - 'http://localhost:8001/tests/linkified.html', - ] - } - } - }, - - // Minify definitions - uglify: { - min: { - src: ["build/jquery.linkify.js"], - dest: "dist/jquery.linkify.min.js" - }, - max: { - src: ["build/jquery.linkify.js"], - dest: "dist/jquery.linkify.js", - options: { - beautify: true, - mangle: false - } - }, - options: { - banner: "<%= meta.banner %>" - } - }, - - copy: { - build: { - expand: true, - flatten: true, - src: "build/build/*", - dest: "build/" - }, - demo: { - src: "dist/*", - dest: "demo/" - }, - }, - - connect: { - demo: { - options: { - base: 'demo/', - open: true, - keepalive: true - } - }, - test: { - options: { - port: 8001 - } - } - }, - - "gh-pages": { - options: { - base: "demo" - }, - src: ["**"] - }, - - bumper: { - options: { - tasks: [ - "default" - ] - }, - push: { - files: [ - "package.json", - "bower.json" - ], - updateConfigs: ["pkg"], - releaseBranch: ["master"] - } - }, - - clean: [ - ".grunt/grunt-gh-pages/gh-pages", - "build/*" - ] - - }); - - grunt.loadNpmTasks("grunt-contrib-copy"); - grunt.loadNpmTasks("grunt-contrib-concat"); - grunt.loadNpmTasks("grunt-contrib-jshint"); - grunt.loadNpmTasks("grunt-contrib-uglify"); - grunt.loadNpmTasks("grunt-contrib-connect"); - grunt.loadNpmTasks("grunt-contrib-clean"); - grunt.loadNpmTasks("grunt-contrib-qunit"); - grunt.loadNpmTasks("grunt-wrap"); - grunt.loadNpmTasks("grunt-gh-pages"); - grunt.loadNpmTasks("grunt-bumper"); - - grunt.registerTask("default", [ - "jshint", - "concat", - "wrap", - "copy:build", - "uglify", - "copy:demo", - "clean" - ]); - - grunt.registerTask("test", [ - "connect:test", - "jshint", - "qunit" - ]); - - grunt.registerTask("demo", [ - 'default', - 'connect:demo' - ]); - - grunt.registerTask("release", ["bumper", "clean"]); - grunt.registerTask("release:minor", ["bumper:minor", "clean"]); - grunt.registerTask("release:major", ["bumper:major", "clean"]); - -}; diff --git a/bower.json b/bower.json index 3a8429ab..8a95f011 100644 --- a/bower.json +++ b/bower.json @@ -1,37 +1,31 @@ { - "name": "jQuery-linkify", - "version": "1.1.7", - "homepage": "https://github.com/SoapBox/jQuery-linkify/", + "name": "linkify", + "main": "index.js", + "version": "2.0.0", "authors": [ - "SoapBox Innovations, Inc. " + "SoapBox Innovations Inc. " ], - "description": "Find URLs in plain text and return HTML for discovered links.", - "main": "dist/jquery.linkify.js", + "description": "Find links in plain text", "keywords": [ + "node", + "js", "jquery", - "plugin", - "boilerplate", - "jquery-plugin", - "jquery-boilerplate", "link", + "autolink", + "text", "url", - "match", - "html" + "email" ], "license": "MIT", - "dependencies": { - "jquery": ">=1.9.0" - }, - "devDependencies": { - "qunit": "1.*" - }, + "homepage": "http://soapbox.github.io/jQuery-linkify/", "ignore": [ "**/.*", "node_modules", "bower_components", "test", - "tests", - "demo", - "assets" - ] + "tests" + ], + "dependencies": { + "jquery": "~2.1.3" + } } diff --git a/dist/jquery.linkify.js b/dist/jquery.linkify.js deleted file mode 100644 index e58bdc13..00000000 --- a/dist/jquery.linkify.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Linkify - v1.1.7 - * Find URLs in plain text and return HTML for discovered links. - * https://github.com/HitSend/jQuery-linkify/ - * - * Made by SoapBox Innovations, Inc. - * Under MIT License - */ -!function($, window, document) { - "use strict"; - function Linkified(element, options) { - this._defaults = defaults, this.element = element, this.setOptions(options), this.init(); - } - var defaults = { - tagName: "a", - newLine: "\n", - target: "_blank", - linkClass: null, - linkClasses: [], - linkAttributes: null - }; - Linkified.prototype = { - constructor: Linkified, - init: function() { - 1 === this.element.nodeType ? Linkified.linkifyNode.call(this, this.element) : this.element = Linkified.linkify.call(this, this.element.toString()); - }, - setOptions: function(options) { - this.settings = Linkified.extendSettings(options, this.settings); - }, - toString: function() { - return this.element.toString(); - } - }, Linkified.extendSettings = function(options, settings) { - var prop; - settings = settings || {}; - for (prop in defaults) settings[prop] || (settings[prop] = defaults[prop]); - for (prop in options) settings[prop] = options[prop]; - return settings; - }, Linkified.linkMatch = new RegExp([ "(", '\\s|[^a-zA-Z0-9.\\+_\\/"\\>\\-]|^', ")(?:", "(", "[a-zA-Z0-9\\+_\\-]+", "(?:", "\\.[a-zA-Z0-9\\+_\\-]+", ")*@", ")?(", "http:\\/\\/|https:\\/\\/|ftp:\\/\\/", ")?(", "(?:(?:[a-z0-9][a-z0-9_%\\-_+]*\\.)+)", ")(", "(?:com|ca|co|edu|gov|net|org|dev|biz|cat|int|pro|tel|mil|aero|asia|coop|info|jobs|mobi|museum|name|post|travel|local|[a-z]{2})", ")(", "(?::\\d{1,5})", ")?(", "(?:", "[\\/|\\?]", "(?:", "[\\-a-zA-Z0-9_%#*&+=~!?,;:.\\/]*", ")*", ")", "[\\-\\/a-zA-Z0-9_%#*&+=~]", "|", "\\/?", ")?", ")(", '[^a-zA-Z0-9\\+_\\/"\\<\\-]|$', ")" ].join(""), "g"), - Linkified.emailLinkMatch = /(<[a-z]+ href=\")(http:\/\/)([a-zA-Z0-9\+_\-]+(?:\.[a-zA-Z0-9\+_\-]+)*@)/g, - Linkified.linkify = function(text, options) { - var attr, settings, linkClasses, linkReplace = []; - this.constructor === Linkified && this.settings ? (settings = this.settings, options && (settings = Linkified.extendSettings(options, settings))) : settings = Linkified.extendSettings(options), - linkClasses = settings.linkClass ? settings.linkClass.split(/\s+/) : [], linkClasses.push.apply(linkClasses, settings.linkClasses), - text = text.replace(/$2$3$4$5$6$7$8"), text = text.replace(Linkified.linkMatch, linkReplace.join(" ")), - text = text.replace(Linkified.emailLinkMatch, "$1mailto:$3"), text = text.replace(/(\s){2}/g, "$1"), - text = text.replace(/\n/g, settings.newLine); - }, Linkified.linkifyNode = function(node) { - var children, childNode, childCount, dummyElement, i; - if (node && "object" == typeof node && 1 === node.nodeType && "a" !== node.tagName.toLowerCase() && !/[^\s]linkified[\s$]/.test(node.className)) { - for (children = [], dummyElement = Linkified._dummyElement || document.createElement("div"), - childNode = node.firstChild, childCount = node.childElementCount; childNode; ) { - if (3 === childNode.nodeType) { - for (;dummyElement.firstChild; ) dummyElement.removeChild(dummyElement.firstChild); - for (dummyElement.innerHTML = Linkified.linkify.call(this, childNode.textContent || childNode.innerText || childNode.nodeValue), - children.push.apply(children, dummyElement.childNodes); dummyElement.firstChild; ) dummyElement.removeChild(dummyElement.firstChild); - } else children.push(1 === childNode.nodeType ? Linkified.linkifyNode.call(this, childNode) : childNode); - childNode = childNode.nextSibling; - } - for (;node.firstChild; ) node.removeChild(node.firstChild); - for (i = 0; i < children.length; i++) node.appendChild(children[i]); - } - return node; - }, Linkified._dummyElement = document.createElement("div"), $.fn.linkify = function(options) { - return this.each(function() { - var linkified; - (linkified = $.data(this, "plugin-linkify")) ? (linkified.setOptions(options), linkified.init()) : $.data(this, "plugin-linkify", new Linkified(this, options)); - }); - }, $.fn.linkify.Constructor = Linkified, $(window).on("load", function() { - $("[data-linkify]").each(function() { - var $target, $this = $(this), target = $this.attr("data-linkify"), options = { - tagName: $this.attr("data-linkify-tagname"), - newLine: $this.attr("data-linkify-newline"), - target: $this.attr("data-linkify-target"), - linkClass: $this.attr("data-linkify-linkclass") - }; - for (var option in options) "undefined" == typeof options[option] && delete options[option]; - $target = "this" === target ? $this : $this.find(target), $target.linkify(options); - }); - }), $("body").on("click", ".linkified", function() { - var $link = $(this), url = $link.attr("href"), isEmail = /^mailto:/i.test(url), target = $link.attr("target"); - return isEmail ? window.location.href = url : window.open(url, target), !1; - }); -}(jQuery, window, document); \ No newline at end of file diff --git a/dist/jquery.linkify.min.js b/dist/jquery.linkify.min.js deleted file mode 100644 index 91acf3a4..00000000 --- a/dist/jquery.linkify.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Linkify - v1.1.7 - * Find URLs in plain text and return HTML for discovered links. - * https://github.com/HitSend/jQuery-linkify/ - * - * Made by SoapBox Innovations, Inc. - * Under MIT License - */ -!function(a,b,c){"use strict";function d(a,b){this._defaults=e,this.element=a,this.setOptions(b),this.init()}var e={tagName:"a",newLine:"\n",target:"_blank",linkClass:null,linkClasses:[],linkAttributes:null};d.prototype={constructor:d,init:function(){1===this.element.nodeType?d.linkifyNode.call(this,this.element):this.element=d.linkify.call(this,this.element.toString())},setOptions:function(a){this.settings=d.extendSettings(a,this.settings)},toString:function(){return this.element.toString()}},d.extendSettings=function(a,b){var c;b=b||{};for(c in e)b[c]||(b[c]=e[c]);for(c in a)b[c]=a[c];return b},d.linkMatch=new RegExp(["(",'\\s|[^a-zA-Z0-9.\\+_\\/"\\>\\-]|^',")(?:","(","[a-zA-Z0-9\\+_\\-]+","(?:","\\.[a-zA-Z0-9\\+_\\-]+",")*@",")?(","http:\\/\\/|https:\\/\\/|ftp:\\/\\/",")?(","(?:(?:[a-z0-9][a-z0-9_%\\-_+]*\\.)+)",")(","(?:com|ca|co|edu|gov|net|org|dev|biz|cat|int|pro|tel|mil|aero|asia|coop|info|jobs|mobi|museum|name|post|travel|local|[a-z]{2})",")(","(?::\\d{1,5})",")?(","(?:","[\\/|\\?]","(?:","[\\-a-zA-Z0-9_%#*&+=~!?,;:.\\/]*",")*",")","[\\-\\/a-zA-Z0-9_%#*&+=~]","|","\\/?",")?",")(",'[^a-zA-Z0-9\\+_\\/"\\<\\-]|$',")"].join(""),"g"),d.emailLinkMatch=/(<[a-z]+ href=\")(http:\/\/)([a-zA-Z0-9\+_\-]+(?:\.[a-zA-Z0-9\+_\-]+)*@)/g,d.linkify=function(a,b){var c,e,f,g=[];this.constructor===d&&this.settings?(e=this.settings,b&&(e=d.extendSettings(b,e))):e=d.extendSettings(b),f=e.linkClass?e.linkClass.split(/\s+/):[],f.push.apply(f,e.linkClasses),a=a.replace(/$2$3$4$5$6$7$8"),a=a.replace(d.linkMatch,g.join(" ")),a=a.replace(d.emailLinkMatch,"$1mailto:$3"),a=a.replace(/(\s){2}/g,"$1"),a=a.replace(/\n/g,e.newLine)},d.linkifyNode=function(a){var b,e,f,g,h;if(a&&"object"==typeof a&&1===a.nodeType&&"a"!==a.tagName.toLowerCase()&&!/[^\s]linkified[\s$]/.test(a.className)){for(b=[],g=d._dummyElement||c.createElement("div"),e=a.firstChild,f=a.childElementCount;e;){if(3===e.nodeType){for(;g.firstChild;)g.removeChild(g.firstChild);for(g.innerHTML=d.linkify.call(this,e.textContent||e.innerText||e.nodeValue),b.push.apply(b,g.childNodes);g.firstChild;)g.removeChild(g.firstChild)}else b.push(1===e.nodeType?d.linkifyNode.call(this,e):e);e=e.nextSibling}for(;a.firstChild;)a.removeChild(a.firstChild);for(h=0;h= 0; + }; +} + +/** + ES6 ~> ES5 +*/ +gulp.task('transpile', function () { + + gulp.src(paths.src) + .pipe(es6transpiler()) + .pipe(gulp.dest('build')); + +}); + +/** + Lint using jshint +*/ +gulp.task('jshint', function () { + gulp.src([paths.src, paths.test, paths.spec]) + .pipe(jshint()) + .pipe(jshint.reporter(stylish)) + .pipe(jshint.reporter('fail')); +}); + +/** + Run mocha tests +*/ +gulp.task('mocha', function () { + gulp.src(paths.test, {read: false}) + .pipe(mocha()); +}); + +gulp.task('uglify', function () { + gulp.src('build/parser/index.js') + .pipe(uglify()) + .pipe(gulp.dest('dist')); +}); + +// Build steps +gulp.task('build', ['transpile']); +gulp.task('dist', ['transpile', 'uglify']); +gulp.task('test', ['build', 'jshint', 'mocha']); + +/** + Build app and begin watching for changes +*/ +gulp.task('default', ['build'], function () { + gulp.watch(paths.src, ['transpile']); +}); diff --git a/index.js b/index.js new file mode 100644 index 00000000..1c33e0bc --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./build/linkify'); diff --git a/package.json b/package.json index 342c122d..6ccfde02 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,23 @@ { - "name": "jQuery-linkify", - "title": "Linkify", - "description": "Find URLs in plain text and return HTML for discovered links.", - "author": "SoapBox Innovations, Inc.", - "repository": { - "type": "git", - "url": "http://github.com/HitSend/jQuery-linkify.git" + "name": "linkify", + "version": "2.0.0", + "description": "Intelligent URL recognition, made easy", + "main": "index.js", + "scripts": { + "prepublish": "node_modules/.bin/gulp build", + "test": "node_modules/.bin/gulp test" }, - "homepage": "https://github.com/HitSend/jQuery-linkify/", - "version": "1.1.7", + "author": "SoapBox Innovations (@SoapBoxHQ)", "license": "MIT", "devDependencies": { - "grunt": "0.4.x", - "grunt-cli": "0.1.x", - "grunt-contrib-jshint": "0.x", - "grunt-contrib-concat": "0.x", - "grunt-contrib-uglify": "0.x", - "grunt-contrib-clean": "0.x", - "grunt-contrib-connect": "0.x", - "grunt-contrib-copy": "0.x", - "grunt-wrap": "0.x", - "grunt-gh-pages": "0.x", - "grunt-bumper": "1.0.x", - "grunt-contrib-qunit": "0.5.x" + "chai": "^1.10.0", + "glob": "^4.3.2", + "gulp": "^3.8.10", + "gulp-es6-transpiler": "^1.0.1", + "gulp-jshint": "^1.9.0", + "gulp-mocha": "^2.0.0", + "gulp-uglify": "^1.0.2", + "jshint-stylish": "^1.0.0" }, - "scripts": { - "test": "grunt test --verbose" - } + "dependencies": {} } diff --git a/src/jquery.linkify.js b/src/jquery.linkify.js deleted file mode 100644 index ecc243bd..00000000 --- a/src/jquery.linkify.js +++ /dev/null @@ -1,73 +0,0 @@ -// Plugin definition -$.fn.linkify = function (options) { - return this.each(function () { - - var linkified; - - if (linkified = $.data(this, 'plugin-linkify')) { - - // Relinkify - linkified.setOptions(options); - linkified.init(); - - } else { - - // Linkify - $.data( - this, - 'plugin-linkify', - new Linkified(this, options) - ); - - } - }); -}; - -// Maintain access to the constructor from the plugin -$.fn.linkify.Constructor = Linkified; - -// DOM data- API setup -$(window).on('load', function () { - $('[data-linkify]').each(function () { - var $this = $(this), - $target, - target = $this.attr('data-linkify'), - options = { - tagName: $this.attr('data-linkify-tagname'), - newLine: $this.attr('data-linkify-newline'), - target: $this.attr('data-linkify-target'), - linkClass: $this.attr('data-linkify-linkclass') - }; - - // Delete undefined options - for (var option in options) { - if (typeof options[option] === 'undefined') { - delete options[option]; - } - } - - $target = target === 'this' ? $this : $this.find(target); - $target.linkify(options); - - }); -}); - -// Setup click events for linkified elements -$('body').on('click', '.linkified', function () { - var $link = $(this), - url = $link.attr('href'), - isEmail = /^mailto:/i.test(url), - target = $link.attr('target'); - - if (isEmail) { - - // mailto links ignore the target - window.location.href = url; - - } else { - window.open(url, target); - } - - return false; -}); - diff --git a/src/linkified.js b/src/linkified.js deleted file mode 100644 index 95f2c971..00000000 --- a/src/linkified.js +++ /dev/null @@ -1,346 +0,0 @@ -/** - A Linkified object contains a DOM node (or just plain text) whose - inner text is replaced by HTML containing `` links to URLs - discovered in that text. Call with - - new Linkified(text, options) - - Here are some the available options and their defaults - - { - tagName: 'a', - newLine: '\n', - target: '_blank', - linkClass: null, - linkClasses: [], - linkAttributes: null - } - - @class Linkified -*/ - -var defaults = { - tagName: 'a', - newLine: '\n', - target: '_blank', - linkClass: null, - linkClasses: [], - linkAttributes: null -}; - -function Linkified(element, options) { - - // Setup - this._defaults = defaults; - this.element = element; - this.setOptions(options); - this.init(); -} - -Linkified.prototype = { - - constructor: Linkified, - - /** - Initializer - @method init - */ - init: function () { - if (this.element.nodeType === 1) { - Linkified.linkifyNode.call(this, this.element); - } else { - this.element = Linkified.linkify.call( - this, - this.element.toString() - ); - } - }, - - /** - Used to reset the options for this plugin - @method setOptions - @param {Object} options - */ - setOptions: function (options) { - this.settings = Linkified.extendSettings(options, this.settings); - }, - - /** - Returns the HTML of the linkified text. - @method toString - @return {String} html - */ - toString: function () { - - // Returned the linkified HTML - return this.element.toString(); - } - - -}; - -/** - Create an extended settings object using the default options. - Include a second hash to use those as defaults instead. - @method extendSettings - @static - @param {Object} options Hash of options to use for extending - @param {Object} settings Existing settings object to extend from. If undefined, the defaults will be used -*/ -Linkified.extendSettings = function (options, settings) { - var prop; - - settings = settings || {}; - - for (prop in defaults) { - if (!settings[prop]) { - settings[prop] = defaults[prop]; - } - } - - for (prop in options) { - settings[prop] = options[prop]; - } - return settings; -}; - - -/** - The url-matching regular expression for double-spaced text - @property linkMatch - @static - @type RegExp -*/ -Linkified.linkMatch = new RegExp([ - - // The groups - '(', // 1. Character before the link - '\\s|[^a-zA-Z0-9.\\+_\\/"\\>\\-]|^', - ')(?:', //Main group - '(', // 2. Email address (optional) - '[a-zA-Z0-9\\+_\\-]+', - '(?:', - '\\.[a-zA-Z0-9\\+_\\-]+', - ')*@', - ')?(', // 3. Protocol (optional) - 'http:\\/\\/|https:\\/\\/|ftp:\\/\\/', - ')?(', // 4. Domain & Subdomains - '(?:(?:[a-z0-9][a-z0-9_%\\-_+]*\\.)+)', - ')(', // 5. Top-level domain - http://en.wikipedia.org/wiki/List_of_Internet_top-level_domains - '(?:com|ca|co|edu|gov|net|org|dev|biz|cat|int|pro|tel|mil|aero|asia|coop|info|jobs|mobi|museum|name|post|travel|local|[a-z]{2})', - ')(', // 6. Port (optional) - '(?::\\d{1,5})', - ')?(', // 7. Query string (optional) - '(?:', - '[\\/|\\?]', - '(?:', - '[\\-a-zA-Z0-9_%#*&+=~!?,;:.\\/]*', - ')*', - ')', - '[\\-\\/a-zA-Z0-9_%#*&+=~]', - '|', - '\\/?', - ')?', - ')(', // 7. Character after the link - '[^a-zA-Z0-9\\+_\\/"\\<\\-]|$', - ')' -].join(''), 'g'); - -/** - The regular expression of matching email links after the - application of the initial link matcher. - - @property emailLinkMatch - @static - @type RegExp -*/ -Linkified.emailLinkMatch = /(<[a-z]+ href=\")(http:\/\/)([a-zA-Z0-9\+_\-]+(?:\.[a-zA-Z0-9\+_\-]+)*@)/g; - - -/** - Linkify the given text - @method linkify - @param {String} text Plain text to linkify - @param {Options} options to linkify with, in addition to the defaults for the context - @return {String} html -*/ -Linkified.linkify = function (text, options) { - - var attr, - settings, - linkClasses, - linkReplace = []; - - if (this.constructor === Linkified && this.settings) { - - // Called from an instance of Linkified - settings = this.settings; - if (options) { - settings = Linkified.extendSettings(options, settings); - } - - } else { - settings = Linkified.extendSettings(options); - } - - // Normalize class names - if (settings.linkClass) { - linkClasses = settings.linkClass.split(/\s+/); - } else { - linkClasses = []; - } - - linkClasses.push.apply(linkClasses, settings.linkClasses); - - - // Get rid of tags and HTML-structure, - // Duplicate whitespace in preparation for linking - text = text - .replace(/$2$3$4$5$6$7$8'); - - // Create the link - text = text.replace(Linkified.linkMatch, linkReplace.join(' ')); - - // The previous line added `http://` to emails. Replace that with `mailto:` - text = text.replace(Linkified.emailLinkMatch, '$1mailto:$3'); - - // Revert whitespace characters back to a single character - text = text.replace(/(\s){2}/g, '$1'); - - // Trim and account for new lines - text = text.replace(/\n/g, settings.newLine); - - return text; - -}; - -/** - Given an HTML DOM node, linkify its contents - @method linkifyNode - @static - @param {Element} node The HTML node to find URLs in - @return {Element} node -*/ -Linkified.linkifyNode = function (node) { - - var children, - childNode, - childCount, - dummyElement, - i; - - // Don't linkify anchor tags or tags that have the .linkified class - if (node && - typeof node === 'object' && - node.nodeType === 1 && - node.tagName.toLowerCase() !== 'a' && - !/[^\s]linkified[\s$]/.test(node.className) - ) { - - children = []; - dummyElement = Linkified._dummyElement || - document.createElement('div'); - - childNode = node.firstChild; - childCount = node.childElementCount; - - while (childNode) { - - if (childNode.nodeType === 3) { - - /* - Cleanup dummy node. This is to make sure that - existing nodes don't get improperly removed - */ - while (dummyElement.firstChild) { - dummyElement.removeChild(dummyElement.firstChild); - } - - /* - Linkify the text node, set the result to the - dummy's contents - */ - dummyElement.innerHTML = Linkified.linkify.call( - this, - childNode.textContent || childNode.innerText || childNode.nodeValue - ); - - /* - Parse the linkified text and append it to the - new children - */ - children.push.apply( - children, - dummyElement.childNodes - ); - - // Clean up the dummy again? - while (dummyElement.firstChild) { - dummyElement.removeChild(dummyElement.firstChild); - } - - } else if (childNode.nodeType === 1) { - - // This is an HTML node, linkify it and add it - children.push(Linkified.linkifyNode.call(this, childNode)); - - } else { - - // This is some other kind of node, just push it - children.push(childNode); - } - - childNode = childNode.nextSibling; - } - - - // Remove all existing nodes. - while (node.firstChild) { - node.removeChild(node.firstChild); - } - - // Replace with all the new nodes - for (i = 0; i < children.length; i++) { - node.appendChild(children[i]); - } - - } - return node; -}, - -Linkified._dummyElement = document.createElement('div'); diff --git a/src/linkify.js b/src/linkify.js new file mode 100644 index 00000000..0109d74b --- /dev/null +++ b/src/linkify.js @@ -0,0 +1,40 @@ +let +scanner = require('./scanner'), +parser = require('./parser'); + +/** + Converts a string into tokens that represent linkable and non-linkable bits + @method tokenize + @param {String} str + @return {Array} tokens +*/ +let tokenize = function (str) { + return parser.run(scanner.run(str)); +}; + +/** + Returns a list of linkable items in the given string. +*/ +let find = function (str) { + + let + tokens = tokenize(str), + filtered = []; + + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].isLink) { + filtered.push(tokens[i].toObject()); + } + } + + return filtered; +}; + +// Scanner and parser provide states and tokens for the lexicographic stage +// (will be used to add additional link types) +module.exports = { + scanner: scanner, + parser: parser, + tokenize: tokenize, + find: find +}; diff --git a/src/parser/index.js b/src/parser/index.js new file mode 100644 index 00000000..01c6d1a9 --- /dev/null +++ b/src/parser/index.js @@ -0,0 +1,291 @@ +/** + Not exactly parser, more like the second-stage scanner (although we can + theoretically hotswap the code here with a real parser in the future... but + for a little URL-finding utility abstract syntax trees may be a little + overkill). + + URL format: http://en.wikipedia.org/wiki/URI_scheme + Email format: http://en.wikipedia.org/wiki/Email_address (links to RFC in + reference) + + @module linkify + @submodule parser + @main parser +*/ + +let +TEXT_TOKENS = require('../tokens/text'), +MULTI_TOKENS = require('../tokens/multi'), +State = require('./state'); + +let makeState = function (tokenClass) { + return new State(tokenClass); +}; + +const +TT_DOMAIN = TEXT_TOKENS.DOMAIN, +TT_AT = TEXT_TOKENS.AT, +TT_COLON = TEXT_TOKENS.COLON, +TT_DOT = TEXT_TOKENS.DOT, +TT_LOCALHOST = TEXT_TOKENS.LOCALHOST, +TT_NL = TEXT_TOKENS.NL, +TT_NUM = TEXT_TOKENS.NUM, +TT_PLUS = TEXT_TOKENS.PLUS, +TT_POUND = TEXT_TOKENS.POUND, +TT_PROTOCOL = TEXT_TOKENS.PROTOCOL, +TT_QUERY = TEXT_TOKENS.QUERY, +TT_SLASH = TEXT_TOKENS.SLASH, +TT_SYM = TEXT_TOKENS.SYM, +TT_TLD = TEXT_TOKENS.TLD; +// TT_WS = TEXT_TOKENS.WS; + +const +T_EMAIL = MULTI_TOKENS.EMAIL, +T_NL = MULTI_TOKENS.NL, +T_TEXT = MULTI_TOKENS.TEXT, +T_URL = MULTI_TOKENS.URL; + +// The universal starting state. +let S_START = makeState(); + +// Intermediate states for URLs. Note that domains that begin with a protocol +// are treated slighly differently from those that don't. +// (PSS == "PROTOCOL SLASH SLASH") +// S_DOMAIN* states can generally become prefixes for email addresses, while +// S_PSS_DOMAIN* cannot +let +S_PROTOCOL = makeState(), // e.g., 'http:' +S_PROTOCOL_SLASH = makeState(), // e.g., '/', 'http:/'' +S_PROTOCOL_SLASH_SLASH = makeState(), // e.g., '//', 'http://' +S_DOMAIN = makeState(), // parsed string ends with a potential domain name (A) +S_DOMAIN_DOT = makeState(), // (A) domain followed by DOT +S_TLD = makeState(T_URL), // (A) Simplest possible URL with no query string +S_TLD_COLON = makeState(), // (A) URL followed by colon (potential port number here) +S_TLD_PORT = makeState(T_URL), // TLD followed by a port number +S_PSS_DOMAIN = makeState(), // parsed string starts with protocol and ends with a potential domain name (B) +S_PSS_DOMAIN_DOT = makeState(), // (B) domain followed by DOT +S_PSS_TLD = makeState(T_URL), // (B) Simplest possible URL with no query string and a protocol +S_PSS_TLD_COLON = makeState(), // (A) URL followed by colon (potential port number here) +S_PSS_TLD_PORT = makeState(T_URL), // TLD followed by a port number +S_URL = makeState(T_URL), // Long URL with optional port and maybe query string +S_URL_SYMS = makeState(), // URL followed by some symbols (will not be part of the final URL) +S_EMAIL_DOMAIN = makeState(), // parsed string starts with local email info + @ with a potential domain name (C) +S_EMAIL_DOMAIN_DOT = makeState(), // (C) domain followed by DOT +S_EMAIL = makeState(T_EMAIL), // (C) Possible email address (could have more tlds) +S_EMAIL_COLON = makeState(), // (C) URL followed by colon (potential port number here) +S_EMAIL_PORT = makeState(T_EMAIL), // (C) Email address with a port +S_LOCALPART = makeState(), // Local part of the email address +S_LOCALPART_AT = makeState(), // Local part of the email address plus @ +S_LOCALPART_DOT = makeState(), // Local part of the email address plus '.' (localpart cannot end in .) +S_NL = makeState(T_NL); // single new line + +// Make path from start to protocol (with '//') +S_START.on(TT_NL, S_NL); +S_START.on(TT_PROTOCOL, S_PROTOCOL); +S_START.on(TT_SLASH, S_PROTOCOL_SLASH); +S_PROTOCOL.on(TT_SLASH, S_PROTOCOL_SLASH); +S_PROTOCOL_SLASH.on(TT_SLASH, S_PROTOCOL_SLASH_SLASH); + +// The very first potential domain name +S_START.on(TT_TLD, S_DOMAIN); +S_START.on(TT_DOMAIN, S_DOMAIN); +S_START.on(TT_LOCALHOST, S_TLD); +S_START.on(TT_NUM, S_LOCALPART); +S_PROTOCOL_SLASH_SLASH.on(TT_TLD, S_PSS_DOMAIN); +S_PROTOCOL_SLASH_SLASH.on(TT_DOMAIN, S_PSS_DOMAIN); +S_PROTOCOL_SLASH_SLASH.on(TT_LOCALHOST, S_PSS_TLD); + +// Account for dots and hyphens +// hyphens are usually parts of domain names +S_DOMAIN.on(TT_DOT, S_DOMAIN_DOT); +S_PSS_DOMAIN.on(TT_DOT, S_PSS_DOMAIN_DOT); +S_EMAIL_DOMAIN.on(TT_DOT, S_EMAIL_DOMAIN_DOT); + +// Hyphen can jump back to a domain name + +// After the first domain and a dot, we can find either a URL or another domain +S_DOMAIN_DOT.on(TT_TLD, S_TLD); +S_DOMAIN_DOT.on(TT_DOMAIN, S_DOMAIN); +S_DOMAIN_DOT.on(TT_LOCALHOST, S_DOMAIN); +S_PSS_DOMAIN_DOT.on(TT_TLD, S_PSS_TLD); +S_PSS_DOMAIN_DOT.on(TT_DOMAIN, S_PSS_DOMAIN); +S_PSS_DOMAIN_DOT.on(TT_LOCALHOST, S_PSS_DOMAIN); +S_EMAIL_DOMAIN_DOT.on(TT_TLD, S_EMAIL); +S_EMAIL_DOMAIN_DOT.on(TT_DOMAIN, S_EMAIL_DOMAIN); +S_EMAIL_DOMAIN_DOT.on(TT_LOCALHOST, S_EMAIL_DOMAIN); + +// S_TLD accepts! But the URL could be longer, try to find a match greedily +// The `run` function should be able to "rollback" to the accepting state +S_TLD.on(TT_DOT, S_DOMAIN_DOT); +S_PSS_TLD.on(TT_DOT, S_PSS_DOMAIN_DOT); +S_EMAIL.on(TT_DOT, S_EMAIL_DOMAIN_DOT); + +// Become real URLs after `SLASH` or `COLON NUM SLASH` +// Here PSS and non-PSS converge +S_TLD.on(TT_COLON, S_TLD_COLON); +S_TLD.on(TT_SLASH, S_URL); +S_TLD_COLON.on(TT_NUM, S_TLD_PORT); +S_TLD_PORT.on(TT_SLASH, S_URL); +S_PSS_TLD.on(TT_COLON, S_PSS_TLD_COLON); +S_PSS_TLD.on(TT_SLASH, S_URL); +S_PSS_TLD_COLON.on(TT_NUM, S_PSS_TLD_PORT); +S_PSS_TLD_PORT.on(TT_SLASH, S_URL); +S_EMAIL.on(TT_COLON, S_EMAIL_COLON); +S_EMAIL_COLON.on(TT_NUM, S_EMAIL_PORT); + +// Types of characters the URL can definitely end in +let qsAccepting = [ + TT_DOMAIN, + TT_AT, + + TT_LOCALHOST, + TT_NUM, + TT_PLUS, + TT_POUND, + TT_PROTOCOL, + TT_SLASH, + TT_TLD +]; + +// Types of tokens that can follow a URL and be part of the query string +// but cannot be the very last characters +// Characters that cannot appear in the URL at all should be excluded +let qsNonAccepting = [ + TT_COLON, + TT_DOT, + TT_QUERY, + TT_SYM +]; + +// Account for the query string +S_URL.on(qsAccepting, S_URL); +S_URL_SYMS.on(qsAccepting, S_URL); + +S_URL.on(qsNonAccepting, S_URL_SYMS); +S_URL_SYMS.on(qsNonAccepting, S_URL_SYMS); + +// Email address-specific state definitions +// Note: We are not allowing '/' in email addresses since this would interfere +// with real URLs + +// Tokens allowed in the localpart of the email +let localpartAccepting = [ + TT_DOMAIN, + TT_COLON, + TT_NUM, + TT_PLUS, + TT_POUND, + TT_QUERY, + TT_SYM, + TT_TLD +]; + +// Some of the tokens in `localpartAccepting` are already accounted for here and +// will not be overwritten (don't worry) +S_DOMAIN.on(localpartAccepting, S_LOCALPART); +S_DOMAIN.on(TT_AT, S_LOCALPART_AT); +S_DOMAIN_DOT.on(localpartAccepting, S_LOCALPART); +S_TLD.on(localpartAccepting, S_LOCALPART); +S_TLD.on(TT_AT, S_LOCALPART_AT); +S_TLD_COLON.on(localpartAccepting, S_LOCALPART); +S_TLD_COLON.on(TT_DOT, S_LOCALPART); +S_TLD_COLON.on(TT_AT, S_LOCALPART_AT); +S_TLD_PORT.on(localpartAccepting, S_LOCALPART); +S_TLD_PORT.on(TT_DOT, S_LOCALPART_DOT); +S_TLD_PORT.on(TT_AT, S_LOCALPART_AT); + +// Okay we're on a localpart. Now what? +// TODO: IP addresses and what if the email starts with numbers? +S_LOCALPART.on(localpartAccepting, S_LOCALPART); +S_LOCALPART.on(TT_AT, S_LOCALPART_AT); // close to an email address now +S_LOCALPART.on(TT_DOT, S_LOCALPART_DOT); +S_LOCALPART_DOT.on(localpartAccepting, S_LOCALPART); +S_LOCALPART_AT.on(TT_TLD, S_EMAIL_DOMAIN); +S_LOCALPART_AT.on(TT_DOMAIN, S_EMAIL_DOMAIN); +S_LOCALPART_AT.on(TT_LOCALHOST, S_EMAIL); +// States following `@` defined above + +let run = function (tokens) { + let + len = tokens.length, + cursor = 0, + multis = [], + textTokens = []; + + while (cursor < len) { + + let + state = S_START, + secondState = null, + nextState = null, + multiLength = 0, + latestAccepting = null, + sinceAccepts = -1; + + while (cursor < len && !(secondState = state.next(tokens[cursor]))) { + // Starting tokens with nowhere to jump to. + // Consider these to be just plain text + textTokens.push(tokens[cursor++]); + } + + while (cursor < len && ( + nextState = secondState || state.next(tokens[cursor])) + ) { + + // Get the next state + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + cursor++; + multiLength++; + } + + if (sinceAccepts < 0) { + + // No accepting state was found, part of a regular text token + // Add all the tokens we looked at to the text tokens array + for (let i = cursor - multiLength; i < cursor; i++) { + textTokens.push(tokens[i]); + } + + } else { + + // Accepting state! + + // First close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new T_TEXT(textTokens)); + textTokens = []; + } + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + multiLength -= sinceAccepts; + + // Create a new multitoken + let MULTI = latestAccepting.emit(); + multis.push(new MULTI(tokens.slice(cursor - multiLength, cursor))); + } + } + + // Finally close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new T_TEXT(textTokens)); + } + + return multis; +}; + +module.exports = { + TOKENS: MULTI_TOKENS, + State: State, + run: run +}; diff --git a/src/parser/state.js b/src/parser/state.js new file mode 100644 index 00000000..f7e83473 --- /dev/null +++ b/src/parser/state.js @@ -0,0 +1,28 @@ +/** + @module linkify + @submodule parser +*/ +let BaseState = require('../state/base'); + +/** + Subclass of + @class CharacterState + @extends BaseState +*/ +class TokenState extends BaseState { + + /** + Is the given token an instance of the given token class? + + @method test + @param {TextToken} token + @param {Class} tokenClass + @return {Boolean} + */ + test(token, tokenClass) { + return tokenClass.test(token); + } + +} + +module.exports = TokenState; diff --git a/src/scanner/index.js b/src/scanner/index.js new file mode 100644 index 00000000..e0672cf7 --- /dev/null +++ b/src/scanner/index.js @@ -0,0 +1,179 @@ +/** + The scanner provides an interface that takes a string of text as input, and + outputs an array of tokens instances that can be used for easy URL parsing. + + @module linkify + @submodule scanner + @main scanner +*/ + +let +TOKENS = require('../tokens/text'), +State = require('./state'), +stateify = require('./stateify'), +tlds = require('./tlds').complete; + +const +REGEXP_NUM = /[0-9]/, +REGEXP_ALPHANUM = /[a-z0-9]/, +COLON = ':'; + +let +domainStates = [], // states that jump to DOMAIN on /[a-z0-9]/ +makeState = function (tokenClass) { + return new State(tokenClass); +}; + +const // Frequently used tokens +T_DOMAIN = TOKENS.DOMAIN, +T_LOCALHOST = TOKENS.LOCALHOST, +T_NUM = TOKENS.NUM, +T_PROTOCOL = TOKENS.PROTOCOL, +T_TLD = TOKENS.TLD, +T_WS = TOKENS.WS; + +const // Frequently used states +S_START = makeState(), // start state +S_NUM = makeState(T_NUM), +S_DOMAIN = makeState(T_DOMAIN), +S_DOMAIN_HYPHEN = makeState(), // domain followed by 1 or more hyphen characters +S_WS = makeState(T_WS); + +// States for special URL symbols +S_START.on('@', makeState(TOKENS.AT)); +S_START.on('.', makeState(TOKENS.DOT)); +S_START.on('+', makeState(TOKENS.PLUS)); +S_START.on('#', makeState(TOKENS.POUND)); +S_START.on('?', makeState(TOKENS.QUERY)); +S_START.on('/', makeState(TOKENS.SLASH)); +S_START.on(COLON, makeState(TOKENS.COLON)); + +// Whitespace jumps +// Tokens of only non-newline whitespace are arbitrarily long +S_START.on(/\n/, makeState(TOKENS.NL)); +S_START.on(/\s/, S_WS); +S_WS.on(/[^\S\n]/, S_WS); // If any whitespace except newline, more whitespace! + +// Generates states for top-level domains +// Note that this is most accurate when tlds are in alphabetical order +for (let i = 0; i < tlds.length; i++) { + let newStates = stateify(tlds[i], S_START, T_TLD, T_DOMAIN); + domainStates.push.apply(domainStates, newStates); +} + +// Collect the states generated by different protocls +let +partialProtocolFileStates = stateify('file', S_START, T_DOMAIN, T_DOMAIN), +partialProtocolFtpStates = stateify('ftp', S_START, T_DOMAIN, T_DOMAIN), +partialProtocolHttpStates = stateify('http', S_START, T_DOMAIN, T_DOMAIN); + +// Add the states to the array of DOMAINeric states +domainStates.push.apply(domainStates, partialProtocolFileStates); +domainStates.push.apply(domainStates, partialProtocolFtpStates); +domainStates.push.apply(domainStates, partialProtocolHttpStates); + +let // Protocol states +S_PROTOCOL_FILE = partialProtocolFileStates.pop(), +S_PROTOCOL_FTP = partialProtocolFtpStates.pop(), +S_PROTOCOL_HTTP = partialProtocolHttpStates.pop(), +S_PROTOCOL_SECURE = makeState(T_DOMAIN), +S_FULL_PROTOCOL = makeState(T_PROTOCOL); // Full protocol ends with COLON + +// Secure protocols (end with 's') +S_PROTOCOL_FTP.on('s', S_PROTOCOL_SECURE); +S_PROTOCOL_HTTP.on('s', S_PROTOCOL_SECURE); +domainStates.push(S_PROTOCOL_SECURE); + +// Become protocol tokens after a COLON +S_PROTOCOL_FILE.on(COLON, S_FULL_PROTOCOL); +S_PROTOCOL_FTP.on(COLON, S_FULL_PROTOCOL); +S_PROTOCOL_HTTP.on(COLON, S_FULL_PROTOCOL); +S_PROTOCOL_SECURE.on(COLON, S_FULL_PROTOCOL); + +// Localhost +let partialLocalhostStates = stateify('localhost', S_START, T_LOCALHOST, T_DOMAIN); +domainStates.push.apply(domainStates, partialLocalhostStates); + +// Everything else +// DOMAINs make more DOMAINs +// Number and character transitions +S_START.on(REGEXP_NUM, S_NUM); +S_NUM.on('-', S_DOMAIN_HYPHEN); +S_NUM.on(REGEXP_NUM, S_NUM); +S_NUM.on(REGEXP_ALPHANUM, S_DOMAIN); // number becomes DOMAIN +S_DOMAIN.on(REGEXP_ALPHANUM, S_DOMAIN); + +// All the generated states should have a jump to DOMAIN +for (let i = 0; i < domainStates.length; i++) { + domainStates[i].on('-', S_DOMAIN_HYPHEN); + domainStates[i].on(REGEXP_ALPHANUM, S_DOMAIN); +} + +S_DOMAIN_HYPHEN.on('-', S_DOMAIN_HYPHEN); +S_DOMAIN_HYPHEN.on(REGEXP_NUM, S_DOMAIN); +S_DOMAIN_HYPHEN.on(REGEXP_ALPHANUM, S_DOMAIN); + +// Any other character is considered a single symbol token +S_START.on(/./, makeState(TOKENS.SYM)); + +// Tokens +exports.TOKENS = TOKENS; + +/** + Given a string, returns an array of TOKEN instances representing the + composition of that string. + + @method run + @param {String} str Input string to scan + @return {Array} Array of TOKEN instances +*/ +exports.run = function (str) { + + let + lowerStr = str.toLowerCase(), // The state machine only looks at lowercase strings + len = str.length, + cursor = 0, + tokens = []; // return value + + // Tokenize the string + while (cursor < len) { + + let + state = S_START, + secondState = null, + nextState = null, + tokenLength = 0, + latestAccepting = null, + sinceAccepts = -1; + + while (cursor < len && (nextState = state.next(lowerStr[cursor]))) { + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + tokenLength++; + cursor++; + } + + if (sinceAccepts < 0) continue; // Should never happen + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + tokenLength -= sinceAccepts; + + // Get the class for the new token + let TOKEN = latestAccepting.emit(); // Current token class + + // No more jumps, just make a new token + tokens.push(new TOKEN(str.substr(cursor - tokenLength, tokenLength))); + } + + return tokens; +}; diff --git a/src/scanner/state.js b/src/scanner/state.js new file mode 100644 index 00000000..e5b51643 --- /dev/null +++ b/src/scanner/state.js @@ -0,0 +1,31 @@ +/** + @module linkify + @submodule scanner +*/ +let BaseState = require('../state/base'); + +/** + Subclass of + @class CharacterState + @extends BaseState +*/ +class CharacterState extends BaseState { + + /** + Does the given character match the given character or regular + expression? + + @method test + @param {String} char + @param {String|RegExp} charOrRegExp + @return {Boolean} + */ + test(char, charOrRegExp) { + return char === charOrRegExp || ( + charOrRegExp instanceof RegExp && charOrRegExp.test(char) + ); + } + +} + +module.exports = CharacterState; diff --git a/src/scanner/stateify.js b/src/scanner/stateify.js new file mode 100644 index 00000000..3f518212 --- /dev/null +++ b/src/scanner/stateify.js @@ -0,0 +1,54 @@ +/** + @module linkify + @submodule tokenizer +*/ + +let CharacterState = require('./state'); + +/** + Given a non-empty target string, generates states (if required) for each + consecutive substring of characters in str starting from the beginning of + the string. The final state will have a special value, as specified in + options. All other "in between" substrings will have a default end state. + + Note that I haven't really tried these with any strings other than + DOMAINeric. + + @param {String} str + @param {CharacterState} start State to jump from the first character + @param {Class} endToken Token class to emit when the given string has been + matched and no more jumps exist. + @param {Class} defaultToken "Filler token", or which token type to emit when + we don't have a full match + @return {Array} list of newly-created states +*/ +module.exports = function (str, start, endToken, defaultToken) { + + let i = 0, + len = str.length, + state = start, + newStates = [], + nextState; + + // Find the next state without a jump to the next character + while (i < len && (nextState = state.next(str[i]))) { + state = nextState; + i++; + } + + if (i >= len) return []; // no new tokens were added + + while (i < len - 1) { + nextState = new CharacterState(defaultToken); + newStates.push(nextState); + state.on(str[i], nextState); + state = nextState; + i++; + } + + nextState = new CharacterState(endToken); + newStates.push(nextState); + state.on(str[len - 1], nextState); + + return newStates; +}; diff --git a/src/scanner/tlds.js b/src/scanner/tlds.js new file mode 100644 index 00000000..97ce2c0a --- /dev/null +++ b/src/scanner/tlds.js @@ -0,0 +1,12 @@ +/** + NOTICE: Please ensure that these strings are sorted in alphabetical order +*/ + +// http://www.seobythesea.com/2006/01/googles-most-popular-and-least-popular-top-level-domains/ +// .co and .io have also been added to the list +exports.essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); + +// http://data.iana.org/TLD/tlds-alpha-by-domain.txt +exports.complete = ( + 'abogado|ac|academy|accountants|active|actor|ad|adult|ae|aero|af|ag|agency|ai|airforce|al|allfinanz|alsace|am|an|android|ao|aq|aquarelle|ar|archi|army|arpa|as|asia|associates|at|attorney|au|auction|audio|autos|aw|ax|axa|az|ba|band|bar|bargains|bayern|bb|bd|be|beer|berlin|best|bf|bg|bh|bi|bid|bike|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bmw|bn|bnpparibas|bo|boo|boutique|br|brussels|bs|bt|budapest|build|builders|business|buzz|bv|bw|by|bz|bzh|ca|cab|cal|camera|camp|cancerresearch|capetown|capital|caravan|cards|care|career|careers|casa|cash|cat|catering|cc|cd|center|ceo|cern|cf|cg|ch|channel|cheap|christmas|chrome|church|ci|citic|city|ck|cl|claims|cleaning|click|clinic|clothing|club|cm|cn|co|coach|codes|coffee|college|cologne|com|community|company|computer|condos|construction|consulting|contractors|cooking|cool|coop|country|cr|credit|creditcard|cricket|crs|cruises|cu|cuisinella|cv|cw|cx|cy|cymru|cz|dad|dance|dating|day|de|deals|degree|delivery|democrat|dental|dentist|desi|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|domains|durban|dvag|dz|eat|ec|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|equipment|er|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|fail|farm|fashion|feedback|fi|finance|financial|firmdale|fish|fishing|fitness|fj|fk|flights|florist|flsmidth|fly|fm|fo|foo|forsale|foundation|fr|frl|frogans|fund|furniture|futbol|ga|gal|gallery|gb|gbiz|gd|ge|gent|gf|gg|gh|gi|gift|gifts|gives|gl|glass|gle|global|globo|gm|gmail|gmo|gmx|gn|google|gop|gov|gp|gq|gr|graphics|gratis|green|gripe|gs|gt|gu|guide|guitars|guru|gw|gy|hamburg|haus|healthcare|help|here|hiphop|hiv|hk|hm|hn|holdings|holiday|homes|horse|host|hosting|house|how|hr|ht|hu|ibm|id|ie|il|im|immo|immobilien|in|industries|info|ing|ink|institute|insure|int|international|investments|io|iq|ir|irish|is|it|je|jetzt|jm|jo|jobs|joburg|jp|juegos|kaufen|ke|kg|kh|ki|kim|kitchen|kiwi|km|kn|koeln|kp|kr|krd|kred|kw|ky|kz|la|lacaixa|land|latrobe|lawyer|lb|lc|lds|lease|legal|lgbt|li|life|lighting|limited|limo|link|lk|loans|london|lotto|lr|ls|lt|ltda|lu|luxe|luxury|lv|ly|ma|madrid|maison|management|mango|market|marketing|mc|md|me|media|meet|melbourne|meme|memorial|menu|mg|mh|miami|mil|mini|mk|ml|mm|mn|mo|mobi|moda|moe|monash|money|mormon|mortgage|moscow|motorcycles|mov|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|nagoya|name|navy|nc|ne|net|network|neustar|new|nexus|nf|ng|ngo|nhk|ni|ninja|nl|no|np|nr|nra|nrw|nu|nyc|nz|okinawa|om|ong|onl|ooo|org|organic|otsuka|ovh|pa|paris|partners|parts|party|pe|pf|pg|ph|pharmacy|photo|photography|photos|physio|pics|pictures|pink|pizza|pk|pl|place|plumbing|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|properties|property|ps|pt|pub|pw|py|qa|qpon|quebec|re|realtor|recipes|red|rehab|reise|reisen|reit|ren|rentals|repair|report|republican|rest|restaurant|reviews|rich|rio|rip|ro|rocks|rodeo|rs|rsvp|ru|ruhr|rw|ryukyu|sa|saarland|sarl|sb|sc|sca|scb|schmidt|schule|science|scot|sd|se|services|sexy|sg|sh|shiksha|shoes|si|singles|sj|sk|sl|sm|sn|so|social|software|sohu|solar|solutions|soy|space|spiegel|sr|st|su|supplies|supply|support|surf|surgery|suzuki|sv|sx|sy|sydney|systems|sz|taipei|tatar|tattoo|tax|tc|td|technology|tel|tf|tg|th|tienda|tips|tirol|tj|tk|tl|tm|tn|to|today|tokyo|tools|top|town|toys|tp|tr|trade|training|travel|trust|tt|tui|tv|tw|tz|ua|ug|uk|university|uno|uol|us|uy|uz|va|vacations|vc|ve|vegas|ventures|versicherung|vet|vg|vi|viajes|villas|vision|vlaanderen|vn|vodka|vote|voting|voto|voyage|vu|wales|wang|watch|webcam|website|wed|wedding|wf|whoswho|wien|wiki|williamhill|wme|work|works|world|ws|wtc|wtf|xxx|xyz|yachts|yandex|ye|yoga|yokohama|youtube|yt|za|zip|zm|zone|zw' +).split('|'); diff --git a/src/state/base.js b/src/state/base.js new file mode 100644 index 00000000..9e34ab81 --- /dev/null +++ b/src/state/base.js @@ -0,0 +1,116 @@ +/** + @module linkify + @submodule state +*/ + +/** + A simple state machine that can emit token classes + + The `j` property in this class refers to state jumps. It's a + multidimensional array where for each element: + + * index [0] is a symbol or class of symbols to transition to. + * index [1] is a State instance which matches + + The type of symbol will depend on the target implementation for this class. + In Linkify, we have a two-stage scanner. Each stage uses this state machine + but with a slighly different (polymorphic) implementation. + + The `T` property refers to the token class. + + TODO: Can the `on` and `next` methods be combined? + + @class BaseState +*/ +class BaseState { + + /** + @method constructor + @param {Class} tClass Pass in the kind of token to emit if there are + no jumps after this state and the state is accepting. + */ + constructor(tClass) { + this.j = []; + this.T = tClass || null; + } + + /** + On the given symbol(s), this machine should go to the given state + + @method on + @param {Array|Mixed} symbol + @param {BaseState} state Note that the type of this state should be the + same as the current instance (i.e., don't pass in a different + subclass) + */ + on(symbol, state) { + if (symbol instanceof Array) { + for (let i = 0; i < symbol.length; i++) { + this.j.push([symbol[i], state]); + } + return; + } + this.j.push([symbol, state]); + } + + /** + Given the next item, returns next state for that item + @method next + @param {Mixed} item Should be an instance of the symbols handled by + this particular machine. + @return {State} state Returns false if no jumps are available + */ + next(item) { + + for (let i = 0; i < this.j.length; i++) { + + let jump = this.j[i], + symbol = jump[0], // Next item to check for + state = jump[1]; // State to jump to if items match + + // compare item with symbol + if (this.test(item, symbol)) return state; + } + + // Nowhere left to jump! + return false; + } + + /** + Does this state accept? + `true` only of `this.T` exists + + @method accepts + @return {Boolean} + */ + accepts() { + return !!this.T; + } + + /** + Determine whether a given item "symbolizes" the symbol, where symbol is + a class of items handled by this state machine. + + This method should be overriden in extended classes. + + @method test + @param {Mixed} item Does this item match the given symbol? + @param {Mixed} symbol + @return {Boolean} + */ + test(item, symbol) { + return item === symbol; + } + + /** + Emit the token for this State (just return it in this case) + If this emits a token, this instance is an accepting state + @method emit + @return {Class} T + */ + emit() { + return this.T; + } +} + +module.exports = BaseState; diff --git a/src/tokens/multi.js b/src/tokens/multi.js new file mode 100644 index 00000000..d8836002 --- /dev/null +++ b/src/tokens/multi.js @@ -0,0 +1,214 @@ +/** + @module linkify + @submodule tokens +*/ +let +TEXT_TOKENS = require('./text'); + +const +TT_PROTOCOL = TEXT_TOKENS.PROTOCOL, +TT_DOMAIN = TEXT_TOKENS.DOMAIN, +TT_TLD = TEXT_TOKENS.TLD, +TT_SLASH = TEXT_TOKENS.SLASH; + +// Is the given token a valid domain token? +// Should nums be included here? +function isDomainToken(token) { + return TT_DOMAIN.test(token) || + TT_TLD.test(token); +} + +/** + Abstract class used for manufacturing tokens of text tokens. That is rather + than the value for a token being a small string of text, it's value an array + of text tokens. + + Used for grouping together URLs, emails, hashtags, and other potential + creations. + + @class MultiToken + @abstract +*/ +class MultiToken { + /** + @method constructor + @param {Array} value The array of `TextToken`s representing this + particular MultiToken + */ + constructor(value) { + this.v = value; + } + + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + get type() { return 'TOKEN'; } + + /** + Is this multitoken a link? + @property isLink + @default false + */ + get isLink() { return false; } + + /** + Return the string this token represents. + @method toString + @return {String} + */ + toString() { + let result = []; + for (let i = 0; i < this.v.length; i++) { + result.push(this.v[i].toString()); + } + return result.join(''); + } + + /** + What should the value for this token be in the `href` HTML attribute? + Returns the `.toString` value by default. + + @method toHref + @return {String} + */ + toHref() { + return this.toString(); + } + + /** + Returns a hash of relevant values for this token, which includes keys + * type - Kind of token ('url', 'email', etc.) + * value - Original text + * href - The value that should be added to the anchor tag's href + attribute + + @method toObject + @param {String} [protocol] `'http'` by default + @return {Object} + */ + toObject(protocol = 'http') { + return { + type: this.type.toLowerCase(), + value: this.toString(), + href: this.toHref(protocol) + }; + } + + /** + Is the given value an instance of this Token? + @method test + @static + @param {Mixed} value + */ + static test(token) { + return token instanceof this; + } + +} + +/** + Represents a list of tokens making up a valid email address + @class EMAIL + @extends MultiToken +*/ +class EMAIL extends MultiToken { + get type() { return 'EMAIL'; } + get isLink() { return true; } + toHref() { + return 'mailto:' + this.toString(); + } +} + +/** + Represents some plain text + @class TEXT + @extends MultiToken +*/ +class TEXT extends MultiToken { + get type() { return 'TEXT'; } +} + +/** + Represents a line break + @class NL + @extends MultiToken +*/ +class NL extends MultiToken { + get type() { return 'NL'; } +} + +/** + Represents a list of tokens making up a valid URL + @class URL + @extends MultiToken +*/ +class URL extends MultiToken { + get type() { return 'URL'; } + get isLink() { return true; } + + /** + Lowercases relevant parts of the domain and adds the protocol if + required. Note that this will not escape unsafe HTML characters in the + URL. + + @method href + @param {String} protocol + @return {String} + */ + toHref(protocol = 'http') { + let + hasProtocol = false, + hasSlashSlash = false, + tokens = this.v, + result = [], + i = 0; + + // Make the first part of the domain lowercase + // Lowercase protocol + while (TT_PROTOCOL.test(tokens[i])) { + hasProtocol = true; + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Skip slash-slash + while (TT_SLASH.test(tokens[i])) { + hasSlashSlash = true; + result.push(tokens[i].toString()); + i++; + } + + // Lowercase all other characters in the domain + while (isDomainToken(tokens[i])) { + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Leave all other characters as they were written + for (; i < tokens.length; i++) { + result.push(tokens[i].toString()); + } + + result = result.join(''); + + if (!(hasProtocol || hasSlashSlash)) { + result = protocol + '://' + result; + } + + return result; + } + + hasProtocol() { + return this.v[0] instanceof TT_PROTOCOL; + } +} + +module.exports = { + Base: MultiToken, + EMAIL: EMAIL, + NL: NL, + TEXT: TEXT, + URL: URL +}; diff --git a/src/tokens/text.js b/src/tokens/text.js new file mode 100644 index 00000000..97a65077 --- /dev/null +++ b/src/tokens/text.js @@ -0,0 +1,204 @@ +/** + @module linkify + @submodule tokens + @main tokens +*/ +/** + Abstract class used for manufacturing text tokens. + Pass in the value this token represents + + @class TextToken + @abstract +*/ +class TextToken { + /** + @method constructor + @param {String} value The string of characters representing this particular Token + */ + constructor(value) { + this.v = value; + } + + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + get type() { return 'TOKEN'; } + + toString() { + return this.v + ''; + } + + /** + Is the given value an instance of this Token? + @method test + @static + @param {Mixed} value + */ + static test(value) { + return value instanceof this; + } +} + +/** + A valid domain token + @class DOMAIN + @extends TextToken +*/ +class DOMAIN extends TextToken { + get type() { return 'DOMAIN'; } +} + +/** + @class AT + @extends TextToken +*/ +class AT extends TextToken { + constructor() { super('@'); } + get type() { return 'AT'; } +} + +/** + Represents a single colon `:` character + + @class COLON + @extends TextToken +*/ +class COLON extends TextToken { + constructor() { super(':'); } + get type() { return 'COLON'; } +} + +/** + @class DOT + @extends TextToken +*/ +class DOT extends TextToken { + constructor() { super('.'); } + get type() { return 'DOT'; } +} + +/** + The word localhost (by itself) + @class LOCALHOST + @extends TextToken +*/ +class LOCALHOST extends TextToken { + get type() { return 'LOCALHOST'; } +} +/** + Newline token + @class NL + @extends TextToken +*/ +class NL extends TextToken { + constructor() { super('\n'); } + get type() { return 'NL'; } +} + +/** + @class NUM + @extends TextToken +*/ +class NUM extends TextToken { + get type() { return 'NUM'; } +} + +/** + @class PLUS + @extends TextToken +*/ +class PLUS extends TextToken { + constructor() { super('+'); } + get type() { return 'PLUS'; } +} + +/** + @class POUND + @extends TextToken +*/ +class POUND extends TextToken { + constructor() { super('#'); } + get type() { return 'POUND'; } +} + +/** + Represents a web URL protocol. Supported types include + + * `http:` + * `https:` + * `ftp:` + * `ftps:` + * There's Another super weird one + + @class PROTOCOL + @extends TextToken +*/ +class PROTOCOL extends TextToken { + get type() { return 'PROTOCOL'; } +} + +/** + @class QUERY + @extends TextToken +*/ +class QUERY extends TextToken { + constructor() { super('?'); } + get type() { return 'QUERY'; } +} + +/** + @class SLASH + @extends TextToken +*/ +class SLASH extends TextToken { + constructor() { super('/'); } + get type() { return 'SLASH'; } +} + +/** + One ore more non-whitespace symbol. + @class SYM + @extends TextToken +*/ +class SYM extends TextToken { + get type() { return 'SYM'; } +} + +/** + @class TLD + @extends TextToken +*/ +class TLD extends TextToken { + get type() { return 'TLD'; } +} + +/** + Represents a string of consecutive whitespace characters + + @class WS + @extends TextToken +*/ +class WS extends TextToken { + get type() { return 'WS'; } +} + +module.exports = { + Base: TextToken, + DOMAIN: DOMAIN, + AT: AT, + COLON: COLON, + DOT: DOT, + LOCALHOST: LOCALHOST, + NL: NL, + NUM: NUM, + PLUS: PLUS, + POUND: POUND, + QUERY: QUERY, + PROTOCOL: PROTOCOL, + SLASH: SLASH, + SYM: SYM, + TLD: TLD, + WS: WS +}; diff --git a/test/benchmarks.js b/test/benchmarks.js new file mode 100644 index 00000000..4aae714e --- /dev/null +++ b/test/benchmarks.js @@ -0,0 +1,36 @@ +var scanner = require('../build/scanner'), sum = 0; + +var ITERATIONS = 2000; + +function benchmark() { + var start = new Date(), end, diff; + + scanner.run('The URL is http://google.com The URL is http://google.com'); + scanner.run('google.com'); + scanner.run('I like google.com the most I like google.com the most'); + scanner.run('I like Google.com the most'); + scanner.run('there are two tests, brennan.com and nick.ca -- do they work?'); + scanner.run('there are two tests!brennan.com. and nick.ca? -- do they work?'); + scanner.run('This [i.imgur.com/ckSj2Ba.jpg)] should also work'); + scanner.run('A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look'); + scanner.run('SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com?q=.exe flickr.com/linktoimage.jpg'); + scanner.run('None.of these.should be.Links okay.please?'); + scanner.run('Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com'); + scanner.run('t.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?'); + scanner.run('This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/'); + scanner.run('About a year ago Graham and I went to Google IO (https://developers.google.com/events/io/) to learn about some upcoming technology and meet some tech folk in the valley. The experience was great. We met a bunch of great people and got our hands on some new technology — check out this page for more on our experience http://digitalmediazone.ryerson.ca/toronto-incubator/brennans-experience-at-google-io/experience. Beyond everything else, the best thing we got out of that conference was a technology/development mentor & a new startup development process. As soapboxhq.com grew, we tweaked our development and deployment process as needed. At the very start we used cheap hosting providers such as ca.godaddy.com and learned to deal with their limitations. We knew there were other ways of doing things, but they seemed to add complex rules and process. This worked for us, so why fix it? We then met Ian (http://iandouglas.com/about/) at Google IO, who agreed to share some of his insight from scaling over and over again. Ian is a senior web developer/architect working over at Sendgrid. Ian is awesome and we really take his advice to heart. He deserves the credit for a lot of what you see below (including the joke I shamelessly stole from him). To see the rest of this post, visit http://soapboxhq.com/blog/startup-development-process-how-we-develop/ or email soapbox-dev-team@example.com.'); + + end = new Date(); + + diff = end.valueOf() - start.valueOf(); + sum += diff; +} + +console.log('Doing ' + ITERATIONS + ' iterations...'); +console.log('Start:', (new Date()).valueOf()); +for (var i = 0; i < ITERATIONS; i++) { + benchmark(); +} +console.log('End:', (new Date()).valueOf()); +console.log('Total time (ms):', sum); +console.log('Average (ms):', sum/i); diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..3d6663d6 --- /dev/null +++ b/test/index.js @@ -0,0 +1,5 @@ +var glob = require('glob'); +require('chai').should(); // Initialize should assertions + +// Require test files +glob.sync('./spec/**/*.js', {cwd: __dirname}).map(require); diff --git a/test/spec/parser/index.js b/test/spec/parser/index.js new file mode 100644 index 00000000..c54e0eac --- /dev/null +++ b/test/spec/parser/index.js @@ -0,0 +1,113 @@ +var +scanner = require('../../../build/scanner'), +parser = require('../../../build/parser'), +MULTI_TOKENS = require('../../../build/tokens/multi'); + +var +TEXT = MULTI_TOKENS.TEXT, +URL = MULTI_TOKENS.URL, +EMAIL = MULTI_TOKENS.EMAIL; +// MNL = MULTI_TOKENS.NL; // new line + +/** + [0] - Original text to parse (should tokenize first) + [1] - The types of tokens the text should result in + [2] - The values of the tokens the text should result in +*/ +var tests = [ + // BEGIN: Original linkify tests + [ + 'google.com', + [URL], + ['google.com'] + ], [ + 'I like google.com the most', + [TEXT, URL, TEXT], + ['I like ', 'google.com', ' the most'] + ], [ + 'I like Google.com the most', + [TEXT, URL, TEXT], + ['I like ', 'Google.com', ' the most'] + ], ['there are two tests, brennan.com and nick.ca -- do they work?', + [TEXT, URL, TEXT, URL, TEXT], + ['there are two tests, ', 'brennan.com', ' and ', 'nick.ca', ' -- do they work?'] + ], [ + 'there are two tests!brennan.com. and nick.ca? -- do they work?', + [TEXT, URL, TEXT], + ['there are two tests!brennan.com. and ', 'nick.ca', '? -- do they work?'] + ], [ + 'This [i.imgur.com/ckSj2Ba.jpg)] should also work', + [TEXT, URL, TEXT], + ['This [', 'i.imgur.com/ckSj2Ba.jpg', ')] should also work'] + ], [ + 'A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look', + [TEXT, URL, TEXT], + ['A link is ', 'http://nick.is', '.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look'] + ], [ + 'SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com/?q=.exe flickr.com/linktoimage.jpg', + [TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL, TEXT, URL], + ['SOme URLS ', 'http://google.com', ' ', 'https://google1.com', ' ', 'google2.com', ' ', 'google.com/search?q=potatoes+oven', ' ', 'goo.gl/0192n1', ' ', 'google.com', '?q=asda test ', 'bit.ly/0912j', ' ', 'www.bob.com', ' ', 'indigo.dev.soapbox.co/mobile', ' ', 'google.com/?q=.exe', ' ', 'flickr.com/linktoimage.jpg'], + ], [ + 'None.of these.should be.Links okay.please?', + [TEXT], + ['None.of these.should be.Links okay.please?'] + ], [ + 'Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com', + [TEXT, EMAIL, TEXT, EMAIL, TEXT, URL], + ['Here are some random emails: ', 'nick@soapbox.com', ', nick@soapbox.soda (invalid), ', 'Nick@dev.dev.soapbox.co', ', random nick.frasser_hitsend@', 'http://facebook.com'] + ], [ + 't.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?', + [URL, TEXT, URL, TEXT], + ['t.c.com/sadqad', ' is a great domain, so is ', 'ftp://i.am.a.b.ca/', ' okay?'] + ], [ + 'This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/', + [TEXT, URL, TEXT, URL, TEXT, URL], + ['This port is too short ', 'someport.com', ': this port is too long ', 'http://googgle.com:789023/myQuery', ' this port is just right ', 'https://github.com:8080/SoapBox/jQuery-linkify/'] + ], + // END: Original linkifiy tests + // BEGIN: New linkify tests + [ + 'The best URL http://google.com/?love=true and t.co', + [TEXT, URL, TEXT, URL], + ['The best URL ', 'http://google.com/?love=true', ' and ', 't.co'] + ], [ + 'Please email me at testy.test+123@gmail.com', + [TEXT, EMAIL], + ['Please email me at ', 'testy.test+123@gmail.com'], + ], [ + 'http://aws.amazon.com:8080/nick?was=here and localhost:3000 are also domains', + [URL, TEXT, URL, TEXT], + ['http://aws.amazon.com:8080/nick?was=here', ' and ', 'localhost:3000', ' are also domains'] + ], [ + 'http://500-px.com is a real domain?', + [URL, TEXT], + ['http://500-px.com', ' is a real domain?'] + ], [ + 'IP loops like email? 192.168.0.1@gmail.com works!!', + [TEXT, EMAIL, TEXT], + ['IP loops like email? ', '192.168.0.1@gmail.com', ' works!!'] + ] + // END: New linkify tests +]; + +describe('parser#run()', function () { + + function makeTest(test) { + return it('Tokenizes the string "' + test[0] + '"', function () { + var + str = test[0], + types = test[1], + values = test[2], + result = parser.run(scanner.run(str)); + + result.map(function (token) { return token.constructor; }) + .should.eql(types); + + result.map(function (token) { return token.toString(); }) + .should.eql(values); + }); + } + + tests.forEach(makeTest, this); + +}); diff --git a/test/spec/parser/state.js b/test/spec/parser/state.js new file mode 100644 index 00000000..7e17e3ba --- /dev/null +++ b/test/spec/parser/state.js @@ -0,0 +1,28 @@ +/*jshint -W030 */ +var +TEXT_TOKENS = require('../../../build/tokens/text'), +TokenState = require('../../../build/parser/state'); + +describe('TokenState', function () { + var TS_START; + + before(function () { + TS_START = new TokenState(); + }); + + describe('#test()', function () { + it('Ensures token instances belong to the given token class', function () { + // This method is used internally + + var + dot = new TEXT_TOKENS.DOT(), + text = new TEXT_TOKENS.DOMAIN('abc123doremi'); + + TS_START.test(dot, TEXT_TOKENS.DOT).should.be.ok; + TS_START.test(text, TEXT_TOKENS.DOMAIN).should.be.ok; + TS_START.test(text, TEXT_TOKENS.DOT).should.not.be.ok; + TS_START.test(dot, TEXT_TOKENS.DOMAIN).should.not.be.ok; + + }); + }); +}); diff --git a/test/spec/scanner/index.js b/test/spec/scanner/index.js new file mode 100644 index 00000000..5a287cf6 --- /dev/null +++ b/test/spec/scanner/index.js @@ -0,0 +1,85 @@ +var +scanner = require('../../../build/scanner'), +TEXT_TOKENS = require('../../../build/tokens/text'); + +var +DOMAIN = TEXT_TOKENS.DOMAIN, +AT = TEXT_TOKENS.AT, +COLON = TEXT_TOKENS.COLON, +DOT = TEXT_TOKENS.DOT, +LOCALHOST = TEXT_TOKENS.LOCALHOST, +NL = TEXT_TOKENS.NL, +NUM = TEXT_TOKENS.NUM, +PLUS = TEXT_TOKENS.PLUS, +POUND = TEXT_TOKENS.POUND, +PROTOCOL = TEXT_TOKENS.PROTOCOL, +QUERY = TEXT_TOKENS.QUERY, +SLASH = TEXT_TOKENS.SLASH, +SYM = TEXT_TOKENS.SYM, +TLD = TEXT_TOKENS.TLD, +WS = TEXT_TOKENS.WS; + +// The elements are +// 1. input string +// 2. Types for the resulting instances +// 3. String values for the resulting instances +var tests = [ + ['', [], []], + ['@', [AT], ['@']], + [':', [COLON], [':']], + ['.', [DOT], ['.']], + ['-', [SYM], ['-']], + ['\n', [NL], ['\n']], + ['+', [PLUS], ['+']], + ['#', [POUND], ['#']], + ['/', [SLASH], ['/']], + ['&', [SYM], ['&']], + ['&?<>(', [SYM, QUERY, SYM, SYM, SYM], ['&', '?', '<', '>', '(']], + ['hello', [DOMAIN], ['hello']], + ['Hello123', [DOMAIN], ['Hello123']], + ['hello123world', [DOMAIN], ['hello123world']], + ['0123', [NUM], ['0123']], + ['123abc', [DOMAIN], ['123abc']], + ['http', [DOMAIN], ['http']], + ['http:', [PROTOCOL], ['http:']], + ['https:', [PROTOCOL], ['https:']], + ['files:', [DOMAIN, COLON], ['files', ':']], + ['file//', [DOMAIN, SLASH, SLASH], ['file', '/', '/']], + ['ftp://', [PROTOCOL, SLASH, SLASH], ['ftp:', '/', '/']], + ['c', [DOMAIN], ['c']], + ['co', [TLD], ['co']], + ['com', [TLD], ['com']], + ['comm', [DOMAIN], ['comm']], + ['abc 123 DoReMi', [DOMAIN, WS, NUM, WS, DOMAIN], ['abc', ' ', '123', ' ', 'DoReMi']], + ['abc 123 \n DoReMi', [DOMAIN, WS, NUM, WS, NL, WS, DOMAIN], ['abc', ' ', '123', ' ', '\n', ' ', 'DoReMi']], + ['local', [DOMAIN], ['local']], + ['localhost', [LOCALHOST], ['localhost']], + ['localhosts', [DOMAIN], ['localhosts']], + ['500px', [DOMAIN], ['500px']], + ['500-px', [DOMAIN], ['500-px']], + ['-500px', [SYM, DOMAIN], ['-', '500px']], + ['500px-', [DOMAIN, SYM], ['500px', '-']], + ['123-456', [DOMAIN], ['123-456']] +]; + +describe('scanner#run()', function () { + + function makeTest(test) { + return it('Tokenizes the string "' + test[0] + '"', function () { + var + str = test[0], + types = test[1], + values = test[2], + result = scanner.run(str); + + result.map(function (token) { return token.constructor; }) + .should.eql(types); + + result.map(function (token) { return token.toString(); }) + .should.eql(values); + }); + } + + tests.forEach(makeTest, this); + +}); diff --git a/test/spec/scanner/state.js b/test/spec/scanner/state.js new file mode 100644 index 00000000..aac2fe73 --- /dev/null +++ b/test/spec/scanner/state.js @@ -0,0 +1,67 @@ +/*jshint -W030 */ +var +TEXT_TOKENS = require('../../../build/tokens/text'), +CharacterState = require('../../../build/scanner/state'); + +describe('CharacterState', function () { + var S_START, S_DOT, S_NUM; + + before(function () { + S_START = new CharacterState(); + S_DOT = new CharacterState(TEXT_TOKENS.DOT); + S_NUM = new CharacterState(TEXT_TOKENS.NUM); + }); + + describe('#next()', function () { + + it('Has no jumps and return null', function () { + S_START.j.length.should.eql(0); + S_START.next('.').should.not.be.ok; + }); + + it('Should return an new state for the ":" character', function () { + S_START.on('.', S_DOT); + S_START.on(/[0-9]/, S_NUM); + + var results = [ + S_START.next('.'), + S_START.next('7'), + ]; + + S_START.j.length.should.eql(2); + + results.map(function (result) { + result.should.be.ok; + result.should.be.an.instanceOf(CharacterState); + }); + + results[0].should.be.eql(S_DOT); + results[1].should.be.eql(S_NUM); + }); + + it('Can return itself (has recursion)', function () { + S_NUM.on(/[0-9]/, S_NUM); + S_NUM.next('8').should.be.eql(S_NUM); + S_NUM.next('0').next('4').should.be.eql(S_NUM); + }); + }); + + describe('#emit()', function () { + it('Should return a falsey value if initalized with no token', function () { + (!S_START.emit()).should.be.ok; + }); + + it('Should return the token it was initialized with', function () { + var state = new CharacterState(TEXT_TOKENS.QUERY); + state.emit().should.be.eql(TEXT_TOKENS.QUERY); + }); + }); + + describe('#test()', function () { + it('Ensures characters match the given token or regexp', function () { + S_START.test('a', 'a').should.be.ok; + S_START.test('b', /[a-z]/).should.be.ok; + S_START.test('\n', /[^\S\n]/).should.not.be.ok; + }); + }); +}); diff --git a/test/spec/scanner/stateify.js b/test/spec/scanner/stateify.js new file mode 100644 index 00000000..22a1e3e3 --- /dev/null +++ b/test/spec/scanner/stateify.js @@ -0,0 +1,54 @@ +var +TOKENS = require('../../../build/tokens/text'), +State = require('../../../build/scanner/state'), +stateify = require('../../../build/scanner/stateify'); + +describe('stateify', function () { + var S_START; + + before(function () { + S_START = new State(); + }); + + it('Makes states for the domain "co"', function () { + var result = stateify('co', S_START, TOKENS.TLD, TOKENS.DOMAIN); + + result.should.be.an.instanceOf(Array); + result.length.should.eql(2); + result[0].T.should.eql(TOKENS.DOMAIN); + result[1].T.should.eql(TOKENS.TLD); + }); + + it('Makes states for the domain "com"', function () { + var result = stateify('com', S_START, TOKENS.TLD, TOKENS.DOMAIN); + result.should.be.an.instanceOf(Array); + result.length.should.eql(1); + result[0].T.should.eql(TOKENS.TLD); + }); + + it('Adding "com" again should not make any new states', function () { + var result = stateify('com', S_START, TOKENS.TLD, TOKENS.DOMAIN); + result.should.be.an.instanceOf(Array); + result.length.should.eql(0); + }); + + it('Makes states for the domain "community"', function () { + var state = S_START, + result = stateify('community', S_START, TOKENS.TLD, TOKENS.DOMAIN); + + result.should.be.an.instanceOf(Array); + result.length.should.eql(6); + + (state = state.next('c')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('o')).T.should.be.eql(TOKENS.TLD); + (state = state.next('m')).T.should.be.eql(TOKENS.TLD); + (state = state.next('m')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('u')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('n')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('i')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('t')).T.should.be.eql(TOKENS.DOMAIN); + (state = state.next('y')).T.should.be.eql(TOKENS.TLD); + + }); + +}); diff --git a/test/spec/tokens/multi.js b/test/spec/tokens/multi.js new file mode 100644 index 00000000..7c089208 --- /dev/null +++ b/test/spec/tokens/multi.js @@ -0,0 +1,200 @@ +/*jshint -W030 */ +var +TEXT_TOKENS = require('../../../build/tokens/text'), +MULTI_TOKENS = require('../../../build/tokens/multi'); + +describe('MULTI_TOKENS', function () { + + describe('URL', function () { + var + urlTextTokens1, urlTextTokens2, urlTextTokens3, + url1, url2, url3; + + before(function () { + urlTextTokens1 = [ // 'Ftps://www.github.com/SoapBox/linkify' + new TEXT_TOKENS.PROTOCOL('Ftps:'), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.DOMAIN('www'), + new TEXT_TOKENS.DOT(), + new TEXT_TOKENS.DOMAIN('github'), + new TEXT_TOKENS.DOT(), + new TEXT_TOKENS.TLD('com'), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.DOMAIN('SoapBox'), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.DOMAIN('linkify'), + ], + + urlTextTokens2 = [ // '//Amazon.ca/Sales' + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.DOMAIN('Amazon'), + new TEXT_TOKENS.DOT(), + new TEXT_TOKENS.TLD('ca'), + new TEXT_TOKENS.SLASH(), + new TEXT_TOKENS.DOMAIN('Sales') + ], + + urlTextTokens3 = [ // 'co.co?o=%2D&p=@gc#wat' + new TEXT_TOKENS.TLD('co'), + new TEXT_TOKENS.DOT(), + new TEXT_TOKENS.TLD('co'), + new TEXT_TOKENS.SYM('?'), + new TEXT_TOKENS.DOMAIN('o'), + new TEXT_TOKENS.SYM('='), + new TEXT_TOKENS.SYM('%'), + new TEXT_TOKENS.DOMAIN('2D'), + new TEXT_TOKENS.SYM('&'), + new TEXT_TOKENS.DOMAIN('p'), + new TEXT_TOKENS.SYM('='), + new TEXT_TOKENS.AT(), + new TEXT_TOKENS.DOMAIN('gc'), + new TEXT_TOKENS.POUND(), + new TEXT_TOKENS.DOMAIN('wat'), + ]; + + url1 = new MULTI_TOKENS.URL(urlTextTokens1); + url2 = new MULTI_TOKENS.URL(urlTextTokens2); + url3 = new MULTI_TOKENS.URL(urlTextTokens3); + }); + + describe('#isLink', function () { + it('Is true in all cases', function () { + url1.isLink.should.be.ok; + url2.isLink.should.be.ok; + url3.isLink.should.be.ok; + }); + }); + + describe('#toString()', function () { + it('Returns the exact URL text', function () { + url1.toString().should.be.eql('Ftps://www.github.com/SoapBox/linkify'); + url2.toString().should.be.eql('//Amazon.ca/Sales'); + url3.toString().should.be.eql('co.co?o=%2D&p=@gc#wat'); + }); + }); + + describe('#toHref()', function () { + it('Keeps the protocol the same as the original URL (and lowercases it)', function () { + url1.toHref().should.be.eql('ftps://www.github.com/SoapBox/linkify'); + }); + + it('Lowercases the domain name only and leaves off the protocol if the URL begins with "//"', function () { + url2.toHref().should.be.eql('//amazon.ca/Sales'); + }); + + it('Adds a default protocol, if required', function () { + url3.toHref().should.be.eql('http://co.co?o=%2D&p=@gc#wat'); + url3.toHref('ftp').should.be.eql('ftp://co.co?o=%2D&p=@gc#wat'); + }); + }); + + describe('#toObject()', function () { + it('Returns a hash with correct type, value, and href', function () { + + url1.toObject('file').should.be.eql({ + type: 'url', + value: 'Ftps://www.github.com/SoapBox/linkify', + href: 'ftps://www.github.com/SoapBox/linkify' + }); + + url2.toObject().should.be.eql({ + type: 'url', + value: '//Amazon.ca/Sales', + href: '//amazon.ca/Sales' + }); + + url3.toObject('https').should.be.eql({ + type: 'url', + value: 'co.co?o=%2D&p=@gc#wat', + href: 'https://co.co?o=%2D&p=@gc#wat' + }); + }); + }); + + }); + + describe('EMAIL', function () { + var emailTextTokens, email; + + before(function () { + emailTextTokens = [ // test@example.com + new TEXT_TOKENS.DOMAIN('test'), + new TEXT_TOKENS.AT(), + new TEXT_TOKENS.DOMAIN('example'), + new TEXT_TOKENS.DOT(), + new TEXT_TOKENS.TLD('com') + ]; + email = new MULTI_TOKENS.EMAIL(emailTextTokens); + }); + + describe('#isLink', function () { + it('Is true in all cases', function () { + email.isLink.should.be.ok; + }); + }); + + describe('#toString()', function () { + it('Returns the exact email address text', function () { + email.toString().should.be.eql('test@example.com'); + }); + }); + + describe('#toHref()', function () { + it('Appends "mailto:" to the email address', function () { + email.toHref().should.be.eql('mailto:test@example.com'); + }); + }); + + }); + + describe('NL', function () { + var nlTokens, nl; + + before(function () { + nlTokens = [new TEXT_TOKENS.NL()]; + nl = new MULTI_TOKENS.NL(nlTokens); + }); + + describe('#isLink', function () { + it('Is false in all cases', function () { + nl.isLink.should.not.be.ok; + }); + }); + + describe('#toString()', function () { + it('Returns a single newline character', function () { + nl.toString().should.be.eql('\n'); + }); + }); + }); + + describe('TEXT', function () { + var textTokens, text; + + before(function () { + textTokens = [ // 'Hello, World!' + new TEXT_TOKENS.DOMAIN('Hello'), + new TEXT_TOKENS.SYM(','), + new TEXT_TOKENS.WS(' '), + new TEXT_TOKENS.DOMAIN('World'), + new TEXT_TOKENS.SYM('!') + ]; + text = new MULTI_TOKENS.NL(textTokens); + }); + + describe('#isLink', function () { + it('Is false in all cases', function () { + text.isLink.should.not.be.ok; + }); + }); + + describe('#toString()', function () { + it('Returns the original string text', function () { + text.toString().should.be.eql('Hello, World!'); + }); + }); + }); + +}); diff --git a/test/spec/tokens/text.js b/test/spec/tokens/text.js new file mode 100644 index 00000000..f5cbdd55 --- /dev/null +++ b/test/spec/tokens/text.js @@ -0,0 +1,47 @@ +var TEXT_TOKENS = require('../../../build/tokens/text'); + +describe('TEXT_TOKENS', function () { + + // Test for two commonly-used tokens + + describe('DOMAIN', function () { + var DOMAIN; + + before(function () { + DOMAIN = new TEXT_TOKENS.DOMAIN('abc123'); + }); + + describe('#type()', function () { + it('should have a type of DOMAIN', function () { + DOMAIN.type.should.eql('DOMAIN'); + }); + }); + + describe('#toString()', function () { + it ('should return the string "abc123"', function () { + DOMAIN.toString().should.eql('abc123'); + }); + }); + }); + + describe('AT', function () { + var at; + + before(function () { + at = new TEXT_TOKENS.AT('asdf'); // should ignore passed-in value + }); + + describe('#type()', function () { + it('should have a type of AT', function () { + at.type.should.eql('AT'); + }); + }); + + describe('#toString()', function () { + it ('should return the string "@"', function () { + at.toString().should.eql('@'); + }); + }); + }); + +}); diff --git a/tests/js/linkified.js b/tests/js/linkified.js deleted file mode 100644 index a57a1a1a..00000000 --- a/tests/js/linkified.js +++ /dev/null @@ -1,94 +0,0 @@ -/* global test:true */ -/* global equal:true */ - - -test("linkify string basics", function () { - - var linkifyTests = [{ - name: 'Basic test', - input: 'google.com', - output: 'google.com', - options: null - }, { - name: 'basic test with a different tag name', - input: 'I like google.com the most', - output: 'I like google.com the most', - options: { - tagName: 'span' - } - }, { - name: 'Capitalized domains should not be linkified', - input: 'I like Google.com the most', - output: 'I like Google.com the most', - options: null - }, { - name: 'Detect tow different links', - input: 'there are two tests, brennan.com and nick.ca -- do they work?', - output: 'there are two tests, brennan.com and nick.ca -- do they work?', - options: { - linkClass: 'alink', - target: '_parent' - } - }, { - name: 'Detect some links with strange punctuation around them', - input: 'there are two tests!brennan.com. and nick.ca? -- do they work?', - output: 'there are two tests!brennan.com. and nick.ca? -- do they work?', - options: { - linkClasses: ['alink', 'blink'], - linkAttributes: { - 'data-link-test': 'awesome\'s' - } - } - }, { - name: 'Links surrounded by irregular brackets', - input: 'This [i.imgur.com/ckSj2Ba.jpg)] should also work', - output: 'This [i.imgur.com/ckSj2Ba.jpg)] should also work', - options: { - linkAttributes: { - rel: 'nofollow' - } - } - }, { - name: 'Invalid top-level domains', - input: 'A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look', - output: 'A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look', - options: null - }, { - name: 'A lot of links, some consecutives', - input: 'SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com?q=.exe flickr.com/linktoimage.jpg', - output: 'SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com?q=.exe flickr.com/linktoimage.jpg', - options: null - }, { - name: 'Word separated by dots should not be links', - input: 'None.of these.should be.Links okay.please?', - output: 'None.of these.should be.Links okay.please?', - options: null - }, { - name: 'Email matching', - input: 'Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com', - output: 'Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com', - options: null - }, { - name: 'Single character domains', - input: 't.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?', - output: 't.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?', - options: null - }, { - name: 'Port numbers', - input: 'This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/', - output: 'This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/', - options: null - }]; - - for (var i = 0; i < linkifyTests.length; i++) { - equal( - (new Linkified( - linkifyTests[i].input, - linkifyTests[i].options - )).toString(), - linkifyTests[i].output, - linkifyTests[i].name - ); - } - -}); diff --git a/tests/linkified.html b/tests/linkified.html deleted file mode 100644 index 495934f5..00000000 --- a/tests/linkified.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Linkify Basic Tests - - - - - - - - - -
- - - \ No newline at end of file From 390ab15bb8d292816a5a46de5ae6650eae4f2ab3 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 2 Jan 2015 20:06:20 -0500 Subject: [PATCH 02/67] Very basic hashtag support As a way to demo the linkify plugin API - Totally works, but requires classes to create new kinds of tokens. This may mean having to convert the ES6 classes to regular JS classes somehow. --- .gitignore | 2 -- gulpfile.js | 2 +- src/linkify-hashtag.js | 3 +++ src/linkify.js | 6 +++--- src/parser/index.js | 3 ++- src/plugins/hashtag.js | 22 ++++++++++++++++++++++ src/scanner/index.js | 14 ++++++++++---- src/tokens/multi.js | 12 ++++++------ src/tokens/text.js | 38 ++++++++------------------------------ test/spec/parser/index.js | 4 ++++ test/spec/scanner/state.js | 1 + test/spec/tokens/text.js | 12 ------------ 12 files changed, 60 insertions(+), 59 deletions(-) create mode 100644 src/linkify-hashtag.js create mode 100644 src/plugins/hashtag.js diff --git a/.gitignore b/.gitignore index 062b2cf5..abb82ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,6 @@ Thumbs.DB node_modules bower_components -build/* -dist/* demo/dist/* # Logs diff --git a/gulpfile.js b/gulpfile.js index 38153e7a..74472e7b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -60,7 +60,7 @@ gulp.task('uglify', function () { // Build steps gulp.task('build', ['transpile']); gulp.task('dist', ['transpile', 'uglify']); -gulp.task('test', ['build', 'jshint', 'mocha']); +gulp.task('test', ['jshint', 'build', 'mocha']); /** Build app and begin watching for changes diff --git a/src/linkify-hashtag.js b/src/linkify-hashtag.js new file mode 100644 index 00000000..58688759 --- /dev/null +++ b/src/linkify-hashtag.js @@ -0,0 +1,3 @@ +let linkify = require('./linkify'); +require('./plugins/hashtag')(linkify); +module.exports = linkify; diff --git a/src/linkify.js b/src/linkify.js index 0109d74b..64e84941 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -33,8 +33,8 @@ let find = function (str) { // Scanner and parser provide states and tokens for the lexicographic stage // (will be used to add additional link types) module.exports = { - scanner: scanner, + find: find, parser: parser, - tokenize: tokenize, - find: find + scanner: scanner, + tokenize: tokenize }; diff --git a/src/parser/index.js b/src/parser/index.js index 01c6d1a9..3255244b 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -287,5 +287,6 @@ let run = function (tokens) { module.exports = { TOKENS: MULTI_TOKENS, State: State, - run: run + run: run, + start: S_START }; diff --git a/src/plugins/hashtag.js b/src/plugins/hashtag.js new file mode 100644 index 00000000..2b4307d3 --- /dev/null +++ b/src/plugins/hashtag.js @@ -0,0 +1,22 @@ +/** + Quick Hashtag parser plugin for linkify +*/ +module.exports = function (linkify) { + let + TT = linkify.scanner.TOKENS, // Text tokens + MT = linkify.parser.TOKENS, // Multi tokens + MultiToken = MT.Base, + S_START = linkify.parser.start, + S_HASH, S_HASHTAG; + + class HASHTAG extends MultiToken { + get type() { return 'hashtag'; } + get isLink() { return true; } + } + + S_HASH = new linkify.parser.State(); + S_HASHTAG = new linkify.parser.State(HASHTAG); + + S_START.on(TT.POUND, S_HASH); + S_HASH.on(TT.DOMAIN, S_HASHTAG); +}; diff --git a/src/scanner/index.js b/src/scanner/index.js index e0672cf7..90ea2e5b 100644 --- a/src/scanner/index.js +++ b/src/scanner/index.js @@ -101,6 +101,7 @@ S_START.on(REGEXP_NUM, S_NUM); S_NUM.on('-', S_DOMAIN_HYPHEN); S_NUM.on(REGEXP_NUM, S_NUM); S_NUM.on(REGEXP_ALPHANUM, S_DOMAIN); // number becomes DOMAIN +S_DOMAIN.on('-', S_DOMAIN_HYPHEN); S_DOMAIN.on(REGEXP_ALPHANUM, S_DOMAIN); // All the generated states should have a jump to DOMAIN @@ -116,9 +117,6 @@ S_DOMAIN_HYPHEN.on(REGEXP_ALPHANUM, S_DOMAIN); // Any other character is considered a single symbol token S_START.on(/./, makeState(TOKENS.SYM)); -// Tokens -exports.TOKENS = TOKENS; - /** Given a string, returns an array of TOKEN instances representing the composition of that string. @@ -127,7 +125,7 @@ exports.TOKENS = TOKENS; @param {String} str Input string to scan @return {Array} Array of TOKEN instances */ -exports.run = function (str) { +let run = function (str) { let lowerStr = str.toLowerCase(), // The state machine only looks at lowercase strings @@ -177,3 +175,11 @@ exports.run = function (str) { return tokens; }; + +module.exports = { + State: State, + TOKENS: TOKENS, + run: run, + start: S_START, + stateify: stateify +}; diff --git a/src/tokens/multi.js b/src/tokens/multi.js index d8836002..0f25f174 100644 --- a/src/tokens/multi.js +++ b/src/tokens/multi.js @@ -44,7 +44,7 @@ class MultiToken { @property type @default 'TOKEN' */ - get type() { return 'TOKEN'; } + get type() { return 'token'; } /** Is this multitoken a link? @@ -90,7 +90,7 @@ class MultiToken { */ toObject(protocol = 'http') { return { - type: this.type.toLowerCase(), + type: this.type, value: this.toString(), href: this.toHref(protocol) }; @@ -114,7 +114,7 @@ class MultiToken { @extends MultiToken */ class EMAIL extends MultiToken { - get type() { return 'EMAIL'; } + get type() { return 'email'; } get isLink() { return true; } toHref() { return 'mailto:' + this.toString(); @@ -127,7 +127,7 @@ class EMAIL extends MultiToken { @extends MultiToken */ class TEXT extends MultiToken { - get type() { return 'TEXT'; } + get type() { return 'text'; } } /** @@ -136,7 +136,7 @@ class TEXT extends MultiToken { @extends MultiToken */ class NL extends MultiToken { - get type() { return 'NL'; } + get type() { return 'nl'; } } /** @@ -145,7 +145,7 @@ class NL extends MultiToken { @extends MultiToken */ class URL extends MultiToken { - get type() { return 'URL'; } + get type() { return 'url'; } get isLink() { return true; } /** diff --git a/src/tokens/text.js b/src/tokens/text.js index 97a65077..c5fa1765 100644 --- a/src/tokens/text.js +++ b/src/tokens/text.js @@ -24,7 +24,6 @@ class TextToken { @property type @default 'TOKEN' */ - get type() { return 'TOKEN'; } toString() { return this.v + ''; @@ -46,9 +45,7 @@ class TextToken { @class DOMAIN @extends TextToken */ -class DOMAIN extends TextToken { - get type() { return 'DOMAIN'; } -} +class DOMAIN extends TextToken {} /** @class AT @@ -56,7 +53,6 @@ class DOMAIN extends TextToken { */ class AT extends TextToken { constructor() { super('@'); } - get type() { return 'AT'; } } /** @@ -67,7 +63,6 @@ class AT extends TextToken { */ class COLON extends TextToken { constructor() { super(':'); } - get type() { return 'COLON'; } } /** @@ -76,7 +71,6 @@ class COLON extends TextToken { */ class DOT extends TextToken { constructor() { super('.'); } - get type() { return 'DOT'; } } /** @@ -84,9 +78,8 @@ class DOT extends TextToken { @class LOCALHOST @extends TextToken */ -class LOCALHOST extends TextToken { - get type() { return 'LOCALHOST'; } -} +class LOCALHOST extends TextToken {} + /** Newline token @class NL @@ -94,16 +87,13 @@ class LOCALHOST extends TextToken { */ class NL extends TextToken { constructor() { super('\n'); } - get type() { return 'NL'; } } /** @class NUM @extends TextToken */ -class NUM extends TextToken { - get type() { return 'NUM'; } -} +class NUM extends TextToken {} /** @class PLUS @@ -111,7 +101,6 @@ class NUM extends TextToken { */ class PLUS extends TextToken { constructor() { super('+'); } - get type() { return 'PLUS'; } } /** @@ -120,7 +109,6 @@ class PLUS extends TextToken { */ class POUND extends TextToken { constructor() { super('#'); } - get type() { return 'POUND'; } } /** @@ -135,9 +123,7 @@ class POUND extends TextToken { @class PROTOCOL @extends TextToken */ -class PROTOCOL extends TextToken { - get type() { return 'PROTOCOL'; } -} +class PROTOCOL extends TextToken {} /** @class QUERY @@ -145,7 +131,6 @@ class PROTOCOL extends TextToken { */ class QUERY extends TextToken { constructor() { super('?'); } - get type() { return 'QUERY'; } } /** @@ -154,7 +139,6 @@ class QUERY extends TextToken { */ class SLASH extends TextToken { constructor() { super('/'); } - get type() { return 'SLASH'; } } /** @@ -162,17 +146,13 @@ class SLASH extends TextToken { @class SYM @extends TextToken */ -class SYM extends TextToken { - get type() { return 'SYM'; } -} +class SYM extends TextToken {} /** @class TLD @extends TextToken */ -class TLD extends TextToken { - get type() { return 'TLD'; } -} +class TLD extends TextToken {} /** Represents a string of consecutive whitespace characters @@ -180,9 +160,7 @@ class TLD extends TextToken { @class WS @extends TextToken */ -class WS extends TextToken { - get type() { return 'WS'; } -} +class WS extends TextToken {} module.exports = { Base: TextToken, diff --git a/test/spec/parser/index.js b/test/spec/parser/index.js index c54e0eac..bbd5380c 100644 --- a/test/spec/parser/index.js +++ b/test/spec/parser/index.js @@ -86,6 +86,10 @@ var tests = [ 'IP loops like email? 192.168.0.1@gmail.com works!!', [TEXT, EMAIL, TEXT], ['IP loops like email? ', '192.168.0.1@gmail.com', ' works!!'] + ], [ + 'Url like bro-215.co with a hyphen?', + [TEXT, URL, TEXT], + ['Url like ', 'bro-215.co', ' with a hyphen?'] ] // END: New linkify tests ]; diff --git a/test/spec/scanner/state.js b/test/spec/scanner/state.js index aac2fe73..ce6eaab1 100644 --- a/test/spec/scanner/state.js +++ b/test/spec/scanner/state.js @@ -60,6 +60,7 @@ describe('CharacterState', function () { describe('#test()', function () { it('Ensures characters match the given token or regexp', function () { S_START.test('a', 'a').should.be.ok; + S_START.test('a', 'b').should.not.be.ok; S_START.test('b', /[a-z]/).should.be.ok; S_START.test('\n', /[^\S\n]/).should.not.be.ok; }); diff --git a/test/spec/tokens/text.js b/test/spec/tokens/text.js index f5cbdd55..426de949 100644 --- a/test/spec/tokens/text.js +++ b/test/spec/tokens/text.js @@ -11,12 +11,6 @@ describe('TEXT_TOKENS', function () { DOMAIN = new TEXT_TOKENS.DOMAIN('abc123'); }); - describe('#type()', function () { - it('should have a type of DOMAIN', function () { - DOMAIN.type.should.eql('DOMAIN'); - }); - }); - describe('#toString()', function () { it ('should return the string "abc123"', function () { DOMAIN.toString().should.eql('abc123'); @@ -31,12 +25,6 @@ describe('TEXT_TOKENS', function () { at = new TEXT_TOKENS.AT('asdf'); // should ignore passed-in value }); - describe('#type()', function () { - it('should have a type of AT', function () { - at.type.should.eql('AT'); - }); - }); - describe('#toString()', function () { it ('should return the string "@"', function () { at.toString().should.eql('@'); From 3cb7101971f9eda94bd6d224237d39f12d4e7436 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 2 Jan 2015 20:18:46 -0500 Subject: [PATCH 03/67] Linkify `test` method To check if the given string is a linkable value --- src/linkify.js | 21 +++++++++++++++++++++ src/parser/index.js | 4 +--- src/scanner/index.js | 4 +--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/linkify.js b/src/linkify.js index 64e84941..3524a7dd 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -30,11 +30,32 @@ let find = function (str) { return filtered; }; +/** + Is the given string valid linkable text of some sort + Note that this does not trim the text for you. + + Optionally pass in a second `type` param, which is the type of link to test + for. + + For example, + + test(str, 'email'); + + Will return `true` if str is a valid email. +*/ +let test = function (str, type=null) { + let tokens = tokenize(str); + return tokens.length === 1 && tokens[0].isLink && ( + !type || tokens[0].type === type + ); +}; + // Scanner and parser provide states and tokens for the lexicographic stage // (will be used to add additional link types) module.exports = { find: find, parser: parser, scanner: scanner, + test: test, tokenize: tokenize }; diff --git a/src/parser/index.js b/src/parser/index.js index 3255244b..845e46d4 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -18,9 +18,7 @@ TEXT_TOKENS = require('../tokens/text'), MULTI_TOKENS = require('../tokens/multi'), State = require('./state'); -let makeState = function (tokenClass) { - return new State(tokenClass); -}; +let makeState = (tokenClass) => new State(tokenClass); const TT_DOMAIN = TEXT_TOKENS.DOMAIN, diff --git a/src/scanner/index.js b/src/scanner/index.js index 90ea2e5b..750e19ff 100644 --- a/src/scanner/index.js +++ b/src/scanner/index.js @@ -20,9 +20,7 @@ COLON = ':'; let domainStates = [], // states that jump to DOMAIN on /[a-z0-9]/ -makeState = function (tokenClass) { - return new State(tokenClass); -}; +makeState = (tokenClass) => new State(tokenClass); const // Frequently used tokens T_DOMAIN = TOKENS.DOMAIN, From 851119a8181e249205abd5f1e6e612bdc0bbb5b7 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 2 Jan 2015 20:45:02 -0500 Subject: [PATCH 04/67] Tests cases for linkify hashtags Also added .travis.yml file --- .travis.yml | 4 ++++ test/spec/plugins/hashtag.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .travis.yml create mode 100644 test/spec/plugins/hashtag.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..18ae2d89 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "0.11" + - "0.10" diff --git a/test/spec/plugins/hashtag.js b/test/spec/plugins/hashtag.js new file mode 100644 index 00000000..c2002a9e --- /dev/null +++ b/test/spec/plugins/hashtag.js @@ -0,0 +1,34 @@ +/*jshint -W030 */ +var +linkify = require('../../../build/linkify'), +hashtag = require('../../../build/plugins/hashtag'); + +describe('Linkify Hashtag Plugin', function () { + + it('Cannot parse hashtags before applying the plugin', function () { + linkify.find('There is a #hashtag #YOLO-2015 and #1234 and #%^&*( should not work') + .should.be.eql([]); + + linkify.test('#wat', 'hashtag').should.not.be.ok; + linkify.test('#987', 'hashtag').should.not.be.ok; + }); + + it ('Can parse hashtags after applying the plugin', function () { + + hashtag(linkify); + + linkify.find('There is a #hashtag #YOLO-2015 and #1234 and #%^&*( should not work') + .should.be.eql([{ + type: 'hashtag', + value: '#hashtag', + href: '#hashtag' + }, { + type: 'hashtag', + value: '#YOLO-2015', + href: '#YOLO-2015' + }]); + + linkify.test('#wat', 'hashtag').should.be.ok; + linkify.test('#987', 'hashtag').should.not.be.ok; + }); +}); From 7f45ef86f7b3776f9960dff452a4de41a1c737ce Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 3 Jan 2015 23:32:54 -0500 Subject: [PATCH 05/67] Basic String API for linkify Brings back functionality of taking a string of text and outputting a string of HTML with anchor tags. --- src/linkify-hashtag.js | 1 + src/linkify-string.js | 83 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/linkify-string.js diff --git a/src/linkify-hashtag.js b/src/linkify-hashtag.js index 58688759..0b6edac6 100644 --- a/src/linkify-hashtag.js +++ b/src/linkify-hashtag.js @@ -1,3 +1,4 @@ +// Linkify with basic hashtags support let linkify = require('./linkify'); require('./plugins/hashtag')(linkify); module.exports = linkify; diff --git a/src/linkify-string.js b/src/linkify-string.js new file mode 100644 index 00000000..6855b9fa --- /dev/null +++ b/src/linkify-string.js @@ -0,0 +1,83 @@ +/** + Convert strings of text into linkable HTML text +*/ + +let linkify = require('./linkify'); + +function typeToTarget(type) { + return type === 'url' ? '_blank' : null; +} + +function attributesToString(attributes) { + + if (!attributes) return ''; + let result = []; + + for (let attr in attributes) { + let val = (attributes[attr] + '').replace(/"/g, '"'); + result.push(`${attr}="${val}}"`); + } + return result.join(' '); +} + +/** + Options: + + tagName: 'a', + target: '_blank', + newLine: '\n', + linkClass: null, + linkAttributes: null, + format: null +*/ +module.exports = function (str, options) { + options = options || {}; + + let + tagName = options.tagName || 'a', + target = options.target || typeToTarget, + newLine = options.newLine || false, // deprecated + nl2br = !!newLine || options.nl2br || false, + format = options.format || null, + linkAttributes = options.linkAttributes || null, + attributesStr = linkAttributes ? attributesToString(linkAttributes) : null, + linkClass = 'linkified', + result = []; + + if (options.linkClass) { + linkClass += ' ' + options.linkClass; + } + + let tokens = linkify.tokenize(str); + + for (let token of tokens) { + if (token.isLink) { + let link = `<${tagName} href="${token.toHref()}" class="${linkClass}"`; + if (target) { + link += ` target=${target}`; + } + if (attributesStr) { + link += ` ${attributesStr}`; + } + + link += '>'; + link += typeof format === 'function' ? + format(token.toString(), token.type) : token.toString(); + link += ``; + + result.push(token); + + } else if (token.type === 'nl' && nl2br) { + if (newLine) { + result.push(newLine); + } else { + result.push('
\n'); + } + } else { + result.push(token.toString()); + } + } + + return result.join(''); + +}; From 027aaa9c3ec46900ddd2a7c79ae366cea88ac841 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 5 Jan 2015 23:08:35 -0500 Subject: [PATCH 06/67] Fixes for linkify-string and initial test cases --- src/linkify-string.js | 11 ++-- ...ify-hashtag.js => linkify-with-hashtag.js} | 1 + test/spec/linkify-string.js | 58 +++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) rename src/{linkify-hashtag.js => linkify-with-hashtag.js} (66%) create mode 100644 test/spec/linkify-string.js diff --git a/src/linkify-string.js b/src/linkify-string.js index 6855b9fa..a5b1a71c 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -23,12 +23,13 @@ function attributesToString(attributes) { /** Options: + format: null + linkAttributes: null, + linkClass: null, + newLine: '\n', // deprecated + nl2br: false, tagName: 'a', target: '_blank', - newLine: '\n', - linkClass: null, - linkAttributes: null, - format: null */ module.exports = function (str, options) { options = options || {}; @@ -65,7 +66,7 @@ module.exports = function (str, options) { format(token.toString(), token.type) : token.toString(); link += ``; - result.push(token); + result.push(link); } else if (token.type === 'nl' && nl2br) { if (newLine) { diff --git a/src/linkify-hashtag.js b/src/linkify-with-hashtag.js similarity index 66% rename from src/linkify-hashtag.js rename to src/linkify-with-hashtag.js index 0b6edac6..7deb0c58 100644 --- a/src/linkify-hashtag.js +++ b/src/linkify-with-hashtag.js @@ -1,3 +1,4 @@ +// NOTE: This file should only be used to build into a browser package // Linkify with basic hashtags support let linkify = require('./linkify'); require('./plugins/hashtag')(linkify); diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js new file mode 100644 index 00000000..9df18c24 --- /dev/null +++ b/test/spec/linkify-string.js @@ -0,0 +1,58 @@ +var linkifyStr = require('../../build/linkify-string'); + +/** + Gracefully truncate a string to a given limit. Will replace extraneous + text with a single ellipsis character (`…`). +*/ +String.prototype.truncate = function (limit) { + var string = this.toString(); + limit = limit || Infinity; + + if (limit <= 3) { + string = '…'; + } else if (string.length > limit) { + string = string.slice(0, limit).split(/\s/); + if (string.length > 1) { + string.pop(); + } + string = string.join(' ') + '…'; + } + return string; +}; + +describe('linkify-string', function () { + var + options = { // test options + tagName: 'span', + target: '_parent', + nl2br: true, + linkClass: 'my-linkify-class', + defaultProtocol: 'https', + linkAttributes: { + rel: 'nofollow', + onclick: 'javascript:;' + }, + format: function (val) { + return val.truncate(20); + } + }, + + // For each element in this array + // [0] - Original text + // [1] - Linkified with default options + // [2] - Linkified with new options + tests = [ + [ + 'Test with no links', + 'Test with no links', + 'Test with no links' + ], [ + 'The URL is google.com and the email is test@example.com', + 'The URL is = 0; - }; -} - /** ES6 ~> ES5 */ -gulp.task('transpile', function () { +gulp.task('6to5', function () { + return gulp.src(paths.src) + .pipe(to5()) + .pipe(gulp.dest('lib')); +}); + +// TODO - Vanilla globals version, probably with AMD +gulp.task('browser', function () { + var ext, + options = { + moduleRoot: 'linkifyjs', + }, + modules = { + amd: 'amd', + umd: 'umd' + }; + + for (var type in modules) { + ext = modules[type]; - gulp.src(paths.src) - .pipe(es6transpiler()) - .pipe(gulp.dest('build')); + gulp.src(paths.src) + .pipe(sourcemaps.init()) + .pipe(to5(extend({modules: type}, options))) + .pipe(concat('linkify.' + ext + '.js')) + .pipe(gulp.dest('build')); + } }); @@ -58,13 +73,13 @@ gulp.task('uglify', function () { }); // Build steps -gulp.task('build', ['transpile']); -gulp.task('dist', ['transpile', 'uglify']); +gulp.task('build', ['6to5']); +gulp.task('dist', ['6to5', 'uglify']); gulp.task('test', ['jshint', 'build', 'mocha']); /** Build app and begin watching for changes */ gulp.task('default', ['build'], function () { - gulp.watch(paths.src, ['transpile']); + gulp.watch(paths.src, ['6to5']); }); diff --git a/index.js b/index.js index 1c33e0bc..f80b499e 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./build/linkify'); +module.exports = require('./lib/linkify'); diff --git a/package.json b/package.json index 6ccfde02..0bf41411 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,15 @@ "chai": "^1.10.0", "glob": "^4.3.2", "gulp": "^3.8.10", + "gulp-6to5": "^2.0.0", + "gulp-concat": "^2.4.3", "gulp-es6-transpiler": "^1.0.1", "gulp-jshint": "^1.9.0", "gulp-mocha": "^2.0.0", + "gulp-sourcemaps": "^1.3.0", "gulp-uglify": "^1.0.2", - "jshint-stylish": "^1.0.0" + "jshint-stylish": "^1.0.0", + "lodash": "^2.4.1" }, "dependencies": {} } diff --git a/src/parser/index.js b/src/parser/index.js index 845e46d4..c0e42fd3 100644 --- a/src/parser/index.js +++ b/src/parser/index.js @@ -13,10 +13,9 @@ @main parser */ -let -TEXT_TOKENS = require('../tokens/text'), -MULTI_TOKENS = require('../tokens/multi'), -State = require('./state'); +import TEXT_TOKENS from '../tokens/text'; +import MULTI_TOKENS from '../tokens/multi'; +import State from './state'; let makeState = (tokenClass) => new State(tokenClass); @@ -282,7 +281,7 @@ let run = function (tokens) { return multis; }; -module.exports = { +export default { TOKENS: MULTI_TOKENS, State: State, run: run, diff --git a/src/parser/state.js b/src/parser/state.js index f7e83473..1e5a5a45 100644 --- a/src/parser/state.js +++ b/src/parser/state.js @@ -2,7 +2,7 @@ @module linkify @submodule parser */ -let BaseState = require('../state/base'); +import BaseState from '../state/base'; /** Subclass of @@ -25,4 +25,4 @@ class TokenState extends BaseState { } -module.exports = TokenState; +export default TokenState; diff --git a/src/plugins/hashtag.js b/src/plugins/hashtag.js index 2b4307d3..faffa3bd 100644 --- a/src/plugins/hashtag.js +++ b/src/plugins/hashtag.js @@ -1,7 +1,7 @@ /** Quick Hashtag parser plugin for linkify */ -module.exports = function (linkify) { +export default function (linkify) { let TT = linkify.scanner.TOKENS, // Text tokens MT = linkify.parser.TOKENS, // Multi tokens @@ -10,8 +10,11 @@ module.exports = function (linkify) { S_HASH, S_HASHTAG; class HASHTAG extends MultiToken { - get type() { return 'hashtag'; } - get isLink() { return true; } + constructor(value) { + super(value); + this.type = 'hashtag'; + this.isLink = true; + } } S_HASH = new linkify.parser.State(); @@ -19,4 +22,4 @@ module.exports = function (linkify) { S_START.on(TT.POUND, S_HASH); S_HASH.on(TT.DOMAIN, S_HASHTAG); -}; +} diff --git a/src/scanner/index.js b/src/scanner/index.js index 750e19ff..d4b5819b 100644 --- a/src/scanner/index.js +++ b/src/scanner/index.js @@ -7,11 +7,10 @@ @main scanner */ -let -TOKENS = require('../tokens/text'), -State = require('./state'), -stateify = require('./stateify'), -tlds = require('./tlds').complete; +import TOKENS from '../tokens/text'; +import State from './state'; +import stateify from './stateify'; +import {complete as tlds} from './tlds'; const REGEXP_NUM = /[0-9]/, @@ -174,7 +173,7 @@ let run = function (str) { return tokens; }; -module.exports = { +export default { State: State, TOKENS: TOKENS, run: run, diff --git a/src/scanner/state.js b/src/scanner/state.js index e5b51643..dc685d1f 100644 --- a/src/scanner/state.js +++ b/src/scanner/state.js @@ -2,7 +2,7 @@ @module linkify @submodule scanner */ -let BaseState = require('../state/base'); +import BaseState from '../state/base'; /** Subclass of @@ -28,4 +28,4 @@ class CharacterState extends BaseState { } -module.exports = CharacterState; +export default CharacterState; diff --git a/src/scanner/stateify.js b/src/scanner/stateify.js index 3f518212..5ed8782f 100644 --- a/src/scanner/stateify.js +++ b/src/scanner/stateify.js @@ -3,7 +3,7 @@ @submodule tokenizer */ -let CharacterState = require('./state'); +import CharacterState from './state'; /** Given a non-empty target string, generates states (if required) for each @@ -11,8 +11,11 @@ let CharacterState = require('./state'); the string. The final state will have a special value, as specified in options. All other "in between" substrings will have a default end state. + This turns the state machine into a Trie-like data structure (rather than a + intelligently-designed DFA). + Note that I haven't really tried these with any strings other than - DOMAINeric. + DOMAIN. @param {String} str @param {CharacterState} start State to jump from the first character @@ -22,7 +25,7 @@ let CharacterState = require('./state'); we don't have a full match @return {Array} list of newly-created states */ -module.exports = function (str, start, endToken, defaultToken) { +export default function (str, start, endToken, defaultToken) { let i = 0, len = str.length, @@ -51,4 +54,4 @@ module.exports = function (str, start, endToken, defaultToken) { state.on(str[len - 1], nextState); return newStates; -}; +} diff --git a/src/scanner/tlds.js b/src/scanner/tlds.js index 97ce2c0a..bf037489 100644 --- a/src/scanner/tlds.js +++ b/src/scanner/tlds.js @@ -4,9 +4,9 @@ // http://www.seobythesea.com/2006/01/googles-most-popular-and-least-popular-top-level-domains/ // .co and .io have also been added to the list -exports.essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); +export var essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); // http://data.iana.org/TLD/tlds-alpha-by-domain.txt -exports.complete = ( +export var complete = ( 'abogado|ac|academy|accountants|active|actor|ad|adult|ae|aero|af|ag|agency|ai|airforce|al|allfinanz|alsace|am|an|android|ao|aq|aquarelle|ar|archi|army|arpa|as|asia|associates|at|attorney|au|auction|audio|autos|aw|ax|axa|az|ba|band|bar|bargains|bayern|bb|bd|be|beer|berlin|best|bf|bg|bh|bi|bid|bike|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bmw|bn|bnpparibas|bo|boo|boutique|br|brussels|bs|bt|budapest|build|builders|business|buzz|bv|bw|by|bz|bzh|ca|cab|cal|camera|camp|cancerresearch|capetown|capital|caravan|cards|care|career|careers|casa|cash|cat|catering|cc|cd|center|ceo|cern|cf|cg|ch|channel|cheap|christmas|chrome|church|ci|citic|city|ck|cl|claims|cleaning|click|clinic|clothing|club|cm|cn|co|coach|codes|coffee|college|cologne|com|community|company|computer|condos|construction|consulting|contractors|cooking|cool|coop|country|cr|credit|creditcard|cricket|crs|cruises|cu|cuisinella|cv|cw|cx|cy|cymru|cz|dad|dance|dating|day|de|deals|degree|delivery|democrat|dental|dentist|desi|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|domains|durban|dvag|dz|eat|ec|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|equipment|er|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|fail|farm|fashion|feedback|fi|finance|financial|firmdale|fish|fishing|fitness|fj|fk|flights|florist|flsmidth|fly|fm|fo|foo|forsale|foundation|fr|frl|frogans|fund|furniture|futbol|ga|gal|gallery|gb|gbiz|gd|ge|gent|gf|gg|gh|gi|gift|gifts|gives|gl|glass|gle|global|globo|gm|gmail|gmo|gmx|gn|google|gop|gov|gp|gq|gr|graphics|gratis|green|gripe|gs|gt|gu|guide|guitars|guru|gw|gy|hamburg|haus|healthcare|help|here|hiphop|hiv|hk|hm|hn|holdings|holiday|homes|horse|host|hosting|house|how|hr|ht|hu|ibm|id|ie|il|im|immo|immobilien|in|industries|info|ing|ink|institute|insure|int|international|investments|io|iq|ir|irish|is|it|je|jetzt|jm|jo|jobs|joburg|jp|juegos|kaufen|ke|kg|kh|ki|kim|kitchen|kiwi|km|kn|koeln|kp|kr|krd|kred|kw|ky|kz|la|lacaixa|land|latrobe|lawyer|lb|lc|lds|lease|legal|lgbt|li|life|lighting|limited|limo|link|lk|loans|london|lotto|lr|ls|lt|ltda|lu|luxe|luxury|lv|ly|ma|madrid|maison|management|mango|market|marketing|mc|md|me|media|meet|melbourne|meme|memorial|menu|mg|mh|miami|mil|mini|mk|ml|mm|mn|mo|mobi|moda|moe|monash|money|mormon|mortgage|moscow|motorcycles|mov|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|nagoya|name|navy|nc|ne|net|network|neustar|new|nexus|nf|ng|ngo|nhk|ni|ninja|nl|no|np|nr|nra|nrw|nu|nyc|nz|okinawa|om|ong|onl|ooo|org|organic|otsuka|ovh|pa|paris|partners|parts|party|pe|pf|pg|ph|pharmacy|photo|photography|photos|physio|pics|pictures|pink|pizza|pk|pl|place|plumbing|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|properties|property|ps|pt|pub|pw|py|qa|qpon|quebec|re|realtor|recipes|red|rehab|reise|reisen|reit|ren|rentals|repair|report|republican|rest|restaurant|reviews|rich|rio|rip|ro|rocks|rodeo|rs|rsvp|ru|ruhr|rw|ryukyu|sa|saarland|sarl|sb|sc|sca|scb|schmidt|schule|science|scot|sd|se|services|sexy|sg|sh|shiksha|shoes|si|singles|sj|sk|sl|sm|sn|so|social|software|sohu|solar|solutions|soy|space|spiegel|sr|st|su|supplies|supply|support|surf|surgery|suzuki|sv|sx|sy|sydney|systems|sz|taipei|tatar|tattoo|tax|tc|td|technology|tel|tf|tg|th|tienda|tips|tirol|tj|tk|tl|tm|tn|to|today|tokyo|tools|top|town|toys|tp|tr|trade|training|travel|trust|tt|tui|tv|tw|tz|ua|ug|uk|university|uno|uol|us|uy|uz|va|vacations|vc|ve|vegas|ventures|versicherung|vet|vg|vi|viajes|villas|vision|vlaanderen|vn|vodka|vote|voting|voto|voyage|vu|wales|wang|watch|webcam|website|wed|wedding|wf|whoswho|wien|wiki|williamhill|wme|work|works|world|ws|wtc|wtf|xxx|xyz|yachts|yandex|ye|yoga|yokohama|youtube|yt|za|zip|zm|zone|zw' ).split('|'); diff --git a/src/tokens/multi.js b/src/tokens/multi.js index 0f25f174..98145e07 100644 --- a/src/tokens/multi.js +++ b/src/tokens/multi.js @@ -37,21 +37,21 @@ class MultiToken { */ constructor(value) { this.v = value; - } - /** - String representing the type for this token - @property type - @default 'TOKEN' - */ - get type() { return 'token'; } - - /** - Is this multitoken a link? - @property isLink - @default false - */ - get isLink() { return false; } + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + this.type = 'token'; + + /** + Is this multitoken a link? + @property isLink + @default false + */ + this.isLink = false; + } /** Return the string this token represents. @@ -114,8 +114,13 @@ class MultiToken { @extends MultiToken */ class EMAIL extends MultiToken { - get type() { return 'email'; } - get isLink() { return true; } + + constructor(value) { + super(value); + this.type = 'email'; + this.isLink = true; + } + toHref() { return 'mailto:' + this.toString(); } @@ -127,7 +132,10 @@ class EMAIL extends MultiToken { @extends MultiToken */ class TEXT extends MultiToken { - get type() { return 'text'; } + constructor(value) { + super(value); + this.type = 'text'; + } } /** @@ -136,7 +144,10 @@ class TEXT extends MultiToken { @extends MultiToken */ class NL extends MultiToken { - get type() { return 'nl'; } + constructor(value) { + super(value); + this.type = 'nl'; + } } /** @@ -145,8 +156,12 @@ class NL extends MultiToken { @extends MultiToken */ class URL extends MultiToken { - get type() { return 'url'; } - get isLink() { return true; } + + constructor(value) { + super(value); + this.type = 'url'; + this.isLink = true; + } /** Lowercases relevant parts of the domain and adds the protocol if diff --git a/test/benchmarks.js b/test/benchmarks.js index 4aae714e..b79b477e 100644 --- a/test/benchmarks.js +++ b/test/benchmarks.js @@ -1,4 +1,4 @@ -var scanner = require('../build/scanner'), sum = 0; +var scanner = require('../lib/scanner'), sum = 0; var ITERATIONS = 2000; diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js index 9df18c24..deac5ae6 100644 --- a/test/spec/linkify-string.js +++ b/test/spec/linkify-string.js @@ -1,4 +1,4 @@ -var linkifyStr = require('../../build/linkify-string'); +var linkifyStr = require('../../lib/linkify-string'); /** Gracefully truncate a string to a given limit. Will replace extraneous diff --git a/test/spec/parser/index.js b/test/spec/parser/index.js index bbd5380c..65a85860 100644 --- a/test/spec/parser/index.js +++ b/test/spec/parser/index.js @@ -1,7 +1,7 @@ var -scanner = require('../../../build/scanner'), -parser = require('../../../build/parser'), -MULTI_TOKENS = require('../../../build/tokens/multi'); +scanner = require('../../../lib/scanner'), +parser = require('../../../lib/parser'), +MULTI_TOKENS = require('../../../lib/tokens/multi'); var TEXT = MULTI_TOKENS.TEXT, diff --git a/test/spec/parser/state.js b/test/spec/parser/state.js index 7e17e3ba..8a6b8650 100644 --- a/test/spec/parser/state.js +++ b/test/spec/parser/state.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../build/tokens/text'), -TokenState = require('../../../build/parser/state'); +TEXT_TOKENS = require('../../../lib/tokens/text'), +TokenState = require('../../../lib/parser/state'); describe('TokenState', function () { var TS_START; diff --git a/test/spec/plugins/hashtag.js b/test/spec/plugins/hashtag.js index c2002a9e..1b2cf0e2 100644 --- a/test/spec/plugins/hashtag.js +++ b/test/spec/plugins/hashtag.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -linkify = require('../../../build/linkify'), -hashtag = require('../../../build/plugins/hashtag'); +linkify = require('../../../lib/linkify'), +hashtag = require('../../../lib/plugins/hashtag'); describe('Linkify Hashtag Plugin', function () { diff --git a/test/spec/scanner/index.js b/test/spec/scanner/index.js index 5a287cf6..c7772f37 100644 --- a/test/spec/scanner/index.js +++ b/test/spec/scanner/index.js @@ -1,6 +1,6 @@ var -scanner = require('../../../build/scanner'), -TEXT_TOKENS = require('../../../build/tokens/text'); +scanner = require('../../../lib/scanner'), +TEXT_TOKENS = require('../../../lib/tokens/text'); var DOMAIN = TEXT_TOKENS.DOMAIN, diff --git a/test/spec/scanner/state.js b/test/spec/scanner/state.js index ce6eaab1..18e5455e 100644 --- a/test/spec/scanner/state.js +++ b/test/spec/scanner/state.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../build/tokens/text'), -CharacterState = require('../../../build/scanner/state'); +TEXT_TOKENS = require('../../../lib/tokens/text'), +CharacterState = require('../../../lib/scanner/state'); describe('CharacterState', function () { var S_START, S_DOT, S_NUM; diff --git a/test/spec/scanner/stateify.js b/test/spec/scanner/stateify.js index 22a1e3e3..e14f54ec 100644 --- a/test/spec/scanner/stateify.js +++ b/test/spec/scanner/stateify.js @@ -1,7 +1,7 @@ var -TOKENS = require('../../../build/tokens/text'), -State = require('../../../build/scanner/state'), -stateify = require('../../../build/scanner/stateify'); +TOKENS = require('../../../lib/tokens/text'), +State = require('../../../lib/scanner/state'), +stateify = require('../../../lib/scanner/stateify'); describe('stateify', function () { var S_START; diff --git a/test/spec/tokens/multi.js b/test/spec/tokens/multi.js index 7c089208..9d627d96 100644 --- a/test/spec/tokens/multi.js +++ b/test/spec/tokens/multi.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../build/tokens/text'), -MULTI_TOKENS = require('../../../build/tokens/multi'); +TEXT_TOKENS = require('../../../lib/tokens/text'), +MULTI_TOKENS = require('../../../lib/tokens/multi'); describe('MULTI_TOKENS', function () { diff --git a/test/spec/tokens/text.js b/test/spec/tokens/text.js index 426de949..06331202 100644 --- a/test/spec/tokens/text.js +++ b/test/spec/tokens/text.js @@ -1,4 +1,4 @@ -var TEXT_TOKENS = require('../../../build/tokens/text'); +var TEXT_TOKENS = require('../../../lib/tokens/text'); describe('TEXT_TOKENS', function () { From bc935acec38ae2c884730f9ff2e55efd84924c8c Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 12 Jan 2015 18:53:25 -0500 Subject: [PATCH 08/67] Quick test cases for linkify-string Needs a few more but this should be good for now --- src/linkify-string.js | 29 ++++++++++++++++++++--------- test/spec/linkify-string.js | 11 ++++++++++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/linkify-string.js b/src/linkify-string.js index a5b1a71c..41129bbf 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -1,5 +1,6 @@ /** Convert strings of text into linkable HTML text + TODO: Support for computed attributes based on the type? */ let linkify = require('./linkify'); @@ -15,7 +16,7 @@ function attributesToString(attributes) { for (let attr in attributes) { let val = (attributes[attr] + '').replace(/"/g, '"'); - result.push(`${attr}="${val}}"`); + result.push(`${attr}="${val}"`); } return result.join(' '); } @@ -23,6 +24,7 @@ function attributesToString(attributes) { /** Options: + defaultProtocol: 'http' format: null linkAttributes: null, linkClass: null, @@ -35,13 +37,13 @@ module.exports = function (str, options) { options = options || {}; let + defaultProtocol = options.defaultProtocol || 'http', tagName = options.tagName || 'a', target = options.target || typeToTarget, newLine = options.newLine || false, // deprecated nl2br = !!newLine || options.nl2br || false, format = options.format || null, - linkAttributes = options.linkAttributes || null, - attributesStr = linkAttributes ? attributesToString(linkAttributes) : null, + attributes = options.linkAttributes || null, linkClass = 'linkified', result = []; @@ -51,14 +53,23 @@ module.exports = function (str, options) { let tokens = linkify.tokenize(str); - for (let token of tokens) { + for (let i = 0; i < tokens.length; i++ ) { + let token = tokens[i]; if (token.isLink) { - let link = `<${tagName} href="${token.toHref()}" class="${linkClass}"`; - if (target) { - link += ` target=${target}`; + + let + link = `<${tagName} href="${token.toHref(defaultProtocol)}" class="${linkClass}"`, + targetStr = typeof target === 'function' ? + target(token.type) : target, + attributesHash = typeof attributes === 'function' ? + attributes(token.type) : attributes; + + if (targetStr) { + link += ` target="${targetStr}"`; } - if (attributesStr) { - link += ` ${attributesStr}`; + + if (attributesHash) { + link += ` ${attributesToString(attributesHash)}`; } link += '>'; diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js index deac5ae6..5e760ec5 100644 --- a/test/spec/linkify-string.js +++ b/test/spec/linkify-string.js @@ -48,11 +48,20 @@ describe('linkify-string', function () { 'Test with no links' ], [ 'The URL is google.com and the email is test@example.com', - 'The URL is google.com and the email is test@example.com', + 'The URL is google.com and the email is test@example.com' ] ]; it('Works with default options', function () { + tests.forEach(function (test) { + linkifyStr(test[0]).should.be.eql(test[1]); + }); + }); + it('Works with overriden options', function () { + tests.forEach(function (test) { + linkifyStr(test[0], options).should.be.eql(test[2]); + }); }); }); From 5e9788c042d7365fe2e51f683a17c14cdaadadf8 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 12 Jan 2015 18:55:38 -0500 Subject: [PATCH 09/67] String jshing error, this fixes it. --- test/spec/linkify-string.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js index 5e760ec5..b6fa9343 100644 --- a/test/spec/linkify-string.js +++ b/test/spec/linkify-string.js @@ -1,3 +1,5 @@ +/*jshint scripturl:true*/ + var linkifyStr = require('../../lib/linkify-string'); /** From 1f4827cb9e7d9a538ceec8202e03cd661c6bf72c Mon Sep 17 00:00:00 2001 From: nfrasser Date: Tue, 13 Jan 2015 21:26:49 -0500 Subject: [PATCH 10/67] Directory restructure to better play with modules Much cleaner this way! --- .npmignore | 2 + gulpfile.js | 64 +++++++++++-------- package.json | 1 + src/{parser/index.js => core/parser.js} | 6 +- src/{scanner/index.js => core/scanner.js} | 8 +-- src/{ => core}/state/base.js | 0 .../state.js => core/state/character.js} | 2 +- src/{scanner => core/state}/stateify.js | 2 +- src/{parser/state.js => core/state/token.js} | 2 +- src/{scanner => core}/tlds.js | 4 +- src/{ => core}/tokens/multi.js | 0 src/{ => core}/tokens/text.js | 0 src/linkify-string.js | 7 +- src/linkify-with-hashtag.js | 4 +- src/linkify.js | 7 +- src/plugins/hashtag.js | 1 + test/spec/{parser/index.js => core/parser.js} | 6 +- .../{scanner/index.js => core/scanner.js} | 6 +- .../state.js => core/state/character.js} | 4 +- test/spec/{scanner => core/state}/stateify.js | 6 +- .../{parser/state.js => core/state/token.js} | 4 +- test/spec/{ => core}/tokens/multi.js | 4 +- test/spec/{ => core}/tokens/text.js | 2 +- 23 files changed, 79 insertions(+), 63 deletions(-) rename src/{parser/index.js => core/parser.js} (98%) rename src/{scanner/index.js => core/scanner.js} (97%) rename src/{ => core}/state/base.js (100%) rename src/{scanner/state.js => core/state/character.js} (92%) rename src/{scanner => core/state}/stateify.js (97%) rename src/{parser/state.js => core/state/token.js} (91%) rename src/{scanner => core}/tlds.js (96%) rename src/{ => core}/tokens/multi.js (100%) rename src/{ => core}/tokens/text.js (100%) rename test/spec/{parser/index.js => core/parser.js} (96%) rename test/spec/{scanner/index.js => core/scanner.js} (94%) rename test/spec/{scanner/state.js => core/state/character.js} (93%) rename test/spec/{scanner => core/state}/stateify.js (90%) rename test/spec/{parser/state.js => core/state/token.js} (84%) rename test/spec/{ => core}/tokens/multi.js (97%) rename test/spec/{ => core}/tokens/text.js (91%) diff --git a/.npmignore b/.npmignore index 6d78225f..9e54bc92 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,8 @@ # All compiled code will be in the "lib" and build folders +amd assets bower_components +build demo src test diff --git a/gulpfile.js b/gulpfile.js index 87a53c29..88306d08 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,23 +1,26 @@ var gulp = require('gulp'), stylish = require('jshint-stylish'), -extend = require('lodash').extend; +amdOptimize = require("amd-optimize");; var // Gulp plugins concat = require('gulp-concat'), jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), +// rjs = require('gulp-r'), sourcemaps = require('gulp-sourcemaps'), to5 = require('gulp-6to5'), uglify = require('gulp-uglify'); +wrap = require('gulp-wrap'); var paths = { src: 'src/**/*.js', + amd: 'build/amd/**/*.js', test: 'test/index.js', spec: 'test/spec/**.js' }; /** - ES6 ~> ES5 + ES6 ~> 6to5 (with CJS Node Modules) */ gulp.task('6to5', function () { return gulp.src(paths.src) @@ -25,28 +28,38 @@ gulp.task('6to5', function () { .pipe(gulp.dest('lib')); }); -// TODO - Vanilla globals version, probably with AMD -gulp.task('browser', function () { - var ext, - options = { - moduleRoot: 'linkifyjs', - }, - modules = { - amd: 'amd', - umd: 'umd' - }; - - for (var type in modules) { - ext = modules[type]; +/** + ES6 to 6to5 AMD modules +*/ +gulp.task('6to5-amd', function () { + gulp.src(paths.src) + .pipe(to5({ + modules: 'amd', + moduleIds: true, + // moduleRoot: 'linkifyjs' + })) + .pipe(gulp.dest('build/amd')) + .pipe(amdOptimize('linkify', { + paths: { + parser: 'build/amd/parser/index', + scanner: 'build/amd/scanner/index' + } + })) + .pipe(concat('linkify.amd.js')) + .pipe(gulp.dest('build')); +}); - gulp.src(paths.src) - .pipe(sourcemaps.init()) - .pipe(to5(extend({modules: type}, options))) - .pipe(concat('linkify.' + ext + '.js')) - .pipe(gulp.dest('build')); - } +// gulp.task('amd', function () { +// gulp.src(paths.amd) +// }); -}); +// gulp.task('rjs', function () { +// gulp.src(paths.amd) +// .pipe(rjs({ +// baseUrl: __dirname + '/build/amd/' +// })) +// .pipe(gulp.dest('dist/amd')); +// }) /** Lint using jshint @@ -73,13 +86,14 @@ gulp.task('uglify', function () { }); // Build steps -gulp.task('build', ['6to5']); -gulp.task('dist', ['6to5', 'uglify']); +gulp.task('build', ['6to5', '6to5-amd']); + +gulp.task('dist', ['6to5', '6to5-amd', 'uglify']); gulp.task('test', ['jshint', 'build', 'mocha']); /** Build app and begin watching for changes */ -gulp.task('default', ['build'], function () { +gulp.task('default', ['6to5'], function () { gulp.watch(paths.src, ['6to5']); }); diff --git a/package.json b/package.json index 0bf41411..631977bd 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "gulp-mocha": "^2.0.0", "gulp-sourcemaps": "^1.3.0", "gulp-uglify": "^1.0.2", + "gulp-wrap": "^0.8.0", "jshint-stylish": "^1.0.0", "lodash": "^2.4.1" }, diff --git a/src/parser/index.js b/src/core/parser.js similarity index 98% rename from src/parser/index.js rename to src/core/parser.js index c0e42fd3..d2c388af 100644 --- a/src/parser/index.js +++ b/src/core/parser.js @@ -13,9 +13,9 @@ @main parser */ -import TEXT_TOKENS from '../tokens/text'; -import MULTI_TOKENS from '../tokens/multi'; -import State from './state'; +import TEXT_TOKENS from './tokens/text'; +import MULTI_TOKENS from './tokens/multi'; +import State from './state/token'; let makeState = (tokenClass) => new State(tokenClass); diff --git a/src/scanner/index.js b/src/core/scanner.js similarity index 97% rename from src/scanner/index.js rename to src/core/scanner.js index d4b5819b..6dd663e4 100644 --- a/src/scanner/index.js +++ b/src/core/scanner.js @@ -7,10 +7,10 @@ @main scanner */ -import TOKENS from '../tokens/text'; -import State from './state'; -import stateify from './stateify'; -import {complete as tlds} from './tlds'; +import TOKENS from './tokens/text'; +import State from './state/character'; +import stateify from './state/stateify'; +import tlds from './tlds'; const REGEXP_NUM = /[0-9]/, diff --git a/src/state/base.js b/src/core/state/base.js similarity index 100% rename from src/state/base.js rename to src/core/state/base.js diff --git a/src/scanner/state.js b/src/core/state/character.js similarity index 92% rename from src/scanner/state.js rename to src/core/state/character.js index dc685d1f..55e5a440 100644 --- a/src/scanner/state.js +++ b/src/core/state/character.js @@ -2,7 +2,7 @@ @module linkify @submodule scanner */ -import BaseState from '../state/base'; +import BaseState from './base'; /** Subclass of diff --git a/src/scanner/stateify.js b/src/core/state/stateify.js similarity index 97% rename from src/scanner/stateify.js rename to src/core/state/stateify.js index 5ed8782f..8b9d5bdb 100644 --- a/src/scanner/stateify.js +++ b/src/core/state/stateify.js @@ -3,7 +3,7 @@ @submodule tokenizer */ -import CharacterState from './state'; +import CharacterState from './character'; /** Given a non-empty target string, generates states (if required) for each diff --git a/src/parser/state.js b/src/core/state/token.js similarity index 91% rename from src/parser/state.js rename to src/core/state/token.js index 1e5a5a45..85bdf277 100644 --- a/src/parser/state.js +++ b/src/core/state/token.js @@ -2,7 +2,7 @@ @module linkify @submodule parser */ -import BaseState from '../state/base'; +import BaseState from './base'; /** Subclass of diff --git a/src/scanner/tlds.js b/src/core/tlds.js similarity index 96% rename from src/scanner/tlds.js rename to src/core/tlds.js index bf037489..f4649334 100644 --- a/src/scanner/tlds.js +++ b/src/core/tlds.js @@ -4,9 +4,9 @@ // http://www.seobythesea.com/2006/01/googles-most-popular-and-least-popular-top-level-domains/ // .co and .io have also been added to the list -export var essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); +// export var essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); // http://data.iana.org/TLD/tlds-alpha-by-domain.txt -export var complete = ( +export default ( 'abogado|ac|academy|accountants|active|actor|ad|adult|ae|aero|af|ag|agency|ai|airforce|al|allfinanz|alsace|am|an|android|ao|aq|aquarelle|ar|archi|army|arpa|as|asia|associates|at|attorney|au|auction|audio|autos|aw|ax|axa|az|ba|band|bar|bargains|bayern|bb|bd|be|beer|berlin|best|bf|bg|bh|bi|bid|bike|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bmw|bn|bnpparibas|bo|boo|boutique|br|brussels|bs|bt|budapest|build|builders|business|buzz|bv|bw|by|bz|bzh|ca|cab|cal|camera|camp|cancerresearch|capetown|capital|caravan|cards|care|career|careers|casa|cash|cat|catering|cc|cd|center|ceo|cern|cf|cg|ch|channel|cheap|christmas|chrome|church|ci|citic|city|ck|cl|claims|cleaning|click|clinic|clothing|club|cm|cn|co|coach|codes|coffee|college|cologne|com|community|company|computer|condos|construction|consulting|contractors|cooking|cool|coop|country|cr|credit|creditcard|cricket|crs|cruises|cu|cuisinella|cv|cw|cx|cy|cymru|cz|dad|dance|dating|day|de|deals|degree|delivery|democrat|dental|dentist|desi|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|domains|durban|dvag|dz|eat|ec|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|equipment|er|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|fail|farm|fashion|feedback|fi|finance|financial|firmdale|fish|fishing|fitness|fj|fk|flights|florist|flsmidth|fly|fm|fo|foo|forsale|foundation|fr|frl|frogans|fund|furniture|futbol|ga|gal|gallery|gb|gbiz|gd|ge|gent|gf|gg|gh|gi|gift|gifts|gives|gl|glass|gle|global|globo|gm|gmail|gmo|gmx|gn|google|gop|gov|gp|gq|gr|graphics|gratis|green|gripe|gs|gt|gu|guide|guitars|guru|gw|gy|hamburg|haus|healthcare|help|here|hiphop|hiv|hk|hm|hn|holdings|holiday|homes|horse|host|hosting|house|how|hr|ht|hu|ibm|id|ie|il|im|immo|immobilien|in|industries|info|ing|ink|institute|insure|int|international|investments|io|iq|ir|irish|is|it|je|jetzt|jm|jo|jobs|joburg|jp|juegos|kaufen|ke|kg|kh|ki|kim|kitchen|kiwi|km|kn|koeln|kp|kr|krd|kred|kw|ky|kz|la|lacaixa|land|latrobe|lawyer|lb|lc|lds|lease|legal|lgbt|li|life|lighting|limited|limo|link|lk|loans|london|lotto|lr|ls|lt|ltda|lu|luxe|luxury|lv|ly|ma|madrid|maison|management|mango|market|marketing|mc|md|me|media|meet|melbourne|meme|memorial|menu|mg|mh|miami|mil|mini|mk|ml|mm|mn|mo|mobi|moda|moe|monash|money|mormon|mortgage|moscow|motorcycles|mov|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|nagoya|name|navy|nc|ne|net|network|neustar|new|nexus|nf|ng|ngo|nhk|ni|ninja|nl|no|np|nr|nra|nrw|nu|nyc|nz|okinawa|om|ong|onl|ooo|org|organic|otsuka|ovh|pa|paris|partners|parts|party|pe|pf|pg|ph|pharmacy|photo|photography|photos|physio|pics|pictures|pink|pizza|pk|pl|place|plumbing|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|properties|property|ps|pt|pub|pw|py|qa|qpon|quebec|re|realtor|recipes|red|rehab|reise|reisen|reit|ren|rentals|repair|report|republican|rest|restaurant|reviews|rich|rio|rip|ro|rocks|rodeo|rs|rsvp|ru|ruhr|rw|ryukyu|sa|saarland|sarl|sb|sc|sca|scb|schmidt|schule|science|scot|sd|se|services|sexy|sg|sh|shiksha|shoes|si|singles|sj|sk|sl|sm|sn|so|social|software|sohu|solar|solutions|soy|space|spiegel|sr|st|su|supplies|supply|support|surf|surgery|suzuki|sv|sx|sy|sydney|systems|sz|taipei|tatar|tattoo|tax|tc|td|technology|tel|tf|tg|th|tienda|tips|tirol|tj|tk|tl|tm|tn|to|today|tokyo|tools|top|town|toys|tp|tr|trade|training|travel|trust|tt|tui|tv|tw|tz|ua|ug|uk|university|uno|uol|us|uy|uz|va|vacations|vc|ve|vegas|ventures|versicherung|vet|vg|vi|viajes|villas|vision|vlaanderen|vn|vodka|vote|voting|voto|voyage|vu|wales|wang|watch|webcam|website|wed|wedding|wf|whoswho|wien|wiki|williamhill|wme|work|works|world|ws|wtc|wtf|xxx|xyz|yachts|yandex|ye|yoga|yokohama|youtube|yt|za|zip|zm|zone|zw' ).split('|'); diff --git a/src/tokens/multi.js b/src/core/tokens/multi.js similarity index 100% rename from src/tokens/multi.js rename to src/core/tokens/multi.js diff --git a/src/tokens/text.js b/src/core/tokens/text.js similarity index 100% rename from src/tokens/text.js rename to src/core/tokens/text.js diff --git a/src/linkify-string.js b/src/linkify-string.js index 41129bbf..3643cd1e 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -3,7 +3,7 @@ TODO: Support for computed attributes based on the type? */ -let linkify = require('./linkify'); +import linkify from './linkify'; function typeToTarget(type) { return type === 'url' ? '_blank' : null; @@ -33,7 +33,7 @@ function attributesToString(attributes) { tagName: 'a', target: '_blank', */ -module.exports = function (str, options) { +export default function (str, options) { options = options || {}; let @@ -91,5 +91,4 @@ module.exports = function (str, options) { } return result.join(''); - -}; +} diff --git a/src/linkify-with-hashtag.js b/src/linkify-with-hashtag.js index 7deb0c58..edd1ffa6 100644 --- a/src/linkify-with-hashtag.js +++ b/src/linkify-with-hashtag.js @@ -1,5 +1,5 @@ // NOTE: This file should only be used to build into a browser package // Linkify with basic hashtags support -let linkify = require('./linkify'); +import linkify from './linkify'; require('./plugins/hashtag')(linkify); -module.exports = linkify; +export default linkify; diff --git a/src/linkify.js b/src/linkify.js index 3524a7dd..362de232 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -1,6 +1,5 @@ -let -scanner = require('./scanner'), -parser = require('./parser'); +import scanner from './core/scanner'; +import parser from './core/parser'; /** Converts a string into tokens that represent linkable and non-linkable bits @@ -52,7 +51,7 @@ let test = function (str, type=null) { // Scanner and parser provide states and tokens for the lexicographic stage // (will be used to add additional link types) -module.exports = { +export default { find: find, parser: parser, scanner: scanner, diff --git a/src/plugins/hashtag.js b/src/plugins/hashtag.js index faffa3bd..369634ea 100644 --- a/src/plugins/hashtag.js +++ b/src/plugins/hashtag.js @@ -22,4 +22,5 @@ export default function (linkify) { S_START.on(TT.POUND, S_HASH); S_HASH.on(TT.DOMAIN, S_HASHTAG); + S_HASH.on(TT.TLD, S_HASHTAG); } diff --git a/test/spec/parser/index.js b/test/spec/core/parser.js similarity index 96% rename from test/spec/parser/index.js rename to test/spec/core/parser.js index 65a85860..efda2801 100644 --- a/test/spec/parser/index.js +++ b/test/spec/core/parser.js @@ -1,7 +1,7 @@ var -scanner = require('../../../lib/scanner'), -parser = require('../../../lib/parser'), -MULTI_TOKENS = require('../../../lib/tokens/multi'); +scanner = require('../../../lib/core/scanner'), +parser = require('../../../lib/core/parser'), +MULTI_TOKENS = require('../../../lib/core/tokens/multi'); var TEXT = MULTI_TOKENS.TEXT, diff --git a/test/spec/scanner/index.js b/test/spec/core/scanner.js similarity index 94% rename from test/spec/scanner/index.js rename to test/spec/core/scanner.js index c7772f37..f198198d 100644 --- a/test/spec/scanner/index.js +++ b/test/spec/core/scanner.js @@ -1,9 +1,9 @@ var -scanner = require('../../../lib/scanner'), -TEXT_TOKENS = require('../../../lib/tokens/text'); +scanner = require('../../../lib/core/scanner'), +TEXT_TOKENS = require('../../../lib/core/tokens/text'); var -DOMAIN = TEXT_TOKENS.DOMAIN, +DOMAIN = TEXT_TOKENS.DOMAIN, AT = TEXT_TOKENS.AT, COLON = TEXT_TOKENS.COLON, DOT = TEXT_TOKENS.DOT, diff --git a/test/spec/scanner/state.js b/test/spec/core/state/character.js similarity index 93% rename from test/spec/scanner/state.js rename to test/spec/core/state/character.js index 18e5455e..e1f415fd 100644 --- a/test/spec/scanner/state.js +++ b/test/spec/core/state/character.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../lib/tokens/text'), -CharacterState = require('../../../lib/scanner/state'); +TEXT_TOKENS = require('../../../../lib/core/tokens/text'), +CharacterState = require('../../../../lib/core/state/character'); describe('CharacterState', function () { var S_START, S_DOT, S_NUM; diff --git a/test/spec/scanner/stateify.js b/test/spec/core/state/stateify.js similarity index 90% rename from test/spec/scanner/stateify.js rename to test/spec/core/state/stateify.js index e14f54ec..c6384a8a 100644 --- a/test/spec/scanner/stateify.js +++ b/test/spec/core/state/stateify.js @@ -1,7 +1,7 @@ var -TOKENS = require('../../../lib/tokens/text'), -State = require('../../../lib/scanner/state'), -stateify = require('../../../lib/scanner/stateify'); +TOKENS = require('../../../../lib/core/tokens/text'), +State = require('../../../../lib/core/state/character'), +stateify = require('../../../../lib/core/state/stateify'); describe('stateify', function () { var S_START; diff --git a/test/spec/parser/state.js b/test/spec/core/state/token.js similarity index 84% rename from test/spec/parser/state.js rename to test/spec/core/state/token.js index 8a6b8650..42ab2d21 100644 --- a/test/spec/parser/state.js +++ b/test/spec/core/state/token.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../lib/tokens/text'), -TokenState = require('../../../lib/parser/state'); +TEXT_TOKENS = require('../../../../lib/core/tokens/text'), +TokenState = require('../../../../lib/core/state/token'); describe('TokenState', function () { var TS_START; diff --git a/test/spec/tokens/multi.js b/test/spec/core/tokens/multi.js similarity index 97% rename from test/spec/tokens/multi.js rename to test/spec/core/tokens/multi.js index 9d627d96..e844997e 100644 --- a/test/spec/tokens/multi.js +++ b/test/spec/core/tokens/multi.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../lib/tokens/text'), -MULTI_TOKENS = require('../../../lib/tokens/multi'); +TEXT_TOKENS = require('../../../../lib/core/tokens/text'), +MULTI_TOKENS = require('../../../../lib/core/tokens/multi'); describe('MULTI_TOKENS', function () { diff --git a/test/spec/tokens/text.js b/test/spec/core/tokens/text.js similarity index 91% rename from test/spec/tokens/text.js rename to test/spec/core/tokens/text.js index 06331202..5b50dc94 100644 --- a/test/spec/tokens/text.js +++ b/test/spec/core/tokens/text.js @@ -1,4 +1,4 @@ -var TEXT_TOKENS = require('../../../lib/tokens/text'); +var TEXT_TOKENS = require('../../../../lib/core/tokens/text'); describe('TEXT_TOKENS', function () { From 987f65fd6e2d5c4b459590a15bbbe455bcbead82 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Tue, 13 Jan 2015 21:49:45 -0500 Subject: [PATCH 11/67] Moving everything yet again to make the AMD compiler happy This is brutal :( --- .jshintrc | 1 + amdwrapper.js | 159 ++++++++++++++++++ gulpfile.js | 10 +- src/linkify-with-hashtag.js | 3 +- src/linkify.js | 4 +- src/{ => linkify}/core/parser.js | 0 src/{ => linkify}/core/scanner.js | 0 src/{ => linkify}/core/state/base.js | 0 src/{ => linkify}/core/state/character.js | 0 src/{ => linkify}/core/state/stateify.js | 0 src/{ => linkify}/core/state/token.js | 0 src/{ => linkify}/core/tlds.js | 0 src/{ => linkify}/core/tokens/multi.js | 0 src/{ => linkify}/core/tokens/text.js | 0 src/{ => linkify}/plugins/hashtag.js | 0 test/index.html | 14 ++ test/index.js | 1 + test/spec/linkify-string.js | 2 +- test/spec/{ => linkify}/core/parser.js | 6 +- test/spec/{ => linkify}/core/scanner.js | 4 +- .../{ => linkify}/core/state/character.js | 4 +- .../spec/{ => linkify}/core/state/stateify.js | 6 +- test/spec/{ => linkify}/core/state/token.js | 4 +- test/spec/{ => linkify}/core/tokens/multi.js | 4 +- test/spec/{ => linkify}/core/tokens/text.js | 2 +- test/spec/{ => linkify}/plugins/hashtag.js | 4 +- test/testem.json | 6 + testem.json | 6 + 28 files changed, 214 insertions(+), 26 deletions(-) create mode 100644 amdwrapper.js rename src/{ => linkify}/core/parser.js (100%) rename src/{ => linkify}/core/scanner.js (100%) rename src/{ => linkify}/core/state/base.js (100%) rename src/{ => linkify}/core/state/character.js (100%) rename src/{ => linkify}/core/state/stateify.js (100%) rename src/{ => linkify}/core/state/token.js (100%) rename src/{ => linkify}/core/tlds.js (100%) rename src/{ => linkify}/core/tokens/multi.js (100%) rename src/{ => linkify}/core/tokens/text.js (100%) rename src/{ => linkify}/plugins/hashtag.js (100%) create mode 100644 test/index.html rename test/spec/{ => linkify}/core/parser.js (96%) rename test/spec/{ => linkify}/core/scanner.js (95%) rename test/spec/{ => linkify}/core/state/character.js (92%) rename test/spec/{ => linkify}/core/state/stateify.js (89%) rename test/spec/{ => linkify}/core/state/token.js (82%) rename test/spec/{ => linkify}/core/tokens/multi.js (97%) rename test/spec/{ => linkify}/core/tokens/text.js (90%) rename test/spec/{ => linkify}/plugins/hashtag.js (88%) create mode 100644 test/testem.json create mode 100644 testem.json diff --git a/.jshintrc b/.jshintrc index 5aede48e..a411bb8d 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,6 +3,7 @@ "globalstrict": false, "node": true, "globals": { + "__base": false, "describe": false, "it": false, "before": false, diff --git a/amdwrapper.js b/amdwrapper.js new file mode 100644 index 00000000..356e40c5 --- /dev/null +++ b/amdwrapper.js @@ -0,0 +1,159 @@ +/* jshint browser:true */ +(function () { + +var define, requireModule, require, requirejs; + +(function() { + + var _isArray; + if (!Array.isArray) { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; + } else { + _isArray = Array.isArray; + } + + var registry = {}, seen = {}; + var FAILED = false; + + var uuid = 0; + + function tryFinally(tryable, finalizer) { + try { + return tryable(); + } finally { + finalizer(); + } + } + + + function Module(name, deps, callback, exports) { + var defaultDeps = ['require', 'exports', 'module']; + + this.id = uuid++; + this.name = name; + this.deps = !deps.length && callback.length ? defaultDeps : deps; + this.exports = exports || { }; + this.callback = callback; + this.state = undefined; + } + + define = function(name, deps, callback) { + if (!_isArray(deps)) { + callback = deps; + deps = []; + } + + registry[name] = new Module(name, deps, callback); + }; + + define.amd = {}; + + function reify(mod, name, seen) { + var deps = mod.deps; + var length = deps.length; + var reified = new Array(length); + var dep; + // TODO: new Module + // TODO: seen refactor + var module = { }; + + /* jshint loopfunc:true */ + for (var i = 0, l = length; i < l; i++) { + dep = deps[i]; + if (dep === 'exports') { + module.exports = reified[i] = seen; + } else if (dep === 'require') { + reified[i] = function requireDep(dep) { + return require(resolve(dep, name)); + }; + } else if (dep === 'module') { + mod.exports = seen; + module = reified[i] = mod; + } else { + reified[i] = require(resolve(dep, name)); + } + } + + return { + deps: reified, + module: module + }; + } + + requirejs = require = requireModule = function (name) { + var mod = registry[name]; + if (!mod) { + throw new Error('Could not find module ' + name); + } + + if (mod.state !== FAILED && + seen.hasOwnProperty(name)) { + return seen[name]; + } + + var reified; + var module; + var loaded = false; + + seen[name] = { }; // placeholder for run-time cycles + + tryFinally(function() { + reified = reify(mod, name, seen[name]); + module = mod.callback.apply(this, reified.deps); + loaded = true; + }, function() { + if (!loaded) { + mod.state = FAILED; + } + }); + + var obj; + if (module === undefined && reified.module.exports) { + obj = reified.module.exports; + } else { + obj = seen[name] = module; + } + + if (obj !== null && + (typeof obj === 'object' || typeof obj === 'function') && + obj['default'] === undefined) { + obj['default'] = obj; + } + + return (seen[name] = obj); + }; + + function resolve(child, name) { + if (child.charAt(0) !== '.') { return child; } + + var parts = child.split('/'); + var nameParts = name.split('/'); + var parentBase = nameParts.slice(0, -1); + + for (var i = 0, l = parts.length; i < l; i++) { + var part = parts[i]; + + if (part === '..') { parentBase.pop(); } + else if (part === '.') { continue; } + else { parentBase.push(part); } + } + + return parentBase.join('/'); + } + + requirejs.entries = requirejs._eak_seen = registry; + requirejs.clear = function(){ + requirejs.entries = requirejs._eak_seen = registry = {}; + seen = state = {}; + }; + +})(); + +<%= contents %> + +window.linkify = require('linkifyjs/linkify'); + +})(); +})(); diff --git a/gulpfile.js b/gulpfile.js index 88306d08..dc46c162 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,6 +1,6 @@ var gulp = require('gulp'), stylish = require('jshint-stylish'), -amdOptimize = require("amd-optimize");; +amdOptimize = require('amd-optimize'); var // Gulp plugins concat = require('gulp-concat'), @@ -40,10 +40,10 @@ gulp.task('6to5-amd', function () { })) .pipe(gulp.dest('build/amd')) .pipe(amdOptimize('linkify', { - paths: { - parser: 'build/amd/parser/index', - scanner: 'build/amd/scanner/index' - } + // paths: { + // parser: 'build/amd/parser/index', + // scanner: 'build/amd/scanner/index' + // } })) .pipe(concat('linkify.amd.js')) .pipe(gulp.dest('build')); diff --git a/src/linkify-with-hashtag.js b/src/linkify-with-hashtag.js index edd1ffa6..3b86fc6a 100644 --- a/src/linkify-with-hashtag.js +++ b/src/linkify-with-hashtag.js @@ -1,5 +1,6 @@ // NOTE: This file should only be used to build into a browser package // Linkify with basic hashtags support import linkify from './linkify'; -require('./plugins/hashtag')(linkify); +import hashtag from './linkify/plugins/hashtag'; +hashtag(linkify); export default linkify; diff --git a/src/linkify.js b/src/linkify.js index 362de232..3d0dd5fa 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -1,5 +1,5 @@ -import scanner from './core/scanner'; -import parser from './core/parser'; +import scanner from './linkify/core/scanner'; +import parser from './linkify/core/parser'; /** Converts a string into tokens that represent linkable and non-linkable bits diff --git a/src/core/parser.js b/src/linkify/core/parser.js similarity index 100% rename from src/core/parser.js rename to src/linkify/core/parser.js diff --git a/src/core/scanner.js b/src/linkify/core/scanner.js similarity index 100% rename from src/core/scanner.js rename to src/linkify/core/scanner.js diff --git a/src/core/state/base.js b/src/linkify/core/state/base.js similarity index 100% rename from src/core/state/base.js rename to src/linkify/core/state/base.js diff --git a/src/core/state/character.js b/src/linkify/core/state/character.js similarity index 100% rename from src/core/state/character.js rename to src/linkify/core/state/character.js diff --git a/src/core/state/stateify.js b/src/linkify/core/state/stateify.js similarity index 100% rename from src/core/state/stateify.js rename to src/linkify/core/state/stateify.js diff --git a/src/core/state/token.js b/src/linkify/core/state/token.js similarity index 100% rename from src/core/state/token.js rename to src/linkify/core/state/token.js diff --git a/src/core/tlds.js b/src/linkify/core/tlds.js similarity index 100% rename from src/core/tlds.js rename to src/linkify/core/tlds.js diff --git a/src/core/tokens/multi.js b/src/linkify/core/tokens/multi.js similarity index 100% rename from src/core/tokens/multi.js rename to src/linkify/core/tokens/multi.js diff --git a/src/core/tokens/text.js b/src/linkify/core/tokens/text.js similarity index 100% rename from src/core/tokens/text.js rename to src/linkify/core/tokens/text.js diff --git a/src/plugins/hashtag.js b/src/linkify/plugins/hashtag.js similarity index 100% rename from src/plugins/hashtag.js rename to src/linkify/plugins/hashtag.js diff --git a/test/index.html b/test/index.html new file mode 100644 index 00000000..a99fc05b --- /dev/null +++ b/test/index.html @@ -0,0 +1,14 @@ + + + + Linkify Tests + + + + + + + diff --git a/test/index.js b/test/index.js index 3d6663d6..27e23eef 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,5 @@ var glob = require('glob'); +global.__base = __dirname.replace(/test$/, ''); require('chai').should(); // Initialize should assertions // Require test files diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js index b6fa9343..14e50950 100644 --- a/test/spec/linkify-string.js +++ b/test/spec/linkify-string.js @@ -1,6 +1,6 @@ /*jshint scripturl:true*/ -var linkifyStr = require('../../lib/linkify-string'); +var linkifyStr = require(__base + 'lib/linkify-string'); /** Gracefully truncate a string to a given limit. Will replace extraneous diff --git a/test/spec/core/parser.js b/test/spec/linkify/core/parser.js similarity index 96% rename from test/spec/core/parser.js rename to test/spec/linkify/core/parser.js index efda2801..954db518 100644 --- a/test/spec/core/parser.js +++ b/test/spec/linkify/core/parser.js @@ -1,7 +1,7 @@ var -scanner = require('../../../lib/core/scanner'), -parser = require('../../../lib/core/parser'), -MULTI_TOKENS = require('../../../lib/core/tokens/multi'); +scanner = require(__base + 'lib/linkify/core/scanner'), +parser = require(__base + 'lib/linkify/core/parser'), +MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens/multi'); var TEXT = MULTI_TOKENS.TEXT, diff --git a/test/spec/core/scanner.js b/test/spec/linkify/core/scanner.js similarity index 95% rename from test/spec/core/scanner.js rename to test/spec/linkify/core/scanner.js index f198198d..c242537c 100644 --- a/test/spec/core/scanner.js +++ b/test/spec/linkify/core/scanner.js @@ -1,6 +1,6 @@ var -scanner = require('../../../lib/core/scanner'), -TEXT_TOKENS = require('../../../lib/core/tokens/text'); +scanner = require(__base + 'lib/linkify/core/scanner'), +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'); var DOMAIN = TEXT_TOKENS.DOMAIN, diff --git a/test/spec/core/state/character.js b/test/spec/linkify/core/state/character.js similarity index 92% rename from test/spec/core/state/character.js rename to test/spec/linkify/core/state/character.js index e1f415fd..16dd504b 100644 --- a/test/spec/core/state/character.js +++ b/test/spec/linkify/core/state/character.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../../lib/core/tokens/text'), -CharacterState = require('../../../../lib/core/state/character'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), +CharacterState = require(__base + '/lib/linkify/core/state/character'); describe('CharacterState', function () { var S_START, S_DOT, S_NUM; diff --git a/test/spec/core/state/stateify.js b/test/spec/linkify/core/state/stateify.js similarity index 89% rename from test/spec/core/state/stateify.js rename to test/spec/linkify/core/state/stateify.js index c6384a8a..631462b7 100644 --- a/test/spec/core/state/stateify.js +++ b/test/spec/linkify/core/state/stateify.js @@ -1,7 +1,7 @@ var -TOKENS = require('../../../../lib/core/tokens/text'), -State = require('../../../../lib/core/state/character'), -stateify = require('../../../../lib/core/state/stateify'); +TOKENS = require(__base + 'lib/linkify/core/tokens/text'), +State = require(__base + 'lib/linkify/core/state/character'), +stateify = require(__base + 'lib/linkify/core/state/stateify'); describe('stateify', function () { var S_START; diff --git a/test/spec/core/state/token.js b/test/spec/linkify/core/state/token.js similarity index 82% rename from test/spec/core/state/token.js rename to test/spec/linkify/core/state/token.js index 42ab2d21..6c984529 100644 --- a/test/spec/core/state/token.js +++ b/test/spec/linkify/core/state/token.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../../lib/core/tokens/text'), -TokenState = require('../../../../lib/core/state/token'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), +TokenState = require(__base + 'lib/linkify/core/state/token'); describe('TokenState', function () { var TS_START; diff --git a/test/spec/core/tokens/multi.js b/test/spec/linkify/core/tokens/multi.js similarity index 97% rename from test/spec/core/tokens/multi.js rename to test/spec/linkify/core/tokens/multi.js index e844997e..c1950e34 100644 --- a/test/spec/core/tokens/multi.js +++ b/test/spec/linkify/core/tokens/multi.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require('../../../../lib/core/tokens/text'), -MULTI_TOKENS = require('../../../../lib/core/tokens/multi'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), +MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens/multi'); describe('MULTI_TOKENS', function () { diff --git a/test/spec/core/tokens/text.js b/test/spec/linkify/core/tokens/text.js similarity index 90% rename from test/spec/core/tokens/text.js rename to test/spec/linkify/core/tokens/text.js index 5b50dc94..da531ee4 100644 --- a/test/spec/core/tokens/text.js +++ b/test/spec/linkify/core/tokens/text.js @@ -1,4 +1,4 @@ -var TEXT_TOKENS = require('../../../../lib/core/tokens/text'); +var TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'); describe('TEXT_TOKENS', function () { diff --git a/test/spec/plugins/hashtag.js b/test/spec/linkify/plugins/hashtag.js similarity index 88% rename from test/spec/plugins/hashtag.js rename to test/spec/linkify/plugins/hashtag.js index 1b2cf0e2..3664d593 100644 --- a/test/spec/plugins/hashtag.js +++ b/test/spec/linkify/plugins/hashtag.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -linkify = require('../../../lib/linkify'), -hashtag = require('../../../lib/plugins/hashtag'); +linkify = require(__base + 'lib/linkify'), +hashtag = require(__base + 'lib/linkify/plugins/hashtag'); describe('Linkify Hashtag Plugin', function () { diff --git a/test/testem.json b/test/testem.json new file mode 100644 index 00000000..a7f32ca2 --- /dev/null +++ b/test/testem.json @@ -0,0 +1,6 @@ +{ + "framework": "mocha", + "src_files": [ + "" + ] +} diff --git a/testem.json b/testem.json new file mode 100644 index 00000000..a7f32ca2 --- /dev/null +++ b/testem.json @@ -0,0 +1,6 @@ +{ + "framework": "mocha", + "src_files": [ + "" + ] +} From 838f3273fc9ff5bfb3d7b68274d14bba01424342 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Tue, 13 Jan 2015 22:11:32 -0500 Subject: [PATCH 12/67] Looks like we have a working AMD module system! Run `gulp build` or `gulp dist` --- gulpfile.js | 7 ++++++- package.json | 1 + src/linkify/core/state/base.js | 2 +- src/linkify/core/tokens/multi.js | 5 ++--- src/linkify/core/tokens/text.js | 2 +- test/index.html | 8 +++++--- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index dc46c162..68187bef 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,6 +8,7 @@ jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), // rjs = require('gulp-r'), sourcemaps = require('gulp-sourcemaps'), +rename = require('gulp-rename'), to5 = require('gulp-6to5'), uglify = require('gulp-uglify'); wrap = require('gulp-wrap'); @@ -80,7 +81,11 @@ gulp.task('mocha', function () { }); gulp.task('uglify', function () { - gulp.src('build/parser/index.js') + gulp.src('build/linkify.amd.js') + .pipe(gulp.dest('dist')) + .pipe(rename(function (path) { + path.extname = '.min.js'; + })) .pipe(uglify()) .pipe(gulp.dest('dist')); }); diff --git a/package.json b/package.json index 631977bd..8357d72d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "gulp-es6-transpiler": "^1.0.1", "gulp-jshint": "^1.9.0", "gulp-mocha": "^2.0.0", + "gulp-rename": "^1.2.0", "gulp-sourcemaps": "^1.3.0", "gulp-uglify": "^1.0.2", "gulp-wrap": "^0.8.0", diff --git a/src/linkify/core/state/base.js b/src/linkify/core/state/base.js index 9e34ab81..fad1f7aa 100644 --- a/src/linkify/core/state/base.js +++ b/src/linkify/core/state/base.js @@ -113,4 +113,4 @@ class BaseState { } } -module.exports = BaseState; +export default BaseState; diff --git a/src/linkify/core/tokens/multi.js b/src/linkify/core/tokens/multi.js index 98145e07..46ad81fb 100644 --- a/src/linkify/core/tokens/multi.js +++ b/src/linkify/core/tokens/multi.js @@ -2,8 +2,7 @@ @module linkify @submodule tokens */ -let -TEXT_TOKENS = require('./text'); +import TEXT_TOKENS from './text'; const TT_PROTOCOL = TEXT_TOKENS.PROTOCOL, @@ -220,7 +219,7 @@ class URL extends MultiToken { } } -module.exports = { +export default { Base: MultiToken, EMAIL: EMAIL, NL: NL, diff --git a/src/linkify/core/tokens/text.js b/src/linkify/core/tokens/text.js index c5fa1765..aa6d1b34 100644 --- a/src/linkify/core/tokens/text.js +++ b/src/linkify/core/tokens/text.js @@ -162,7 +162,7 @@ class TLD extends TextToken {} */ class WS extends TextToken {} -module.exports = { +export default { Base: TextToken, DOMAIN: DOMAIN, AT: AT, diff --git a/test/index.html b/test/index.html index a99fc05b..1ea78261 100644 --- a/test/index.html +++ b/test/index.html @@ -6,9 +6,11 @@ - + From 5846ac080830a9088cf58bfadad83ae294255662 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Tue, 13 Jan 2015 23:33:01 -0500 Subject: [PATCH 13/67] More tweaking, preparing additional building blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let’s make a really good module system with flexible builds --- amdwrapper.js | 159 ---------------------------- gulpfile.js | 23 +--- package.json | 1 + src/linkify-with-hashtag.js | 6 -- src/linkify/core/state/character.js | 6 +- src/linkify/core/tlds.js | 1 + test/index.html | 11 ++ 7 files changed, 20 insertions(+), 187 deletions(-) delete mode 100644 amdwrapper.js delete mode 100644 src/linkify-with-hashtag.js diff --git a/amdwrapper.js b/amdwrapper.js deleted file mode 100644 index 356e40c5..00000000 --- a/amdwrapper.js +++ /dev/null @@ -1,159 +0,0 @@ -/* jshint browser:true */ -(function () { - -var define, requireModule, require, requirejs; - -(function() { - - var _isArray; - if (!Array.isArray) { - _isArray = function (x) { - return Object.prototype.toString.call(x) === '[object Array]'; - }; - } else { - _isArray = Array.isArray; - } - - var registry = {}, seen = {}; - var FAILED = false; - - var uuid = 0; - - function tryFinally(tryable, finalizer) { - try { - return tryable(); - } finally { - finalizer(); - } - } - - - function Module(name, deps, callback, exports) { - var defaultDeps = ['require', 'exports', 'module']; - - this.id = uuid++; - this.name = name; - this.deps = !deps.length && callback.length ? defaultDeps : deps; - this.exports = exports || { }; - this.callback = callback; - this.state = undefined; - } - - define = function(name, deps, callback) { - if (!_isArray(deps)) { - callback = deps; - deps = []; - } - - registry[name] = new Module(name, deps, callback); - }; - - define.amd = {}; - - function reify(mod, name, seen) { - var deps = mod.deps; - var length = deps.length; - var reified = new Array(length); - var dep; - // TODO: new Module - // TODO: seen refactor - var module = { }; - - /* jshint loopfunc:true */ - for (var i = 0, l = length; i < l; i++) { - dep = deps[i]; - if (dep === 'exports') { - module.exports = reified[i] = seen; - } else if (dep === 'require') { - reified[i] = function requireDep(dep) { - return require(resolve(dep, name)); - }; - } else if (dep === 'module') { - mod.exports = seen; - module = reified[i] = mod; - } else { - reified[i] = require(resolve(dep, name)); - } - } - - return { - deps: reified, - module: module - }; - } - - requirejs = require = requireModule = function (name) { - var mod = registry[name]; - if (!mod) { - throw new Error('Could not find module ' + name); - } - - if (mod.state !== FAILED && - seen.hasOwnProperty(name)) { - return seen[name]; - } - - var reified; - var module; - var loaded = false; - - seen[name] = { }; // placeholder for run-time cycles - - tryFinally(function() { - reified = reify(mod, name, seen[name]); - module = mod.callback.apply(this, reified.deps); - loaded = true; - }, function() { - if (!loaded) { - mod.state = FAILED; - } - }); - - var obj; - if (module === undefined && reified.module.exports) { - obj = reified.module.exports; - } else { - obj = seen[name] = module; - } - - if (obj !== null && - (typeof obj === 'object' || typeof obj === 'function') && - obj['default'] === undefined) { - obj['default'] = obj; - } - - return (seen[name] = obj); - }; - - function resolve(child, name) { - if (child.charAt(0) !== '.') { return child; } - - var parts = child.split('/'); - var nameParts = name.split('/'); - var parentBase = nameParts.slice(0, -1); - - for (var i = 0, l = parts.length; i < l; i++) { - var part = parts[i]; - - if (part === '..') { parentBase.pop(); } - else if (part === '.') { continue; } - else { parentBase.push(part); } - } - - return parentBase.join('/'); - } - - requirejs.entries = requirejs._eak_seen = registry; - requirejs.clear = function(){ - requirejs.entries = requirejs._eak_seen = registry = {}; - seen = state = {}; - }; - -})(); - -<%= contents %> - -window.linkify = require('linkifyjs/linkify'); - -})(); -})(); diff --git a/gulpfile.js b/gulpfile.js index 68187bef..e5015a2d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -33,34 +33,19 @@ gulp.task('6to5', function () { ES6 to 6to5 AMD modules */ gulp.task('6to5-amd', function () { - gulp.src(paths.src) + + var amd = gulp.src(paths.src) .pipe(to5({ modules: 'amd', moduleIds: true, // moduleRoot: 'linkifyjs' })) .pipe(gulp.dest('build/amd')) - .pipe(amdOptimize('linkify', { - // paths: { - // parser: 'build/amd/parser/index', - // scanner: 'build/amd/scanner/index' - // } - })) + .pipe(amdOptimize('linkify')) .pipe(concat('linkify.amd.js')) .pipe(gulp.dest('build')); -}); -// gulp.task('amd', function () { -// gulp.src(paths.amd) -// }); - -// gulp.task('rjs', function () { -// gulp.src(paths.amd) -// .pipe(rjs({ -// baseUrl: __dirname + '/build/amd/' -// })) -// .pipe(gulp.dest('dist/amd')); -// }) +}); /** Lint using jshint diff --git a/package.json b/package.json index 8357d72d..da45b93b 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "author": "SoapBox Innovations (@SoapBoxHQ)", "license": "MIT", "devDependencies": { + "amd-optimize": "^0.4.3", "chai": "^1.10.0", "glob": "^4.3.2", "gulp": "^3.8.10", diff --git a/src/linkify-with-hashtag.js b/src/linkify-with-hashtag.js deleted file mode 100644 index 3b86fc6a..00000000 --- a/src/linkify-with-hashtag.js +++ /dev/null @@ -1,6 +0,0 @@ -// NOTE: This file should only be used to build into a browser package -// Linkify with basic hashtags support -import linkify from './linkify'; -import hashtag from './linkify/plugins/hashtag'; -hashtag(linkify); -export default linkify; diff --git a/src/linkify/core/state/character.js b/src/linkify/core/state/character.js index 55e5a440..20afcfaa 100644 --- a/src/linkify/core/state/character.js +++ b/src/linkify/core/state/character.js @@ -20,9 +20,9 @@ class CharacterState extends BaseState { @param {String|RegExp} charOrRegExp @return {Boolean} */ - test(char, charOrRegExp) { - return char === charOrRegExp || ( - charOrRegExp instanceof RegExp && charOrRegExp.test(char) + test(character, charOrRegExp) { + return character === charOrRegExp || ( + charOrRegExp instanceof RegExp && charOrRegExp.test(character) ); } diff --git a/src/linkify/core/tlds.js b/src/linkify/core/tlds.js index f4649334..3161e08f 100644 --- a/src/linkify/core/tlds.js +++ b/src/linkify/core/tlds.js @@ -1,5 +1,6 @@ /** NOTICE: Please ensure that these strings are sorted in alphabetical order + TODO: Divide into essential and complete builds */ // http://www.seobythesea.com/2006/01/googles-most-popular-and-least-popular-top-level-domains/ diff --git a/test/index.html b/test/index.html index 1ea78261..d70bb31a 100644 --- a/test/index.html +++ b/test/index.html @@ -7,10 +7,21 @@ + + From 355c8140cd75257a59ac865eb068d69f1654e7fd Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 17 Jan 2015 22:07:41 -0500 Subject: [PATCH 14/67] Finishing off the Linkify 2.0 build system * Basic linkify.js compiled with closure compiler * Build steps for linkify interfaces including string - jQuery/DOM interfaces soon to follow * Build steps for linkify plugins * AMD versions of all of the above Also includes some updates to the core directory and file structure for better compression/minification. --- gulpfile.js | 134 ++++++++++- package.json | 2 + src/linkify-string.js | 8 +- src/linkify.js | 12 +- src/linkify/core/parser.js | 9 +- src/linkify/core/scanner.js | 14 +- src/linkify/core/{state/base.js => state.js} | 104 ++++++++- src/linkify/core/state/character.js | 31 --- src/linkify/core/state/stateify.js | 57 ----- src/linkify/core/state/token.js | 28 --- .../core/{tokens/multi.js => tokens.js} | 215 ++++++++++++++++-- src/linkify/core/tokens/text.js | 182 --------------- src/linkify/plugins/hashtag.js | 4 +- templates/linkify-string.amd.js | 6 + templates/linkify-string.js | 9 + templates/linkify.js | 6 + templates/linkify/plugins/hashtag.amd.js | 4 + templates/linkify/plugins/hashtag.js | 4 + test/spec/linkify/core/parser.js | 8 +- test/spec/linkify/core/scanner.js | 2 +- test/spec/linkify/core/state/character.js | 4 +- test/spec/linkify/core/state/stateify.js | 6 +- test/spec/linkify/core/state/token.js | 4 +- test/spec/linkify/core/tokens/multi.js | 4 +- test/spec/linkify/core/tokens/text.js | 2 +- test/testem.json | 6 - 26 files changed, 484 insertions(+), 381 deletions(-) rename src/linkify/core/{state/base.js => state.js} (52%) delete mode 100644 src/linkify/core/state/character.js delete mode 100644 src/linkify/core/state/stateify.js delete mode 100644 src/linkify/core/state/token.js rename src/linkify/core/{tokens/multi.js => tokens.js} (54%) delete mode 100644 src/linkify/core/tokens/text.js create mode 100644 templates/linkify-string.amd.js create mode 100644 templates/linkify-string.js create mode 100644 templates/linkify.js create mode 100644 templates/linkify/plugins/hashtag.amd.js create mode 100644 templates/linkify/plugins/hashtag.js delete mode 100644 test/testem.json diff --git a/gulpfile.js b/gulpfile.js index e5015a2d..48df150e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,16 +1,17 @@ var gulp = require('gulp'), +path = require('path'), +glob = require('glob'), stylish = require('jshint-stylish'), amdOptimize = require('amd-optimize'); var // Gulp plugins concat = require('gulp-concat'), +closureCompiler = require('gulp-closure-compiler'), jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), -// rjs = require('gulp-r'), -sourcemaps = require('gulp-sourcemaps'), rename = require('gulp-rename'), to5 = require('gulp-6to5'), -uglify = require('gulp-uglify'); +uglify = require('gulp-uglify'), wrap = require('gulp-wrap'); var paths = { @@ -20,12 +21,20 @@ var paths = { spec: 'test/spec/**.js' }; +var to5format = { + comments: true, + indent: { + style: ' ' + } +}; + /** ES6 ~> 6to5 (with CJS Node Modules) + This populates the `lib` folder, allows usage with Node.js */ gulp.task('6to5', function () { return gulp.src(paths.src) - .pipe(to5()) + .pipe(to5({format: to5format})) .pipe(gulp.dest('lib')); }); @@ -34,19 +43,120 @@ gulp.task('6to5', function () { */ gulp.task('6to5-amd', function () { - var amd = gulp.src(paths.src) + gulp.src(paths.src) .pipe(to5({ modules: 'amd', moduleIds: true, + format: to5format // moduleRoot: 'linkifyjs' })) - .pipe(gulp.dest('build/amd')) + .pipe(gulp.dest('build/amd')) // Required for building plugins separately .pipe(amdOptimize('linkify')) .pipe(concat('linkify.amd.js')) .pipe(gulp.dest('build')); + // Plugins + // gulp +}); + +// Build core linkify.js +// Closure compiler is used here since it can correctly concatenate CJS modules +gulp.task('build-core', function () { + + gulp.src(['lib/linkify/core/*.js', 'lib/linkify.js']) + .pipe(closureCompiler({ + compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', + fileName: 'linkify.js', + compilerFlags: { + process_common_js_modules: null, + common_js_entry_module: 'lib/linkify', + common_js_module_path_prefix: path.join(__dirname, 'lib'), + formatting: 'PRETTY_PRINT' + } + })) + .pipe(wrap({src: 'templates/linkify.js'})) + .pipe(gulp.dest('build')); +}); + +// Build root linkify interfaces (files located at the root src folder other +// than linkify.js) +// Depends on build-core +gulp.task('build-interfaces', function () { + + // Core linkify functionality as plugins + var interface, interfaces = [ + 'string', + // 'dom', + // 'jquery' + ]; + + // Globals browser interface + for (var i = 0; i < interfaces.length; i++) { + interface = interfaces[i]; + + // Browser interface + gulp.src('src/linkify-' + interface + '.js') + .pipe(to5({ + modules: 'ignore', + format: to5format + })) + .pipe(wrap({src: 'templates/linkify-' + interface + '.js'})) + .pipe(concat('linkify-' + interface + '.js')) + .pipe(gulp.dest('build')); + + // AMD interface + gulp.src('build/amd/linkify-' + interface + '.js') + .pipe(wrap({src: 'templates/linkify-' + interface + '.amd.js'})) + .pipe(concat('linkify-' + interface + '.amd.js')) + .pipe(gulp.dest('build')); + } + +}); + +/** + NOTE - Run '6to5' and '6to5-amd' first +*/ +gulp.task('build-plugins', function () { + + // Get the filenames of all available plugins + var + plugin, + plugins = glob.sync('*.js', { + cwd: path.join(__dirname, 'src', 'linkify', 'plugins') + }).map(function (plugin) { + return plugin.replace(/\.js$/, ''); + }); + + // Browser plugins + for (var i = 0; i < plugins.length; i++) { + plugin = plugins[i]; + + // Global plugins + gulp.src('src/linkify/plugins/' + plugin + '.js') + .pipe(to5({ + modules: 'ignore', + format: to5format + })) + .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.js'})) + .pipe(concat(plugin + '.js')) + .pipe(gulp.dest('build/linkify/plugins')); + // AMD plugins + gulp.src('build/amd/linkify/plugins/' + plugin + '.js') + .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.amd.js'})) + .pipe(concat(plugin + '.amd.js')) + .pipe(gulp.dest('build/linkify/plugins')); + + } + + // AMD Browser plugins + for (i = 0; i < plugins.length; i++) { + plugin = plugins[i]; + + } }); +// Build steps + /** Lint using jshint */ @@ -66,8 +176,13 @@ gulp.task('mocha', function () { }); gulp.task('uglify', function () { - gulp.src('build/linkify.amd.js') - .pipe(gulp.dest('dist')) + gulp.src([ + 'build/*.js', + 'build/**/*.js', + '!build/amd/*.js', + '!build/amd/**/*.js' + ]) + .pipe(gulp.dest('dist')) // non-minified copy .pipe(rename(function (path) { path.extname = '.min.js'; })) @@ -75,9 +190,6 @@ gulp.task('uglify', function () { .pipe(gulp.dest('dist')); }); -// Build steps -gulp.task('build', ['6to5', '6to5-amd']); - gulp.task('dist', ['6to5', '6to5-amd', 'uglify']); gulp.task('test', ['jshint', 'build', 'mocha']); diff --git a/package.json b/package.json index da45b93b..77e6bc50 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "devDependencies": { "amd-optimize": "^0.4.3", "chai": "^1.10.0", + "closure-compiler": "^0.2.6", "glob": "^4.3.2", "gulp": "^3.8.10", "gulp-6to5": "^2.0.0", + "gulp-closure-compiler": "^0.2.14", "gulp-concat": "^2.4.3", "gulp-es6-transpiler": "^1.0.1", "gulp-jshint": "^1.9.0", diff --git a/src/linkify-string.js b/src/linkify-string.js index 3643cd1e..0d9e9084 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -3,7 +3,7 @@ TODO: Support for computed attributes based on the type? */ -import linkify from './linkify'; +import {tokenize} from './linkify'; function typeToTarget(type) { return type === 'url' ? '_blank' : null; @@ -33,7 +33,7 @@ function attributesToString(attributes) { tagName: 'a', target: '_blank', */ -export default function (str, options) { +function linkifyStr(str, options) { options = options || {}; let @@ -51,7 +51,7 @@ export default function (str, options) { linkClass += ' ' + options.linkClass; } - let tokens = linkify.tokenize(str); + let tokens = tokenize(str); for (let i = 0; i < tokens.length; i++ ) { let token = tokens[i]; @@ -92,3 +92,5 @@ export default function (str, options) { return result.join(''); } + +export default linkifyStr; diff --git a/src/linkify.js b/src/linkify.js index 3d0dd5fa..3f9c656e 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -1,5 +1,5 @@ -import scanner from './linkify/core/scanner'; -import parser from './linkify/core/parser'; +import * as scanner from './linkify/core/scanner'; +import * as parser from './linkify/core/parser'; /** Converts a string into tokens that represent linkable and non-linkable bits @@ -51,10 +51,4 @@ let test = function (str, type=null) { // Scanner and parser provide states and tokens for the lexicographic stage // (will be used to add additional link types) -export default { - find: find, - parser: parser, - scanner: scanner, - test: test, - tokenize: tokenize -}; +export {find, parser, scanner, test, tokenize}; diff --git a/src/linkify/core/parser.js b/src/linkify/core/parser.js index d2c388af..b33ce78b 100644 --- a/src/linkify/core/parser.js +++ b/src/linkify/core/parser.js @@ -13,9 +13,8 @@ @main parser */ -import TEXT_TOKENS from './tokens/text'; -import MULTI_TOKENS from './tokens/multi'; -import State from './state/token'; +import {text as TEXT_TOKENS, multi as MULTI_TOKENS} from './tokens'; +import {TokenState as State} from './state'; let makeState = (tokenClass) => new State(tokenClass); @@ -282,8 +281,8 @@ let run = function (tokens) { }; export default { + State, TOKENS: MULTI_TOKENS, - State: State, - run: run, + run, start: S_START }; diff --git a/src/linkify/core/scanner.js b/src/linkify/core/scanner.js index 6dd663e4..57a8ec95 100644 --- a/src/linkify/core/scanner.js +++ b/src/linkify/core/scanner.js @@ -7,9 +7,8 @@ @main scanner */ -import TOKENS from './tokens/text'; -import State from './state/character'; -import stateify from './state/stateify'; +import {text as TOKENS} from './tokens'; +import {CharacterState as State, stateify} from './state'; import tlds from './tlds'; const @@ -173,10 +172,5 @@ let run = function (str) { return tokens; }; -export default { - State: State, - TOKENS: TOKENS, - run: run, - start: S_START, - stateify: stateify -}; +let start = S_START; +export {State, TOKENS, run, start}; diff --git a/src/linkify/core/state/base.js b/src/linkify/core/state.js similarity index 52% rename from src/linkify/core/state/base.js rename to src/linkify/core/state.js index fad1f7aa..563d3675 100644 --- a/src/linkify/core/state/base.js +++ b/src/linkify/core/state.js @@ -1,7 +1,3 @@ -/** - @module linkify - @submodule state -*/ /** A simple state machine that can emit token classes @@ -113,4 +109,102 @@ class BaseState { } } -export default BaseState; + +/** + State machine for string-based input + + @class CharacterState + @extends BaseState +*/ +class CharacterState extends BaseState { + + /** + Does the given character match the given character or regular + expression? + + @method test + @param {String} char + @param {String|RegExp} charOrRegExp + @return {Boolean} + */ + test(character, charOrRegExp) { + return character === charOrRegExp || ( + charOrRegExp instanceof RegExp && charOrRegExp.test(character) + ); + } +} + + +/** + State machine for input in the form of TextTokens + + @class TokenState + @extends BaseState +*/ +class TokenState extends BaseState { + + /** + Is the given token an instance of the given token class? + + @method test + @param {TextToken} token + @param {Class} tokenClass + @return {Boolean} + */ + test(token, tokenClass) { + return tokenClass.test(token); + } +} + +/** + Given a non-empty target string, generates states (if required) for each + consecutive substring of characters in str starting from the beginning of + the string. The final state will have a special value, as specified in + options. All other "in between" substrings will have a default end state. + + This turns the state machine into a Trie-like data structure (rather than a + intelligently-designed DFA). + + Note that I haven't really tried these with any strings other than + DOMAIN. + + @param {String} str + @param {CharacterState} start State to jump from the first character + @param {Class} endToken Token class to emit when the given string has been + matched and no more jumps exist. + @param {Class} defaultToken "Filler token", or which token type to emit when + we don't have a full match + @return {Array} list of newly-created states +*/ +function stateify(str, start, endToken, defaultToken) { + + let i = 0, + len = str.length, + state = start, + newStates = [], + nextState; + + // Find the next state without a jump to the next character + while (i < len && (nextState = state.next(str[i]))) { + state = nextState; + i++; + } + + if (i >= len) return []; // no new tokens were added + + while (i < len - 1) { + nextState = new CharacterState(defaultToken); + newStates.push(nextState); + state.on(str[i], nextState); + state = nextState; + i++; + } + + nextState = new CharacterState(endToken); + newStates.push(nextState); + state.on(str[len - 1], nextState); + + return newStates; +} + +export {CharacterState, TokenState, stateify}; diff --git a/src/linkify/core/state/character.js b/src/linkify/core/state/character.js deleted file mode 100644 index 20afcfaa..00000000 --- a/src/linkify/core/state/character.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - @module linkify - @submodule scanner -*/ -import BaseState from './base'; - -/** - Subclass of - @class CharacterState - @extends BaseState -*/ -class CharacterState extends BaseState { - - /** - Does the given character match the given character or regular - expression? - - @method test - @param {String} char - @param {String|RegExp} charOrRegExp - @return {Boolean} - */ - test(character, charOrRegExp) { - return character === charOrRegExp || ( - charOrRegExp instanceof RegExp && charOrRegExp.test(character) - ); - } - -} - -export default CharacterState; diff --git a/src/linkify/core/state/stateify.js b/src/linkify/core/state/stateify.js deleted file mode 100644 index 8b9d5bdb..00000000 --- a/src/linkify/core/state/stateify.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - @module linkify - @submodule tokenizer -*/ - -import CharacterState from './character'; - -/** - Given a non-empty target string, generates states (if required) for each - consecutive substring of characters in str starting from the beginning of - the string. The final state will have a special value, as specified in - options. All other "in between" substrings will have a default end state. - - This turns the state machine into a Trie-like data structure (rather than a - intelligently-designed DFA). - - Note that I haven't really tried these with any strings other than - DOMAIN. - - @param {String} str - @param {CharacterState} start State to jump from the first character - @param {Class} endToken Token class to emit when the given string has been - matched and no more jumps exist. - @param {Class} defaultToken "Filler token", or which token type to emit when - we don't have a full match - @return {Array} list of newly-created states -*/ -export default function (str, start, endToken, defaultToken) { - - let i = 0, - len = str.length, - state = start, - newStates = [], - nextState; - - // Find the next state without a jump to the next character - while (i < len && (nextState = state.next(str[i]))) { - state = nextState; - i++; - } - - if (i >= len) return []; // no new tokens were added - - while (i < len - 1) { - nextState = new CharacterState(defaultToken); - newStates.push(nextState); - state.on(str[i], nextState); - state = nextState; - i++; - } - - nextState = new CharacterState(endToken); - newStates.push(nextState); - state.on(str[len - 1], nextState); - - return newStates; -} diff --git a/src/linkify/core/state/token.js b/src/linkify/core/state/token.js deleted file mode 100644 index 85bdf277..00000000 --- a/src/linkify/core/state/token.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - @module linkify - @submodule parser -*/ -import BaseState from './base'; - -/** - Subclass of - @class CharacterState - @extends BaseState -*/ -class TokenState extends BaseState { - - /** - Is the given token an instance of the given token class? - - @method test - @param {TextToken} token - @param {Class} tokenClass - @return {Boolean} - */ - test(token, tokenClass) { - return tokenClass.test(token); - } - -} - -export default TokenState; diff --git a/src/linkify/core/tokens/multi.js b/src/linkify/core/tokens.js similarity index 54% rename from src/linkify/core/tokens/multi.js rename to src/linkify/core/tokens.js index 46ad81fb..07106c99 100644 --- a/src/linkify/core/tokens/multi.js +++ b/src/linkify/core/tokens.js @@ -1,20 +1,197 @@ +/****************************************************************************** + Text Tokens + Tokens composed of strings +******************************************************************************/ + /** - @module linkify - @submodule tokens + Abstract class used for manufacturing text tokens. + Pass in the value this token represents + + @class TextToken + @abstract */ -import TEXT_TOKENS from './text'; +class TextToken { + /** + @method constructor + @param {String} value The string of characters representing this particular Token + */ + constructor(value) { + this.v = value; + } + + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + + toString() { + return this.v + ''; + } + + /** + Is the given value an instance of this Token? + @method test + @static + @param {Mixed} value + */ + static test(value) { + return value instanceof this; + } +} + +/** + A valid domain token + @class DOMAIN + @extends TextToken +*/ +class DOMAIN extends TextToken {} + +/** + @class AT + @extends TextToken +*/ +class AT extends TextToken { + constructor() { super('@'); } +} + +/** + Represents a single colon `:` character + + @class COLON + @extends TextToken +*/ +class COLON extends TextToken { + constructor() { super(':'); } +} + +/** + @class DOT + @extends TextToken +*/ +class DOT extends TextToken { + constructor() { super('.'); } +} + +/** + The word localhost (by itself) + @class LOCALHOST + @extends TextToken +*/ +class LOCALHOST extends TextToken {} + +/** + Newline token + @class NL + @extends TextToken +*/ +class NL extends TextToken { + constructor() { super('\n'); } +} + +/** + @class NUM + @extends TextToken +*/ +class NUM extends TextToken {} + +/** + @class PLUS + @extends TextToken +*/ +class PLUS extends TextToken { + constructor() { super('+'); } +} + +/** + @class POUND + @extends TextToken +*/ +class POUND extends TextToken { + constructor() { super('#'); } +} + +/** + Represents a web URL protocol. Supported types include + + * `http:` + * `https:` + * `ftp:` + * `ftps:` + * There's Another super weird one + + @class PROTOCOL + @extends TextToken +*/ +class PROTOCOL extends TextToken {} + +/** + @class QUERY + @extends TextToken +*/ +class QUERY extends TextToken { + constructor() { super('?'); } +} + +/** + @class SLASH + @extends TextToken +*/ +class SLASH extends TextToken { + constructor() { super('/'); } +} + +/** + One ore more non-whitespace symbol. + @class SYM + @extends TextToken +*/ +class SYM extends TextToken {} + +/** + @class TLD + @extends TextToken +*/ +class TLD extends TextToken {} + +/** + Represents a string of consecutive whitespace characters + + @class WS + @extends TextToken +*/ +class WS extends TextToken {} + +let text = { + Base: TextToken, + DOMAIN, + AT, + COLON, + DOT, + LOCALHOST, + NL, + NUM, + PLUS, + POUND, + QUERY, + PROTOCOL, + SLASH, + SYM, + TLD, + WS +}; + +/****************************************************************************** + Multi-Tokens + Tokens composed of arrays of TextTokens +******************************************************************************/ -const -TT_PROTOCOL = TEXT_TOKENS.PROTOCOL, -TT_DOMAIN = TEXT_TOKENS.DOMAIN, -TT_TLD = TEXT_TOKENS.TLD, -TT_SLASH = TEXT_TOKENS.SLASH; // Is the given token a valid domain token? // Should nums be included here? function isDomainToken(token) { - return TT_DOMAIN.test(token) || - TT_TLD.test(token); + return DOMAIN.test(token) || + TLD.test(token); } /** @@ -181,14 +358,14 @@ class URL extends MultiToken { // Make the first part of the domain lowercase // Lowercase protocol - while (TT_PROTOCOL.test(tokens[i])) { + while (PROTOCOL.test(tokens[i])) { hasProtocol = true; result.push(tokens[i].toString().toLowerCase()); i++; } // Skip slash-slash - while (TT_SLASH.test(tokens[i])) { + while (SLASH.test(tokens[i])) { hasSlashSlash = true; result.push(tokens[i].toString()); i++; @@ -215,14 +392,16 @@ class URL extends MultiToken { } hasProtocol() { - return this.v[0] instanceof TT_PROTOCOL; + return this.v[0] instanceof PROTOCOL; } } -export default { +let multi = { Base: MultiToken, - EMAIL: EMAIL, - NL: NL, - TEXT: TEXT, - URL: URL + EMAIL, + NL, + TEXT, + URL }; + +export {text, multi}; diff --git a/src/linkify/core/tokens/text.js b/src/linkify/core/tokens/text.js deleted file mode 100644 index aa6d1b34..00000000 --- a/src/linkify/core/tokens/text.js +++ /dev/null @@ -1,182 +0,0 @@ -/** - @module linkify - @submodule tokens - @main tokens -*/ -/** - Abstract class used for manufacturing text tokens. - Pass in the value this token represents - - @class TextToken - @abstract -*/ -class TextToken { - /** - @method constructor - @param {String} value The string of characters representing this particular Token - */ - constructor(value) { - this.v = value; - } - - /** - String representing the type for this token - @property type - @default 'TOKEN' - */ - - toString() { - return this.v + ''; - } - - /** - Is the given value an instance of this Token? - @method test - @static - @param {Mixed} value - */ - static test(value) { - return value instanceof this; - } -} - -/** - A valid domain token - @class DOMAIN - @extends TextToken -*/ -class DOMAIN extends TextToken {} - -/** - @class AT - @extends TextToken -*/ -class AT extends TextToken { - constructor() { super('@'); } -} - -/** - Represents a single colon `:` character - - @class COLON - @extends TextToken -*/ -class COLON extends TextToken { - constructor() { super(':'); } -} - -/** - @class DOT - @extends TextToken -*/ -class DOT extends TextToken { - constructor() { super('.'); } -} - -/** - The word localhost (by itself) - @class LOCALHOST - @extends TextToken -*/ -class LOCALHOST extends TextToken {} - -/** - Newline token - @class NL - @extends TextToken -*/ -class NL extends TextToken { - constructor() { super('\n'); } -} - -/** - @class NUM - @extends TextToken -*/ -class NUM extends TextToken {} - -/** - @class PLUS - @extends TextToken -*/ -class PLUS extends TextToken { - constructor() { super('+'); } -} - -/** - @class POUND - @extends TextToken -*/ -class POUND extends TextToken { - constructor() { super('#'); } -} - -/** - Represents a web URL protocol. Supported types include - - * `http:` - * `https:` - * `ftp:` - * `ftps:` - * There's Another super weird one - - @class PROTOCOL - @extends TextToken -*/ -class PROTOCOL extends TextToken {} - -/** - @class QUERY - @extends TextToken -*/ -class QUERY extends TextToken { - constructor() { super('?'); } -} - -/** - @class SLASH - @extends TextToken -*/ -class SLASH extends TextToken { - constructor() { super('/'); } -} - -/** - One ore more non-whitespace symbol. - @class SYM - @extends TextToken -*/ -class SYM extends TextToken {} - -/** - @class TLD - @extends TextToken -*/ -class TLD extends TextToken {} - -/** - Represents a string of consecutive whitespace characters - - @class WS - @extends TextToken -*/ -class WS extends TextToken {} - -export default { - Base: TextToken, - DOMAIN: DOMAIN, - AT: AT, - COLON: COLON, - DOT: DOT, - LOCALHOST: LOCALHOST, - NL: NL, - NUM: NUM, - PLUS: PLUS, - POUND: POUND, - QUERY: QUERY, - PROTOCOL: PROTOCOL, - SLASH: SLASH, - SYM: SYM, - TLD: TLD, - WS: WS -}; diff --git a/src/linkify/plugins/hashtag.js b/src/linkify/plugins/hashtag.js index 369634ea..2ac94045 100644 --- a/src/linkify/plugins/hashtag.js +++ b/src/linkify/plugins/hashtag.js @@ -1,7 +1,7 @@ /** Quick Hashtag parser plugin for linkify */ -export default function (linkify) { +function hashtag (linkify) { let TT = linkify.scanner.TOKENS, // Text tokens MT = linkify.parser.TOKENS, // Multi tokens @@ -24,3 +24,5 @@ export default function (linkify) { S_HASH.on(TT.DOMAIN, S_HASHTAG); S_HASH.on(TT.TLD, S_HASHTAG); } + +export default hashtag; diff --git a/templates/linkify-string.amd.js b/templates/linkify-string.amd.js new file mode 100644 index 00000000..c9b9e963 --- /dev/null +++ b/templates/linkify-string.amd.js @@ -0,0 +1,6 @@ +<%= contents %> +require(['linkify-string'], function (linkifyStr) { + String.prototype.linkify = function (options) { + return linkifyStr(this, options); + } +}); diff --git a/templates/linkify-string.js b/templates/linkify-string.js new file mode 100644 index 00000000..1e4f3c01 --- /dev/null +++ b/templates/linkify-string.js @@ -0,0 +1,9 @@ +;(function (linkify) { +"use strict"; +var tokenize = linkify.tokenize; +<%= contents %> +window.linkifyStr = linkifyStr; +String.prototype.linkify = function (options) { + return linkifyStr(this, options); +}; +})(window.linkify); diff --git a/templates/linkify.js b/templates/linkify.js new file mode 100644 index 00000000..39a9370b --- /dev/null +++ b/templates/linkify.js @@ -0,0 +1,6 @@ +;(function () { +"use strict"; +// Output from the Closure Compiler +<%= contents %> +window.linkify = module$$linkify; +})(); diff --git a/templates/linkify/plugins/hashtag.amd.js b/templates/linkify/plugins/hashtag.amd.js new file mode 100644 index 00000000..65de6ec8 --- /dev/null +++ b/templates/linkify/plugins/hashtag.amd.js @@ -0,0 +1,4 @@ +<%= contents %> +require(['linkify', 'linkify/plugins/hashtag'], function (linkify, hashtag) { + hashtag(linkify); +}); diff --git a/templates/linkify/plugins/hashtag.js b/templates/linkify/plugins/hashtag.js new file mode 100644 index 00000000..2997b955 --- /dev/null +++ b/templates/linkify/plugins/hashtag.js @@ -0,0 +1,4 @@ +;(function (linkify) { +<%= contents %> +hashtag(linkify); +})(window.linkify); diff --git a/test/spec/linkify/core/parser.js b/test/spec/linkify/core/parser.js index 954db518..fd9303d7 100644 --- a/test/spec/linkify/core/parser.js +++ b/test/spec/linkify/core/parser.js @@ -1,12 +1,12 @@ var scanner = require(__base + 'lib/linkify/core/scanner'), parser = require(__base + 'lib/linkify/core/parser'), -MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens/multi'); +MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens').multi; var -TEXT = MULTI_TOKENS.TEXT, -URL = MULTI_TOKENS.URL, -EMAIL = MULTI_TOKENS.EMAIL; +TEXT = MULTI_TOKENS.TEXT, +URL = MULTI_TOKENS.URL, +EMAIL = MULTI_TOKENS.EMAIL; // MNL = MULTI_TOKENS.NL; // new line /** diff --git a/test/spec/linkify/core/scanner.js b/test/spec/linkify/core/scanner.js index c242537c..7511da89 100644 --- a/test/spec/linkify/core/scanner.js +++ b/test/spec/linkify/core/scanner.js @@ -1,6 +1,6 @@ var scanner = require(__base + 'lib/linkify/core/scanner'), -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text; var DOMAIN = TEXT_TOKENS.DOMAIN, diff --git a/test/spec/linkify/core/state/character.js b/test/spec/linkify/core/state/character.js index 16dd504b..dd2cdb38 100644 --- a/test/spec/linkify/core/state/character.js +++ b/test/spec/linkify/core/state/character.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), -CharacterState = require(__base + '/lib/linkify/core/state/character'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, +CharacterState = require(__base + '/lib/linkify/core/state').CharacterState; describe('CharacterState', function () { var S_START, S_DOT, S_NUM; diff --git a/test/spec/linkify/core/state/stateify.js b/test/spec/linkify/core/state/stateify.js index 631462b7..c1a28490 100644 --- a/test/spec/linkify/core/state/stateify.js +++ b/test/spec/linkify/core/state/stateify.js @@ -1,7 +1,7 @@ var -TOKENS = require(__base + 'lib/linkify/core/tokens/text'), -State = require(__base + 'lib/linkify/core/state/character'), -stateify = require(__base + 'lib/linkify/core/state/stateify'); +TOKENS = require(__base + 'lib/linkify/core/tokens').text, +State = require(__base + 'lib/linkify/core/state').CharacterState, +stateify = require(__base + 'lib/linkify/core/state').stateify; describe('stateify', function () { var S_START; diff --git a/test/spec/linkify/core/state/token.js b/test/spec/linkify/core/state/token.js index 6c984529..d4a985e0 100644 --- a/test/spec/linkify/core/state/token.js +++ b/test/spec/linkify/core/state/token.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), -TokenState = require(__base + 'lib/linkify/core/state/token'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, +TokenState = require(__base + 'lib/linkify/core/state').TokenState; describe('TokenState', function () { var TS_START; diff --git a/test/spec/linkify/core/tokens/multi.js b/test/spec/linkify/core/tokens/multi.js index c1950e34..35fb6b4e 100644 --- a/test/spec/linkify/core/tokens/multi.js +++ b/test/spec/linkify/core/tokens/multi.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'), -MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens/multi'); +TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, +MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens').multi; describe('MULTI_TOKENS', function () { diff --git a/test/spec/linkify/core/tokens/text.js b/test/spec/linkify/core/tokens/text.js index da531ee4..8bd5440d 100644 --- a/test/spec/linkify/core/tokens/text.js +++ b/test/spec/linkify/core/tokens/text.js @@ -1,4 +1,4 @@ -var TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens/text'); +var TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text; describe('TEXT_TOKENS', function () { diff --git a/test/testem.json b/test/testem.json deleted file mode 100644 index a7f32ca2..00000000 --- a/test/testem.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "framework": "mocha", - "src_files": [ - "" - ] -} From ea32f8716031ce3c0a4399d2bddb25ab3a1ef489 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 17 Jan 2015 22:20:29 -0500 Subject: [PATCH 15/67] Configuration for Node.js --- .npmignore | 11 ++++++++++- gulpfile.js | 11 ++++++++++- plugins/hashtag.js | 1 + string.js | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 plugins/hashtag.js create mode 100644 string.js diff --git a/.npmignore b/.npmignore index 9e54bc92..ae689a8d 100644 --- a/.npmignore +++ b/.npmignore @@ -1,8 +1,17 @@ -# All compiled code will be in the "lib" and build folders +# All compiled code will be in the `lib` folders amd assets bower_components build demo src +templates test + +# Files +.editorconfig +.jshintrc +.travis.yml +bower.json +gulpfile.js +testem.json diff --git a/gulpfile.js b/gulpfile.js index 48df150e..e3eeb667 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -190,7 +190,16 @@ gulp.task('uglify', function () { .pipe(gulp.dest('dist')); }); -gulp.task('dist', ['6to5', '6to5-amd', 'uglify']); +gulp.task('build', [ + '6to5', + '6to5-amd', + 'build-core', + 'build-interfaces', + 'build-plugins' +]); + +gulp.task('dist', ['build', 'uglify']); + gulp.task('test', ['jshint', 'build', 'mocha']); /** diff --git a/plugins/hashtag.js b/plugins/hashtag.js new file mode 100644 index 00000000..e6ec8ec3 --- /dev/null +++ b/plugins/hashtag.js @@ -0,0 +1 @@ +module.exports = require('../lib/linkify/plugins/hashtag'); diff --git a/string.js b/string.js new file mode 100644 index 00000000..71dd9473 --- /dev/null +++ b/string.js @@ -0,0 +1 @@ +module.exports = require('./lib/linkify-string'); From c537170de316048a69c77e7366abe9101b216149 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 17 Jan 2015 23:12:05 -0500 Subject: [PATCH 16/67] Updated plugin build locations For a more filename-friendly browser shim. Also removed bower.json (moved to shim repository) --- bower.json | 31 ------------------------------- gulpfile.js | 15 +++++---------- 2 files changed, 5 insertions(+), 41 deletions(-) delete mode 100644 bower.json diff --git a/bower.json b/bower.json deleted file mode 100644 index 8a95f011..00000000 --- a/bower.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "linkify", - "main": "index.js", - "version": "2.0.0", - "authors": [ - "SoapBox Innovations Inc. " - ], - "description": "Find links in plain text", - "keywords": [ - "node", - "js", - "jquery", - "link", - "autolink", - "text", - "url", - "email" - ], - "license": "MIT", - "homepage": "http://soapbox.github.io/jQuery-linkify/", - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "jquery": "~2.1.3" - } -} diff --git a/gulpfile.js b/gulpfile.js index e3eeb667..a23a8f14 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -137,14 +137,14 @@ gulp.task('build-plugins', function () { format: to5format })) .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.js'})) - .pipe(concat(plugin + '.js')) - .pipe(gulp.dest('build/linkify/plugins')); + .pipe(concat('linkify-plugin-' + plugin + '.js')) + .pipe(gulp.dest('build')); // AMD plugins gulp.src('build/amd/linkify/plugins/' + plugin + '.js') .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.amd.js'})) - .pipe(concat(plugin + '.amd.js')) - .pipe(gulp.dest('build/linkify/plugins')); + .pipe(concat('linkify-plugin-' + plugin + '.amd.js')) + .pipe(gulp.dest('build')); } @@ -176,12 +176,7 @@ gulp.task('mocha', function () { }); gulp.task('uglify', function () { - gulp.src([ - 'build/*.js', - 'build/**/*.js', - '!build/amd/*.js', - '!build/amd/**/*.js' - ]) + gulp.src('build/*.js') .pipe(gulp.dest('dist')) // non-minified copy .pipe(rename(function (path) { path.extname = '.min.js'; From f87023b0a7615f73433faec3c0668e07b404a7a6 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 18 Jan 2015 01:11:30 -0500 Subject: [PATCH 17/67] Better linkify-string options Includes a few new options and ability to set some options to functions. Also better HTML cleaning --- src/linkify-string.js | 81 +++++++++++++++++----------------- src/linkify/plugins/hashtag.js | 2 +- test/spec/linkify-string.js | 14 +++++- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/linkify-string.js b/src/linkify-string.js index 0d9e9084..10711962 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -9,6 +9,16 @@ function typeToTarget(type) { return type === 'url' ? '_blank' : null; } +function cleanText(text) { + return text + .replace(//g, '>'); +} + +function cleanAttr(href) { + return href.replace(/"/g, '"'); +} + function attributesToString(attributes) { if (!attributes) return ''; @@ -16,41 +26,33 @@ function attributesToString(attributes) { for (let attr in attributes) { let val = (attributes[attr] + '').replace(/"/g, '"'); - result.push(`${attr}="${val}"`); + result.push(`${attr}="${cleanAttr(val)}"`); } return result.join(' '); } -/** - Options: - - defaultProtocol: 'http' - format: null - linkAttributes: null, - linkClass: null, - newLine: '\n', // deprecated - nl2br: false, - tagName: 'a', - target: '_blank', -*/ -function linkifyStr(str, options) { - options = options || {}; +function resolveOption(value, ...params) { + return typeof value === 'function' ? value(...params) : value; +} + +function noop(val) { + return val; +} + +function linkifyStr(str, opts={}) { let - defaultProtocol = options.defaultProtocol || 'http', - tagName = options.tagName || 'a', - target = options.target || typeToTarget, - newLine = options.newLine || false, // deprecated - nl2br = !!newLine || options.nl2br || false, - format = options.format || null, - attributes = options.linkAttributes || null, - linkClass = 'linkified', - result = []; - - if (options.linkClass) { - linkClass += ' ' + options.linkClass; - } + attributes = opts.linkAttributes || null, + defaultProtocol = opts.defaultProtocol || 'http', + format = opts.format || noop, + formatHref = opts.formatHref || noop, + newLine = opts.newLine || false, // deprecated + nl2br = !!newLine || opts.nl2br || false, + tagName = opts.tagName || 'a', + target = opts.target || typeToTarget, + linkClass = opts.linkClass || 'linkified'; + let result = []; let tokens = tokenize(str); for (let i = 0; i < tokens.length; i++ ) { @@ -58,12 +60,15 @@ function linkifyStr(str, options) { if (token.isLink) { let - link = `<${tagName} href="${token.toHref(defaultProtocol)}" class="${linkClass}"`, - targetStr = typeof target === 'function' ? - target(token.type) : target, - attributesHash = typeof attributes === 'function' ? - attributes(token.type) : attributes; - + tagNameStr = resolveOption(tagName, token.type), + classStr = resolveOption(linkClass, token.type), + targetStr = resolveOption(target, token.type), + formatted = resolveOption(format, token.toString(), token.type), + href = token.toHref(defaultProtocol), + formattedHref = resolveOption(formatHref, href, token.type), + attributesHash = resolveOption(attributes, token.type); + + let link = `<${tagNameStr} href="${cleanAttr(formattedHref)}" class="${classStr}"`; if (targetStr) { link += ` target="${targetStr}"`; } @@ -72,11 +77,7 @@ function linkifyStr(str, options) { link += ` ${attributesToString(attributesHash)}`; } - link += '>'; - link += typeof format === 'function' ? - format(token.toString(), token.type) : token.toString(); - link += ``; - + link += `>${cleanText(formatted)}`; result.push(link); } else if (token.type === 'nl' && nl2br) { @@ -86,7 +87,7 @@ function linkifyStr(str, options) { result.push('
\n'); } } else { - result.push(token.toString()); + result.push(cleanText(token.toString())); } } diff --git a/src/linkify/plugins/hashtag.js b/src/linkify/plugins/hashtag.js index 2ac94045..c21386d6 100644 --- a/src/linkify/plugins/hashtag.js +++ b/src/linkify/plugins/hashtag.js @@ -1,7 +1,7 @@ /** Quick Hashtag parser plugin for linkify */ -function hashtag (linkify) { +function hashtag(linkify) { let TT = linkify.scanner.TOKENS, // Text tokens MT = linkify.parser.TOKENS, // Multi tokens diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string.js index 14e50950..c0ceef3e 100644 --- a/test/spec/linkify-string.js +++ b/test/spec/linkify-string.js @@ -35,7 +35,13 @@ describe('linkify-string', function () { onclick: 'javascript:;' }, format: function (val) { - return val.truncate(20); + return val.truncate(40); + }, + formatHref: function (href, type) { + if (type === 'email') { + href += '?subject=Hello%20from%20Linkify'; + } + return href; } }, @@ -51,7 +57,11 @@ describe('linkify-string', function () { ], [ 'The URL is google.com and the email is test@example.com', 'The URL is google.com and the email is test@example.com', - 'The URL is google.com and the email is test@example.com' + 'The URL is google.com and the email is test@example.com' + ], [ + 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test."wut".yo@gmail.co.uk!\n', + 'Super long maps URL https://www.google.ca/maps/@43.472082,-80.5426668,18z?hl=en, a #hash-tag, and an email: test."wut".yo@gmail.co.uk!\n', + 'Super long maps URL https://www.google.ca/maps/@43.472082,-8…, a #hash-tag, and an email: test."wut".yo@gmail.co.uk!
\n', ] ]; From cd63fcce571579cdfc4b949976831665387874fe Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 18 Jan 2015 03:24:31 -0500 Subject: [PATCH 18/67] linkify-string prototype, initial round of docs updates --- README.md | 286 +++++++++++++++++++++++++++----- src/linkify-string.js | 6 + templates/linkify-string.amd.js | 5 - templates/linkify-string.js | 3 - 4 files changed, 251 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2123173d..488506b0 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,14 @@ [![Node Dependencies](https://david-dm.org/SoapBox/jQuery-linkify/dev-status.png)](https://david-dm.org/SoapBox/jQuery-linkify#info=devDependencies&view=table) -__Download 1.1__ -- [Minified](https://github.com/SoapBox/jQuery-linkify/blob/master/dist/jquery.linkify.min.js) -- [Source](https://github.com/SoapBox/jQuery-linkify/blob/master/dist/jquery.linkify.js) - __Jump to__ - [Demo](#demo) -- [Installing](#installing) - - [Basic](#basic) - - [Bower](#bower) +- [Installation and Usage](#installation-and-usage) + - [Quick Start](#quick-start) + - [Usage](#usage) + - [Node.js/Browserify](#node-js-browserify) + - [AMD Modules](#amd-modules) + - [Browser](#browser) - [Examples](#examples) - [Basic Usage](#basic-usage) - [Usage via HTML attributes](#usage-via-html-attributes) @@ -25,60 +24,265 @@ Linkify is a jQuery plugin for finding URLs in plain-text and converting them to ## Demo [Launch demo](http://soapbox.github.io/jQuery-linkify/) -## Installing +## Installation and Usage + +### Quick Start -### Basic -Just download [jquery.linkify.min.js](https://github.com/HitSend/jQuery-linkify/blob/master/dist/jquery.linkify.min.js) from this repo's `dist` folder and include it on your web page with ` - + + + + ``` -### Bower -Run `bower install jQuery-linkify` from your project's root folder. +### Usage +#### Node.js/Browserify -## Examples +```js +var linkify = require('linkifyjs'); +var linkifyInterface = require('linkifyjs/'); +require('linkifyjs/plugin/')(linkify); -### Basic Usage +linkify.find('github.com'); +linkifyInterface(target, options); +``` -To detect links within any set of elements, just call `$(selector).linkify()` on document load. +#### AMD modules -#### Code +```html + + + + + +``` + +#### Browser ```html -

Check out this link to http://google.com

-

You can also email support@example.com to view more.

+ + + +``` + +## Downloads + +* linkify _(required)_ · [View docs](#linkify) + * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify.js) + * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify.amd.js) + * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify.min.js) + * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify.amd.min.js) - - +**Interfaces** _(recommended - include at least one)_ · [View docs](#linkify) + +* [string](#string) · [View docs](#linkify) + * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.js) + * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.amd.js) + * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.min.js) + * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.amd.min.js) + +**Plugins** _(optional)_ · [View docs](#linkify) + +* [hashtag](#plugin-hashtag) · [View docs](#linkify) + * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.js) + * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.amd.js) + * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.min.js) + * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.amd.min.js) + +## API + +```js +// Node.js/Browserify usage +var linkify = require('linkifyjs'); ``` -```javascript -$(window).on('load', function () { - $('p').linkify(); -}); +```html + + + + + + ``` -#### Output +#### linkify.find _(str)_ -``` html -

- Check out this link to - - http://google.com - -

-

- You can also email - - support@example.com - - to view more. -

+Finds all links in the given string + +**Params** + +* `String` **`str`** Search string + +**Returns** _`Array`_ List of links where each element is a hash with properties `type`, `value`, and `href` + +```js +linkify.find('For help with GitHub.com, please email support@github.com'); +/** // returns +[{ + type: 'url', + value: 'GitHub.com', + href: 'http://github.com', +}, { + type: 'email', + value: 'support@github.com', + href: 'mailto:support@github.com' +}] +*/ +``` + +#### linkify.test _(str)_ + +Is the given string a link? Not to be used for strict validation - See [Caveats](#) + +**Params** + +* `String` **`str`** Test string + +**Returns** _`Boolean`_ + +```js +linkify.test('google.dev'); // false +linkify.test('google.com'); // true +``` + +#### linkify.tokenize _(str)_ + +Internal method used to perform lexicographical analysis on the given string and output the resulting array tokens. + +**Params** + +* `String` **`str`** + +**Returns** _`Array`_ + +### Interfaces + +#### string + +Interface for replacing links within native strings with anchor tags. Note that this function will **not** parse HTML strings - use [linkify-dom](#) or [linkify-jquery](#) instead. + +```js +// Node.js/Browserify usage +var linkifyStr = require('linkifyjs/string'),; +``` + +```html + + + + + + + + +``` + +**Usage** + +```js +var options = {/* ... */}; +linkifyStr('For help with GitHub.com, please email support@github.com'); +// returns "For help with GitHub.com, please email support@github.com +``` + +or + +```js +var options = {/* ... */}; +'For help with GitHub.com, please email support@github.com'.linkify(options); +``` + +**Params** + +* `String` **`str`** String to linkify +* `Object` [**`options`**] [Options hash](#) + +**Returns** _`String`_ Linkified string + +### Plugins + +Plugins provide no new interfaces but add additional detection functionality to Linkify. A plugic plugin API is currently in the works. + +#### hashtag + +Adds basic support for Twitter-style hashtags + +```js +// Node.js/Browserify +var linkify = require('linkifyjs'); +require('linkifyjs/plugins/hashtag')(linkify); ``` +```html + + + + + + + + +``` + +**Usage** + +```js +var options = {/* ... */}; +var str = "Linkify is #super #rad"; + +linkify.find(str); +// [ +// {type: 'hashtag', value: "#super", href: "#super"}, +// {type: 'hashtag', value: "#rad", href: "#rad"} +// ] + +// If the linkifyStr interface has also been included +linkifyStr(str) + +``` + + ### Usage via HTML attributes Linkify also provides a DOM data- API. The following code will find links in the `#linkify-example` paragraph element: diff --git a/src/linkify-string.js b/src/linkify-string.js index 10711962..6f764aca 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -94,4 +94,10 @@ function linkifyStr(str, opts={}) { return result.join(''); } +if (!String.prototype.linkify) { + String.prototype.linkify = function (options) { + return linkifyStr(this, options); + }; +} + export default linkifyStr; diff --git a/templates/linkify-string.amd.js b/templates/linkify-string.amd.js index c9b9e963..339e4544 100644 --- a/templates/linkify-string.amd.js +++ b/templates/linkify-string.amd.js @@ -1,6 +1 @@ <%= contents %> -require(['linkify-string'], function (linkifyStr) { - String.prototype.linkify = function (options) { - return linkifyStr(this, options); - } -}); diff --git a/templates/linkify-string.js b/templates/linkify-string.js index 1e4f3c01..04ec320b 100644 --- a/templates/linkify-string.js +++ b/templates/linkify-string.js @@ -3,7 +3,4 @@ var tokenize = linkify.tokenize; <%= contents %> window.linkifyStr = linkifyStr; -String.prototype.linkify = function (options) { - return linkifyStr(this, options); -}; })(window.linkify); From 754f7b575c9b1cf472ce0042f3005f19644a5476 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 18 Jan 2015 23:48:56 -0500 Subject: [PATCH 19/67] Updated docs and renamed test files Will try out running browser tests with Karma --- README.md | 323 +++++++----------- src/linkify-string.js | 1 - test/benchmarks.js | 47 ++- ...nkify-string.js => linkify-string-test.js} | 0 .../core/{parser.js => parser-test.js} | 0 .../core/{scanner.js => scanner-test.js} | 0 .../state/{character.js => character-test.js} | 0 .../state/{stateify.js => stateify-test.js} | 0 .../core/state/{token.js => token-test.js} | 0 .../core/tokens/{multi.js => multi-test.js} | 0 .../core/tokens/{text.js => text-test.js} | 0 .../plugins/{hashtag.js => hashtag-test.js} | 0 testem.json | 6 - 13 files changed, 161 insertions(+), 216 deletions(-) rename test/spec/{linkify-string.js => linkify-string-test.js} (100%) rename test/spec/linkify/core/{parser.js => parser-test.js} (100%) rename test/spec/linkify/core/{scanner.js => scanner-test.js} (100%) rename test/spec/linkify/core/state/{character.js => character-test.js} (100%) rename test/spec/linkify/core/state/{stateify.js => stateify-test.js} (100%) rename test/spec/linkify/core/state/{token.js => token-test.js} (100%) rename test/spec/linkify/core/tokens/{multi.js => multi-test.js} (100%) rename test/spec/linkify/core/tokens/{text.js => text-test.js} (100%) rename test/spec/linkify/plugins/{hashtag.js => hashtag-test.js} (100%) delete mode 100644 testem.json diff --git a/README.md b/README.md index 488506b0..b59b4975 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ [![Node Dependencies](https://david-dm.org/SoapBox/jQuery-linkify/dev-status.png)](https://david-dm.org/SoapBox/jQuery-linkify#info=devDependencies&view=table) +Linkify is a small yet comprehensive JavaScript plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. + __Jump to__ + +- [Features](#features) - [Demo](#demo) - [Installation and Usage](#installation-and-usage) - [Quick Start](#quick-start) @@ -10,16 +14,21 @@ __Jump to__ - [Node.js/Browserify](#node-js-browserify) - [AMD Modules](#amd-modules) - [Browser](#browser) -- [Examples](#examples) - - [Basic Usage](#basic-usage) - - [Usage via HTML attributes](#usage-via-html-attributes) -- [Options](#options) -- [Building and Development Tasks](#building-and-development-tasks) - - [Setup](#setup) - - [Development](#development) -- [Authors](#authors) - -Linkify is a jQuery plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. +- [API](#api) + - [`string`](#string) + - [`jquery`](#string) + - [Options](#options) +- [Contributing](#contributing) + [Authors](#authors) +- [License](#license) + +## Features + +* **Accuracy**
Linkify uses a (close to) complete list of valid top-level domains to ensure that only valid URLs and email addresses are matched. +* **Speed**
Each string is analyzied exactly once to detect every kind of linkable entity +* **Extensibility**
Linkify is designed to be fast and lightweight, but comes with a powerful plugin API that lets you detect even more information like #hashtags and @mentions. +* **Small footprint**
Linkify and its jQuery interface clock in at approximately 15KB minified (5KB gzipped) - approximately 50% the size of Twitter Text +* **Modern implementation**
Linkify is written in ECMAScript6 and compiles to ES5 for modern JavaScript runtimes. ## Demo [Launch demo](http://soapbox.github.io/jQuery-linkify/) @@ -35,26 +44,24 @@ Add [linkify](#) and [linkify-jquery](#) to your HTML following jQuery: ``` -### Usage - -#### Node.js/Browserify +### Node.js/Browserify ```js var linkify = require('linkifyjs'); @@ -62,15 +69,15 @@ var linkifyInterface = require('linkifyjs/'); require('linkifyjs/plugin/')(linkify); linkify.find('github.com'); -linkifyInterface(target, options); +linkifyInterface(target, options); // varies ``` -#### AMD modules +### AMD ```html - + ``` -#### Browser +### Browser ```html - + ``` ## Downloads -* linkify _(required)_ · [View docs](#linkify) - * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify.js) - * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify.amd.js) - * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify.min.js) - * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify.amd.min.js) +**[linkify](#api)** _(required)_
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.amd.js) + +**Interfaces** _(recommended - include at least one)_ -**Interfaces** _(recommended - include at least one)_ · [View docs](#linkify) +* **[string](#linkify-string)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.js) -* [string](#string) · [View docs](#linkify) - * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.js) - * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.amd.js) - * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.min.js) - * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-string.amd.min.js) -**Plugins** _(optional)_ · [View docs](#linkify) +**Plugins** _(optional)_ -* [hashtag](#plugin-hashtag) · [View docs](#linkify) - * [raw](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.js) - * [AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.amd.js) - * [minified](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.min.js) - * [minified AMD](https://github.com/nfrasser/linkify-shim/blob/master/linkify-plugin-hashtag.amd.min.js) +* **[hashtag](#linkify-plugin-hashtag)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.js) ## API @@ -174,8 +170,8 @@ Is the given string a link? Not to be used for strict validation - See [Caveats] **Returns** _`Boolean`_ ```js -linkify.test('google.dev'); // false linkify.test('google.com'); // true +linkify.test('google.com', 'email'); // false ``` #### linkify.tokenize _(str)_ @@ -190,7 +186,56 @@ Internal method used to perform lexicographical analysis on the given string and ### Interfaces -#### string +#### linkify-jquery + +Provides the Linkify jQuery plugin. + +```js +// TODO: How do you build a Browserify jQuery plugin?? +var $ = require('jquery'); +require('linkifyjs/jquery')($); +``` + +```html + + + + + + + + + + + +``` + +**Usage** + +```js +$(selector).linkify(options); +``` + +**DOM Data API** + +```html + +
...
+ + +... +``` + +**Params** + +* `Object` [**`options`**] [Options hash](#) + + +#### linkify-string Interface for replacing links within native strings with anchor tags. Note that this function will **not** parse HTML strings - use [linkify-dom](#) or [linkify-jquery](#) instead. @@ -236,13 +281,14 @@ var options = {/* ... */}; **Returns** _`String`_ Linkified string + ### Plugins Plugins provide no new interfaces but add additional detection functionality to Linkify. A plugic plugin API is currently in the works. #### hashtag -Adds basic support for Twitter-style hashtags +Adds basic support for Twitter-style hashtags. ```js // Node.js/Browserify @@ -272,10 +318,10 @@ var options = {/* ... */}; var str = "Linkify is #super #rad"; linkify.find(str); -// [ -// {type: 'hashtag', value: "#super", href: "#super"}, -// {type: 'hashtag', value: "#rad", href: "#rad"} -// ] +/* [ + {type: 'hashtag', value: "#super", href: "#super"}, + {type: 'hashtag', value: "#rad", href: "#rad"} +] */ // If the linkifyStr interface has also been included linkifyStr(str) @@ -283,151 +329,44 @@ linkifyStr(str) ``` -### Usage via HTML attributes - -Linkify also provides a DOM data- API. The following code will find links in the `#linkify-example` paragraph element: - -```html -

- Lorem ipsum dolor sit amet, consectetur adipisicing - elit, sed do eiusmod tempor incididunt ut labore et - dolore magna aliqua. -

-``` - -Pass in a selector instead of this to linkify every element with that selector. The example below linkifies every paragraph and `.plain-text` element in the bodytag: - -```html - - ... - -``` - ## Options Linkify is applied with the following default options. Below is a description of each. -```javascript -$('selector').linkify({ - tagName: 'a', - target: '_blank', - newLine: '\n', - linkClass: null, - linkAttributes: null -}); -``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OptionTypeDefaultDescriptionData Attribute
tagNameString"a" - The tag that should be used to wrap each URL. This is - useful for cases where a tags might be - innapropriate, or might syntax problems (e.g., finding - URLs inside an a tag). - - data-linkify-tagname -
targetString"_blank"target attribute for each linkified tag.data-linkify-target
newLineString"\n" - The character to replace new lines with. Replace with - "<br>" to space out multi-line user - content. - data-linkify-newline
linkClassStringnull - The class to be added to each linkified tag. An extra .linkified class ensures that each link will be clickable, regardless of the value of tagName. Linkify won't attempt finding links in .linkified elements. - data-linkify-linkclass
linkAttributesObjectnull - HTML attributes to add to each linkified tag. In the - following example, the tabindex and - rel attributes will be added to each link. - -
-$('p').linkify({
-	linkAttributes: {
-		tabindex: 0,
-		rel: 'nofollow'
-	}
-});
-
- -
N/A
- -## Building and Development Tasks - -### Setup - -Linkify uses [Grunt](http://gruntjs.com/) for building and testing, and -[Bower](http://bower.io/) for dependency management. Both can be installed -via [npm](https://npmjs.org/) by running: - -```bash -npm install -g grunt-cli -npm install -g bower -``` - -Once you have those, navigate into the repo's root directory and run - -```bash -npm install && bower install +```js +var options = { + tagName: 'span', + defaultProtocol: 'https', + target: '_parent', + nl2br: true, + linkClass: 'a-new-link', + linkAttributes: { + rel: 'nofollow' + }, + format: function (link, type) { + if (type === 'hashtag') { + link = link.toLowerCase(); + } + return link; + }, + formatHref: function (link, type) { + if (type === 'hashtag') { + link = 'https://twitter.com/hashtag/' + link.replace('#', ''); + } + return link; + } +}; + +// jQuery +$('selector').linkify(options); + +// String +linkifyStr(str, options); +str.linkify(options); ``` -### Development - -Each of these tasks can be called by running `grunt taskName` from the -repo's root folder. - -1. `default`: Also available by just calling `grunt`, this task tests -the plugin code in the `src` folder for JSHint compliance and builds and -minifies it into the `dist` folder. - -2. `demo`: Builds everything and launches the demo page at -[localhost:8000](http://localhost:8000/). -3. `test`: Runs the complete test suite, including JSHint and QUnit. QUnit -tests will be executed at [localhost:8001](http://localhost:8000/). +## Contributing ## Authors diff --git a/src/linkify-string.js b/src/linkify-string.js index 6f764aca..f994043a 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -1,6 +1,5 @@ /** Convert strings of text into linkable HTML text - TODO: Support for computed attributes based on the type? */ import {tokenize} from './linkify'; diff --git a/test/benchmarks.js b/test/benchmarks.js index b79b477e..73fc97d2 100644 --- a/test/benchmarks.js +++ b/test/benchmarks.js @@ -1,36 +1,49 @@ -var scanner = require('../lib/scanner'), sum = 0; +var usageInitial = process.memoryUsage(); +var linkify = require('../lib/linkify'); +var usageLinkify = process.memoryUsage(); -var ITERATIONS = 2000; +var sum = 0, ITERATIONS = 2000; function benchmark() { var start = new Date(), end, diff; - scanner.run('The URL is http://google.com The URL is http://google.com'); - scanner.run('google.com'); - scanner.run('I like google.com the most I like google.com the most'); - scanner.run('I like Google.com the most'); - scanner.run('there are two tests, brennan.com and nick.ca -- do they work?'); - scanner.run('there are two tests!brennan.com. and nick.ca? -- do they work?'); - scanner.run('This [i.imgur.com/ckSj2Ba.jpg)] should also work'); - scanner.run('A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look'); - scanner.run('SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com?q=.exe flickr.com/linktoimage.jpg'); - scanner.run('None.of these.should be.Links okay.please?'); - scanner.run('Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com'); - scanner.run('t.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?'); - scanner.run('This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/'); - scanner.run('About a year ago Graham and I went to Google IO (https://developers.google.com/events/io/) to learn about some upcoming technology and meet some tech folk in the valley. The experience was great. We met a bunch of great people and got our hands on some new technology — check out this page for more on our experience http://digitalmediazone.ryerson.ca/toronto-incubator/brennans-experience-at-google-io/experience. Beyond everything else, the best thing we got out of that conference was a technology/development mentor & a new startup development process. As soapboxhq.com grew, we tweaked our development and deployment process as needed. At the very start we used cheap hosting providers such as ca.godaddy.com and learned to deal with their limitations. We knew there were other ways of doing things, but they seemed to add complex rules and process. This worked for us, so why fix it? We then met Ian (http://iandouglas.com/about/) at Google IO, who agreed to share some of his insight from scaling over and over again. Ian is a senior web developer/architect working over at Sendgrid. Ian is awesome and we really take his advice to heart. He deserves the credit for a lot of what you see below (including the joke I shamelessly stole from him). To see the rest of this post, visit http://soapboxhq.com/blog/startup-development-process-how-we-develop/ or email soapbox-dev-team@example.com.'); + // linkify.tokenize('The URL is http://google.com The URL is http://google.com'); + // linkify.tokenize('google.com'); + // linkify.tokenize('I like google.com the most I like google.com the most'); + // linkify.tokenize('I like Google.com the most'); + // linkify.tokenize('there are two tests, brennan.com and nick.ca -- do they work?'); + // linkify.tokenize('there are two tests!brennan.com. and nick.ca? -- do they work?'); + // linkify.tokenize('This [i.imgur.com/ckSj2Ba.jpg)] should also work'); + // linkify.tokenize('A link is http://nick.is.awesome/?q=nick+amazing&nick=yo%29%30hellp another is http://nick.con/?q=look'); + // linkify.tokenize('SOme URLS http://google.com https://google1.com google2.com google.com/search?q=potatoes+oven goo.gl/0192n1 google.com?q=asda test bit.ly/0912j www.bob.com indigo.dev.soapbox.co/mobile google.com?q=.exe flickr.com/linktoimage.jpg'); + // linkify.tokenize('None.of these.should be.Links okay.please?'); + // linkify.tokenize('Here are some random emails: nick@soapbox.com, nick@soapbox.soda (invalid), Nick@dev.dev.soapbox.co, random nick.frasser_hitsend@http://facebook.com'); + // linkify.tokenize('t.c.com/sadqad is a great domain, so is ftp://i.am.a.b.ca/ okay?'); + // linkify.tokenize('This port is too short someport.com: this port is too long http://googgle.com:789023/myQuery this port is just right https://github.com:8080/SoapBox/jQuery-linkify/'); + // + linkify.tokenize('About a year ago Graham and I went to Google IO (https://developers.google.com/events/io/) to learn about some upcoming technology and meet some tech folk in the valley. The experience was great. We met a bunch of great people and got our hands on some new technology — check out this page for more on our experience http://digitalmediazone.ryerson.ca/toronto-incubator/brennans-experience-at-google-io/experience. Beyond everything else, the best thing we got out of that conference was a technology/development mentor & a new startup development process. As soapboxhq.com grew, we tweaked our development and deployment process as needed. At the very start we used cheap hosting providers such as ca.godaddy.com and learned to deal with their limitations. We knew there were other ways of doing things, but they seemed to add complex rules and process. This worked for us, so why fix it? We then met Ian (http://iandouglas.com/about/) at Google IO, who agreed to share some of his insight from scaling over and over again. Ian is a senior web developer/architect working over at Sendgrid. Ian is awesome and we really take his advice to heart. He deserves the credit for a lot of what you see below (including the joke I shamelessly stole from him). To see the rest of this post, visit http://soapboxhq.com/blog/startup-development-process-how-we-develop/ or email soapbox-dev-team@example.com.'); end = new Date(); - diff = end.valueOf() - start.valueOf(); sum += diff; } + console.log('Doing ' + ITERATIONS + ' iterations...'); console.log('Start:', (new Date()).valueOf()); + +var usageBeforeIterations = process.memoryUsage(); for (var i = 0; i < ITERATIONS; i++) { benchmark(); } +var usageAfterIterations = process.memoryUsage(); + console.log('End:', (new Date()).valueOf()); console.log('Total time (ms):', sum); console.log('Average (ms):', sum/i); +console.log('Memory usage:'); +console.log(' Before require:', usageInitial); +console.log(' After require:', usageLinkify); +console.log(' Before benchmark:', usageBeforeIterations); +console.log(' After benchmark:', usageAfterIterations); + diff --git a/test/spec/linkify-string.js b/test/spec/linkify-string-test.js similarity index 100% rename from test/spec/linkify-string.js rename to test/spec/linkify-string-test.js diff --git a/test/spec/linkify/core/parser.js b/test/spec/linkify/core/parser-test.js similarity index 100% rename from test/spec/linkify/core/parser.js rename to test/spec/linkify/core/parser-test.js diff --git a/test/spec/linkify/core/scanner.js b/test/spec/linkify/core/scanner-test.js similarity index 100% rename from test/spec/linkify/core/scanner.js rename to test/spec/linkify/core/scanner-test.js diff --git a/test/spec/linkify/core/state/character.js b/test/spec/linkify/core/state/character-test.js similarity index 100% rename from test/spec/linkify/core/state/character.js rename to test/spec/linkify/core/state/character-test.js diff --git a/test/spec/linkify/core/state/stateify.js b/test/spec/linkify/core/state/stateify-test.js similarity index 100% rename from test/spec/linkify/core/state/stateify.js rename to test/spec/linkify/core/state/stateify-test.js diff --git a/test/spec/linkify/core/state/token.js b/test/spec/linkify/core/state/token-test.js similarity index 100% rename from test/spec/linkify/core/state/token.js rename to test/spec/linkify/core/state/token-test.js diff --git a/test/spec/linkify/core/tokens/multi.js b/test/spec/linkify/core/tokens/multi-test.js similarity index 100% rename from test/spec/linkify/core/tokens/multi.js rename to test/spec/linkify/core/tokens/multi-test.js diff --git a/test/spec/linkify/core/tokens/text.js b/test/spec/linkify/core/tokens/text-test.js similarity index 100% rename from test/spec/linkify/core/tokens/text.js rename to test/spec/linkify/core/tokens/text-test.js diff --git a/test/spec/linkify/plugins/hashtag.js b/test/spec/linkify/plugins/hashtag-test.js similarity index 100% rename from test/spec/linkify/plugins/hashtag.js rename to test/spec/linkify/plugins/hashtag-test.js diff --git a/testem.json b/testem.json deleted file mode 100644 index a7f32ca2..00000000 --- a/testem.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "framework": "mocha", - "src_files": [ - "" - ] -} From 960d375eced02ca5bb5b99af178747f8141db31d Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 19 Jan 2015 02:04:54 -0500 Subject: [PATCH 20/67] Further tests updates, dependencies Karma test runner fully working, cross-browser SauceLabs configuration still in the works. --- gulpfile.js | 33 ++++++++-- package.json | 45 ++++++++------ test/chrome.conf.js | 18 ++++++ test/ci.conf.js | 50 +++++++++++++++ test/conf.js | 62 +++++++++++++++++++ test/dev.conf.js | 17 +++++ test/index.js | 4 +- test/init.js | 1 + test/spec/linkify-string-test.js | 2 +- test/spec/linkify/core/parser-test.js | 6 +- test/spec/linkify/core/scanner-test.js | 4 +- .../spec/linkify/core/state/character-test.js | 4 +- test/spec/linkify/core/state/stateify-test.js | 6 +- test/spec/linkify/core/state/token-test.js | 4 +- test/spec/linkify/core/tokens/multi-test.js | 4 +- test/spec/linkify/core/tokens/text-test.js | 2 +- test/spec/linkify/plugins/hashtag-test.js | 4 +- 17 files changed, 223 insertions(+), 43 deletions(-) create mode 100644 test/chrome.conf.js create mode 100644 test/ci.conf.js create mode 100644 test/conf.js create mode 100644 test/dev.conf.js create mode 100644 test/init.js diff --git a/gulpfile.js b/gulpfile.js index a23a8f14..57f1c357 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,8 +1,9 @@ var gulp = require('gulp'), -path = require('path'), +amdOptimize = require('amd-optimize'), glob = require('glob'), -stylish = require('jshint-stylish'), -amdOptimize = require('amd-optimize'); +karma = require('karma').server, +path = require('path'), +stylish = require('jshint-stylish'); var // Gulp plugins concat = require('gulp-concat'), @@ -171,10 +172,30 @@ gulp.task('jshint', function () { Run mocha tests */ gulp.task('mocha', function () { - gulp.src(paths.test, {read: false}) + return gulp.src(paths.test, {read: false}) .pipe(mocha()); }); +gulp.task('karma', function () { + return karma.start({ + configFile: __dirname + '/test/dev.conf.js', + singleRun: true + }); +}); + +gulp.task('karma-chrome', function () { + karma.start({ + configFile: __dirname + '/test/chrome.conf.js', + }); +}); + +gulp.task('karma-ci', function () { + karma.start({ + configFile: __dirname + '/test/ci.conf.js', + singleRun: true + }); +}); + gulp.task('uglify', function () { gulp.src('build/*.js') .pipe(gulp.dest('dist')) // non-minified copy @@ -196,6 +217,8 @@ gulp.task('build', [ gulp.task('dist', ['build', 'uglify']); gulp.task('test', ['jshint', 'build', 'mocha']); +gulp.task('test-ci', ['karma-ci']); +// Using with other tasks causes an error here for some reason /** Build app and begin watching for changes @@ -203,3 +226,5 @@ gulp.task('test', ['jshint', 'build', 'mocha']); gulp.task('default', ['6to5'], function () { gulp.watch(paths.src, ['6to5']); }); + + diff --git a/package.json b/package.json index 77e6bc50..9d99f57a 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,31 @@ }, "author": "SoapBox Innovations (@SoapBoxHQ)", "license": "MIT", + "dependencies": {}, "devDependencies": { - "amd-optimize": "^0.4.3", - "chai": "^1.10.0", - "closure-compiler": "^0.2.6", - "glob": "^4.3.2", - "gulp": "^3.8.10", - "gulp-6to5": "^2.0.0", - "gulp-closure-compiler": "^0.2.14", - "gulp-concat": "^2.4.3", - "gulp-es6-transpiler": "^1.0.1", - "gulp-jshint": "^1.9.0", - "gulp-mocha": "^2.0.0", - "gulp-rename": "^1.2.0", - "gulp-sourcemaps": "^1.3.0", - "gulp-uglify": "^1.0.2", - "gulp-wrap": "^0.8.0", - "jshint-stylish": "^1.0.0", - "lodash": "^2.4.1" - }, - "dependencies": {} + "amd-optimize": "0.4.x", + "chai": "1.10.x", + "closure-compiler": "0.2.x", + "glob": "4.3.x", + "gulp": "3.8.x", + "gulp-6to5": "2.0.x", + "gulp-closure-compiler": "0.2.x", + "gulp-concat": "2.4.x", + "gulp-jshint": "1.9.x", + "gulp-mocha": "2.0.x", + "gulp-rename": "1.2.x", + "gulp-uglify": "1.0.x", + "gulp-wrap": "0.8.x", + "jquery": "2.1.x", + "jsdom": "3.0.x", + "jshint-stylish": "1.0.x", + "karma": "0.12.x", + "karma-browserify": "2.0.x", + "karma-chrome-launcher": "0.1.x", + "karma-mocha": "0.1.x", + "karma-phantomjs-launcher": "0.1.x", + "karma-sauce-launcher": "0.2.x", + "lodash": "^2.4.1", + "mocha": "2.1.x" + } } diff --git a/test/chrome.conf.js b/test/chrome.conf.js new file mode 100644 index 00000000..f5fd1d39 --- /dev/null +++ b/test/chrome.conf.js @@ -0,0 +1,18 @@ +// Karma Chrome configuration +// Just opens Google Chrome for testing + +var +base = require('./conf'), +extend = require('lodash').extend; + +module.exports = function (config) { + + config.set(extend(base, { + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + browsers: ['Chrome'], + + })); +}; diff --git a/test/ci.conf.js b/test/ci.conf.js new file mode 100644 index 00000000..ec74d3e1 --- /dev/null +++ b/test/ci.conf.js @@ -0,0 +1,50 @@ +// Karma CI configuration + +var +base = require('./conf'), +extend = require('lodash').extend; + +module.exports = function (config) { + + // Check out https://saucelabs.com/platforms for all browser/platform combos + var customLaunchers = { + sl_chrome: { + base: 'SauceLabs', + browserName: 'chrome', + platform: 'Windows 7', + version: '35' + }, + sl_firefox: { + base: 'SauceLabs', + browserName: 'firefox', + version: '30' + }, + sl_ios_safari: { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.9', + version: '7.1' + }, + sl_ie_11: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8.1', + version: '11' + } + }; + + config.set(extend(base, { + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + sauceLabs: { + testName: 'Linkify Browser Tests' + }, + + logLevel: config.LOG_WARN, + browsers: ['Chrome'], + autoWatch: false, + singleRun: true + + })); +}; diff --git a/test/conf.js b/test/conf.js new file mode 100644 index 00000000..c56b3461 --- /dev/null +++ b/test/conf.js @@ -0,0 +1,62 @@ +module.exports = { + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: __dirname.replace(/\/?test\/?$/, '/'), + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'browserify'], + + + // list of files / patterns to load in the browser + files: [ + 'lib/*.js', + 'lib/**/*.js', + 'test/init.js', + 'test/spec/*.js', + 'test/spec/**/*.js', + ], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'lib/*.js': ['browserify'], + 'lib/**/*.js': ['browserify'], + 'test/init.js': ['browserify'], + 'test/spec/*.js': ['browserify'], + 'test/spec/**/*.js': ['browserify'], + }, + + browserify: { + debug: false, + // transform: [ 'brfs' ] + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false +} diff --git a/test/dev.conf.js b/test/dev.conf.js new file mode 100644 index 00000000..a5c01a89 --- /dev/null +++ b/test/dev.conf.js @@ -0,0 +1,17 @@ +// Karma Development configuration + +var +base = require('./conf'), +extend = require('lodash').extend; + +module.exports = function (config) { + + config.set(extend(base, { + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + browsers: ['PhantomJS'], + + })); +}; diff --git a/test/index.js b/test/index.js index 27e23eef..33321ca6 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,6 @@ var glob = require('glob'); -global.__base = __dirname.replace(/test$/, ''); -require('chai').should(); // Initialize should assertions +require('./init'); +global.__base = __dirname.replace(/test$/, 'lib/'); // Require test files glob.sync('./spec/**/*.js', {cwd: __dirname}).map(require); diff --git a/test/init.js b/test/init.js new file mode 100644 index 00000000..d6413243 --- /dev/null +++ b/test/init.js @@ -0,0 +1 @@ +require('chai').should(); // Initialize should assertions diff --git a/test/spec/linkify-string-test.js b/test/spec/linkify-string-test.js index c0ceef3e..a957127c 100644 --- a/test/spec/linkify-string-test.js +++ b/test/spec/linkify-string-test.js @@ -1,6 +1,6 @@ /*jshint scripturl:true*/ -var linkifyStr = require(__base + 'lib/linkify-string'); +var linkifyStr = require('../../lib/linkify-string'); /** Gracefully truncate a string to a given limit. Will replace extraneous diff --git a/test/spec/linkify/core/parser-test.js b/test/spec/linkify/core/parser-test.js index fd9303d7..6d2f1d98 100644 --- a/test/spec/linkify/core/parser-test.js +++ b/test/spec/linkify/core/parser-test.js @@ -1,7 +1,7 @@ var -scanner = require(__base + 'lib/linkify/core/scanner'), -parser = require(__base + 'lib/linkify/core/parser'), -MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens').multi; +scanner = require('../../../../lib/linkify/core/scanner'), +parser = require('../../../../lib/linkify/core/parser'), +MULTI_TOKENS = require('../../../../lib/linkify/core/tokens').multi; var TEXT = MULTI_TOKENS.TEXT, diff --git a/test/spec/linkify/core/scanner-test.js b/test/spec/linkify/core/scanner-test.js index 7511da89..b6dcec96 100644 --- a/test/spec/linkify/core/scanner-test.js +++ b/test/spec/linkify/core/scanner-test.js @@ -1,6 +1,6 @@ var -scanner = require(__base + 'lib/linkify/core/scanner'), -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text; +scanner = require('../../../../lib/linkify/core/scanner'), +TEXT_TOKENS = require('../../../../lib/linkify/core/tokens').text; var DOMAIN = TEXT_TOKENS.DOMAIN, diff --git a/test/spec/linkify/core/state/character-test.js b/test/spec/linkify/core/state/character-test.js index dd2cdb38..14a574d0 100644 --- a/test/spec/linkify/core/state/character-test.js +++ b/test/spec/linkify/core/state/character-test.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, -CharacterState = require(__base + '/lib/linkify/core/state').CharacterState; +TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, +CharacterState = require('../../../../../lib/linkify/core/state').CharacterState; describe('CharacterState', function () { var S_START, S_DOT, S_NUM; diff --git a/test/spec/linkify/core/state/stateify-test.js b/test/spec/linkify/core/state/stateify-test.js index c1a28490..b4f19824 100644 --- a/test/spec/linkify/core/state/stateify-test.js +++ b/test/spec/linkify/core/state/stateify-test.js @@ -1,7 +1,7 @@ var -TOKENS = require(__base + 'lib/linkify/core/tokens').text, -State = require(__base + 'lib/linkify/core/state').CharacterState, -stateify = require(__base + 'lib/linkify/core/state').stateify; +TOKENS = require('../../../../../lib/linkify/core/tokens').text, +State = require('../../../../../lib/linkify/core/state').CharacterState, +stateify = require('../../../../../lib/linkify/core/state').stateify; describe('stateify', function () { var S_START; diff --git a/test/spec/linkify/core/state/token-test.js b/test/spec/linkify/core/state/token-test.js index d4a985e0..eb8a20e4 100644 --- a/test/spec/linkify/core/state/token-test.js +++ b/test/spec/linkify/core/state/token-test.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, -TokenState = require(__base + 'lib/linkify/core/state').TokenState; +TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, +TokenState = require('../../../../../lib/linkify/core/state').TokenState; describe('TokenState', function () { var TS_START; diff --git a/test/spec/linkify/core/tokens/multi-test.js b/test/spec/linkify/core/tokens/multi-test.js index 35fb6b4e..af2eab0e 100644 --- a/test/spec/linkify/core/tokens/multi-test.js +++ b/test/spec/linkify/core/tokens/multi-test.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text, -MULTI_TOKENS = require(__base + 'lib/linkify/core/tokens').multi; +TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, +MULTI_TOKENS = require('../../../../../lib/linkify/core/tokens').multi; describe('MULTI_TOKENS', function () { diff --git a/test/spec/linkify/core/tokens/text-test.js b/test/spec/linkify/core/tokens/text-test.js index 8bd5440d..7d6ee278 100644 --- a/test/spec/linkify/core/tokens/text-test.js +++ b/test/spec/linkify/core/tokens/text-test.js @@ -1,4 +1,4 @@ -var TEXT_TOKENS = require(__base + 'lib/linkify/core/tokens').text; +var TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text; describe('TEXT_TOKENS', function () { diff --git a/test/spec/linkify/plugins/hashtag-test.js b/test/spec/linkify/plugins/hashtag-test.js index 3664d593..28999a66 100644 --- a/test/spec/linkify/plugins/hashtag-test.js +++ b/test/spec/linkify/plugins/hashtag-test.js @@ -1,7 +1,7 @@ /*jshint -W030 */ var -linkify = require(__base + 'lib/linkify'), -hashtag = require(__base + 'lib/linkify/plugins/hashtag'); +linkify = require('../../../../lib/linkify'), +hashtag = require('../../../../lib/linkify/plugins/hashtag'); describe('Linkify Hashtag Plugin', function () { From 54c8a9ee3c75c1f60008f21c445331db17788dd4 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Wed, 28 Jan 2015 12:45:19 -0500 Subject: [PATCH 21/67] Options and initial linkify-element code Still not fully complete but on its way --- gulpfile.js | 2 +- src/linkify-element.js | 80 ++++++++++++++++++++++++++++++++++++ src/linkify-string.js | 61 ++++++++++----------------- src/linkify.js | 3 +- src/linkify/core/scanner.js | 10 ++--- src/linkify/utils/options.js | 28 +++++++++++++ 6 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 src/linkify-element.js create mode 100644 src/linkify/utils/options.js diff --git a/gulpfile.js b/gulpfile.js index 57f1c357..450c1c50 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -63,7 +63,7 @@ gulp.task('6to5-amd', function () { // Closure compiler is used here since it can correctly concatenate CJS modules gulp.task('build-core', function () { - gulp.src(['lib/linkify/core/*.js', 'lib/linkify.js']) + gulp.src(['lib/linkify/core/*.js', 'lib/linkify/utils/*.js', 'lib/linkify.js']) .pipe(closureCompiler({ compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', fileName: 'linkify.js', diff --git a/src/linkify-element.js b/src/linkify-element.js new file mode 100644 index 00000000..0637b640 --- /dev/null +++ b/src/linkify-element.js @@ -0,0 +1,80 @@ +/** + Linkify a HTML DOM node +*/ + +import {tokenize} from './linkify'; +import * as options from './linkify/utils/options'; + +let +HTML_NODE = 1, +TXT_NODE = 3; + +/** + Given an array of MultiTokens, return an array of Nodes that are either + (a) Plain Text nodes (node type 3) + (b) Anchor tag nodes (usually, unless tag name is overriden in the options) + + Takes the same options as linkifyElement and an optional doc element (this should be passed in by linkifyElement) +*/ +function tokensToNodes(tokens, opts, doc) { + // TODO: Write this +} + +// Requires document.createElement +function linkifyElement(element, opts, doc=null) { + + doc = doc || window && window.document || global && global.document; + + if (!doc) { + throw new Error( + 'Cannot find document implementation. ' + + 'If you are in a non-browser environment like Node.js, ' + + 'pass the document implementation as the third argument to linkifyElement.' + ); + } + + // Can the element be linkified? + if (!element || typeof element !== 'object' || element.nodeType !== HTML_NODE) { + throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`); + } + + // Is this element already a link? + if (element.tagName.toLowerCase() === 'a' /*|| element.hasClass('linkified')*/) { + // No need to linkify + return element; + } + + let + children = [], + childElement = element.firstChild; + + while (childElement) { + + switch (childElement.nodeType) { + case HTML_NODE: + children.push(linkifyElement(childElement, opts)); + break; + case TXT_NODE: + + let + str = childElement.nodeValue, + tokens = tokenize(str); + + children.push(...tokensToNodes(tokens, opts, doc)); + + break; + + default: children.push(childElement); break; + } + + childElement = childElement.nextSibling; + } + + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + return linkifyElement; +} + +module.exports = linkifyElement; diff --git a/src/linkify-string.js b/src/linkify-string.js index f994043a..a6258e58 100644 --- a/src/linkify-string.js +++ b/src/linkify-string.js @@ -2,14 +2,11 @@ Convert strings of text into linkable HTML text */ -import {tokenize} from './linkify'; - -function typeToTarget(type) { - return type === 'url' ? '_blank' : null; -} +import {tokenize, options} from './linkify'; function cleanText(text) { return text + .replace(/&/g, '&') .replace(//g, '>'); } @@ -30,58 +27,42 @@ function attributesToString(attributes) { return result.join(' '); } -function resolveOption(value, ...params) { - return typeof value === 'function' ? value(...params) : value; -} - -function noop(val) { - return val; -} - function linkifyStr(str, opts={}) { - let - attributes = opts.linkAttributes || null, - defaultProtocol = opts.defaultProtocol || 'http', - format = opts.format || noop, - formatHref = opts.formatHref || noop, - newLine = opts.newLine || false, // deprecated - nl2br = !!newLine || opts.nl2br || false, - tagName = opts.tagName || 'a', - target = opts.target || typeToTarget, - linkClass = opts.linkClass || 'linkified'; + opts = options.normalize(opts); - let result = []; - let tokens = tokenize(str); + let + tokens = tokenize(str), + result = []; for (let i = 0; i < tokens.length; i++ ) { let token = tokens[i]; if (token.isLink) { let - tagNameStr = resolveOption(tagName, token.type), - classStr = resolveOption(linkClass, token.type), - targetStr = resolveOption(target, token.type), - formatted = resolveOption(format, token.toString(), token.type), - href = token.toHref(defaultProtocol), - formattedHref = resolveOption(formatHref, href, token.type), - attributesHash = resolveOption(attributes, token.type); - - let link = `<${tagNameStr} href="${cleanAttr(formattedHref)}" class="${classStr}"`; - if (targetStr) { - link += ` target="${targetStr}"`; + tagName = options.resolve(opts.tagName, token.type), + linkClass = options.resolve(opts.linkClass, token.type), + target = options.resolve(opts.target, token.type), + formatted = options.resolve(opts.format, token.toString(), token.type), + href = token.toHref(opts.defaultProtocol), + formattedHref = options.resolve(opts.formatHref, href, token.type), + attributesHash = options.resolve(opts.attributes, token.type); + + let link = `<${tagName} href="${cleanAttr(formattedHref)}" class="${linkClass}"`; + if (target) { + link += ` target="${target}"`; } if (attributesHash) { link += ` ${attributesToString(attributesHash)}`; } - link += `>${cleanText(formatted)}`; + link += `>${cleanText(formatted)}`; result.push(link); - } else if (token.type === 'nl' && nl2br) { - if (newLine) { - result.push(newLine); + } else if (token.type === 'nl' && opts.nl2br) { + if (opts.newLine) { + result.push(opts.newLine); } else { result.push('
\n'); } diff --git a/src/linkify.js b/src/linkify.js index 3f9c656e..c9c7d573 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -1,3 +1,4 @@ +import * as options from './linkify/utils/options'; import * as scanner from './linkify/core/scanner'; import * as parser from './linkify/core/parser'; @@ -51,4 +52,4 @@ let test = function (str, type=null) { // Scanner and parser provide states and tokens for the lexicographic stage // (will be used to add additional link types) -export {find, parser, scanner, test, tokenize}; +export {find, options, parser, scanner, test, tokenize}; diff --git a/src/linkify/core/scanner.js b/src/linkify/core/scanner.js index 57a8ec95..f30b701a 100644 --- a/src/linkify/core/scanner.js +++ b/src/linkify/core/scanner.js @@ -54,7 +54,7 @@ S_WS.on(/[^\S\n]/, S_WS); // If any whitespace except newline, more whitespace! // Note that this is most accurate when tlds are in alphabetical order for (let i = 0; i < tlds.length; i++) { let newStates = stateify(tlds[i], S_START, T_TLD, T_DOMAIN); - domainStates.push.apply(domainStates, newStates); + domainStates.concat(newStates); } // Collect the states generated by different protocls @@ -64,9 +64,9 @@ partialProtocolFtpStates = stateify('ftp', S_START, T_DOMAIN, T_DOMAIN), partialProtocolHttpStates = stateify('http', S_START, T_DOMAIN, T_DOMAIN); // Add the states to the array of DOMAINeric states -domainStates.push.apply(domainStates, partialProtocolFileStates); -domainStates.push.apply(domainStates, partialProtocolFtpStates); -domainStates.push.apply(domainStates, partialProtocolHttpStates); +domainStates.concat(partialProtocolFileStates); +domainStates.concat(partialProtocolFtpStates); +domainStates.concat(partialProtocolHttpStates); let // Protocol states S_PROTOCOL_FILE = partialProtocolFileStates.pop(), @@ -88,7 +88,7 @@ S_PROTOCOL_SECURE.on(COLON, S_FULL_PROTOCOL); // Localhost let partialLocalhostStates = stateify('localhost', S_START, T_LOCALHOST, T_DOMAIN); -domainStates.push.apply(domainStates, partialLocalhostStates); +domainStates.concat(partialLocalhostStates); // Everything else // DOMAINs make more DOMAINs diff --git a/src/linkify/utils/options.js b/src/linkify/utils/options.js new file mode 100644 index 00000000..5f18c584 --- /dev/null +++ b/src/linkify/utils/options.js @@ -0,0 +1,28 @@ +function noop(val) { + return val; +} + +function typeToTarget(type) { + return type === 'url' ? '_blank' : null; +} + +function normalize(opts={}) { + let newLine = opts.newLine || false; // deprecated + return { + attributes: opts.linkAttributes || null, + defaultProtocol: opts.defaultProtocol || 'http', + format: opts.format || noop, + formatHref: opts.formatHref || noop, + newLine: opts.newLine || false, // deprecated + nl2br: !!newLine || opts.nl2br || false, + tagName: opts.tagName || 'a', + target: opts.target || typeToTarget, + linkClass: opts.linkClass || 'linkified' + }; +} + +function resolve(value, ...params) { + return typeof value === 'function' ? value(...params) : value; +} + +export {normalize, resolve}; From bb5e6a937792f0b9a2cf565fe6750a7fe350e533 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 1 Feb 2015 14:41:17 -0500 Subject: [PATCH 22/67] Tokens to nodes implementation Given an array of Multitoken instances, returns an array of HTML or Text nodes. --- src/linkify-element.js | 45 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/linkify-element.js b/src/linkify-element.js index 0637b640..41fd9085 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -17,11 +17,45 @@ TXT_NODE = 3; Takes the same options as linkifyElement and an optional doc element (this should be passed in by linkifyElement) */ function tokensToNodes(tokens, opts, doc) { - // TODO: Write this + let result = []; + + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + if (token.isLink) { + let + tagName = options.resolve(opts.tagName, token.type), + linkClass = options.resolve(opts.linkClass, token.type), + target = options.resolve(opts.target, token.type), + formatted = options.resolve(opts.format, token.toString(), token.type), + href = token.toHref(opts.defaultProtocol), + formattedHref = options.resolve(opts.formatHref, href, token.type), + attributesHash = options.resolve(opts.attributes, token.type); + + // Build the link + let link = doc.createElement(tagName); + link.setAttribute('href', formattedHref); + link.setAttribute('class', linkClass); + if (target) { + link.setAttribute('target', target); + } + + // Build up additional attributes + if (attributesHash) { + for (let attr in attributesHash) { + link.setAttribute(attr, attributesHash); + } + } + + link.appendChild(doc.createTextNode(formatted)); + result.push(link); + } + } + + return result; } // Requires document.createElement -function linkifyElement(element, opts, doc=null) { +function linkifyElement(element, opts, doc) { doc = doc || window && window.document || global && global.document; @@ -70,6 +104,7 @@ function linkifyElement(element, opts, doc=null) { childElement = childElement.nextSibling; } + // Clear out the element while (element.firstChild) { element.removeChild(element.firstChild); } @@ -77,4 +112,8 @@ function linkifyElement(element, opts, doc=null) { return linkifyElement; } -module.exports = linkifyElement; +export { linkifyElement }; +export default function (element, opts, doc=null) { + opts = options.normalize(opts); + return linkifyElement(element, opts, doc); +} From 87db493c99b0e2b2b9258f03381a6c6fd1ceb1cd Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 7 Feb 2015 22:23:41 -0500 Subject: [PATCH 23/67] Linkify element, reverting to push.apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In case this was breaking something… there’s some kind of problem with parsing --- src/linkify-element.js | 28 ++++++++++++++-------------- src/linkify/core/scanner.js | 10 +++++----- templates/linkify-element.amd.js | 1 + templates/linkify-element.js | 6 ++++++ test/index.html | 1 + 5 files changed, 27 insertions(+), 19 deletions(-) create mode 100644 templates/linkify-element.amd.js create mode 100644 templates/linkify-element.js diff --git a/src/linkify-element.js b/src/linkify-element.js index 41fd9085..6421f538 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -57,16 +57,6 @@ function tokensToNodes(tokens, opts, doc) { // Requires document.createElement function linkifyElement(element, opts, doc) { - doc = doc || window && window.document || global && global.document; - - if (!doc) { - throw new Error( - 'Cannot find document implementation. ' + - 'If you are in a non-browser environment like Node.js, ' + - 'pass the document implementation as the third argument to linkifyElement.' - ); - } - // Can the element be linkified? if (!element || typeof element !== 'object' || element.nodeType !== HTML_NODE) { throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`); @@ -86,15 +76,14 @@ function linkifyElement(element, opts, doc) { switch (childElement.nodeType) { case HTML_NODE: - children.push(linkifyElement(childElement, opts)); + children.push(linkifyElement(childElement, opts, doc)); break; case TXT_NODE: let str = childElement.nodeValue, tokens = tokenize(str); - - children.push(...tokensToNodes(tokens, opts, doc)); + children = children.concat(tokensToNodes(tokens, opts, doc)); break; @@ -113,7 +102,18 @@ function linkifyElement(element, opts, doc) { } export { linkifyElement }; -export default function (element, opts, doc=null) { +export default function exec(element, opts, doc=null) { + + doc = doc || window && window.document || global && global.document; + + if (!doc) { + throw new Error( + 'Cannot find document implementation. ' + + 'If you are in a non-browser environment like Node.js, ' + + 'pass the document implementation as the third argument to linkifyElement.' + ); + } + opts = options.normalize(opts); return linkifyElement(element, opts, doc); } diff --git a/src/linkify/core/scanner.js b/src/linkify/core/scanner.js index f30b701a..5cc5480d 100644 --- a/src/linkify/core/scanner.js +++ b/src/linkify/core/scanner.js @@ -54,7 +54,7 @@ S_WS.on(/[^\S\n]/, S_WS); // If any whitespace except newline, more whitespace! // Note that this is most accurate when tlds are in alphabetical order for (let i = 0; i < tlds.length; i++) { let newStates = stateify(tlds[i], S_START, T_TLD, T_DOMAIN); - domainStates.concat(newStates); + domainStates.push.apply(newStates); } // Collect the states generated by different protocls @@ -64,9 +64,9 @@ partialProtocolFtpStates = stateify('ftp', S_START, T_DOMAIN, T_DOMAIN), partialProtocolHttpStates = stateify('http', S_START, T_DOMAIN, T_DOMAIN); // Add the states to the array of DOMAINeric states -domainStates.concat(partialProtocolFileStates); -domainStates.concat(partialProtocolFtpStates); -domainStates.concat(partialProtocolHttpStates); +domainStates.push.apply(domainStates, partialProtocolFileStates); +domainStates.push.apply(domainStates, partialProtocolFtpStates); +domainStates.push.apply(domainStates, partialProtocolHttpStates); let // Protocol states S_PROTOCOL_FILE = partialProtocolFileStates.pop(), @@ -88,7 +88,7 @@ S_PROTOCOL_SECURE.on(COLON, S_FULL_PROTOCOL); // Localhost let partialLocalhostStates = stateify('localhost', S_START, T_LOCALHOST, T_DOMAIN); -domainStates.concat(partialLocalhostStates); +domainStates.push.apply(domainStates, partialLocalhostStates); // Everything else // DOMAINs make more DOMAINs diff --git a/templates/linkify-element.amd.js b/templates/linkify-element.amd.js new file mode 100644 index 00000000..339e4544 --- /dev/null +++ b/templates/linkify-element.amd.js @@ -0,0 +1 @@ +<%= contents %> diff --git a/templates/linkify-element.js b/templates/linkify-element.js new file mode 100644 index 00000000..6a87e648 --- /dev/null +++ b/templates/linkify-element.js @@ -0,0 +1,6 @@ +;(function (linkify) { +"use strict"; +var tokenize = linkify.tokenize; +<%= contents %> +window.linkifyElement = exec; +})(window.linkify); diff --git a/test/index.html b/test/index.html index d70bb31a..06b20745 100644 --- a/test/index.html +++ b/test/index.html @@ -8,6 +8,7 @@ + - - - + + + +

+ You let's get all up in the + http://element.co/?wat=this and the #swag +

-

+

You let's get all up in the http://element.co/?wat=this and the #swag

diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js new file mode 100644 index 00000000..0b3af8d3 --- /dev/null +++ b/test/spec/linkify-element-test.js @@ -0,0 +1,53 @@ +/*jshint -W030 */ +var +doc, testContainer, +jsdom = require('jsdom'), +linkifyElement = require('../../lib/linkify-element'); + +try { + doc = document; +} catch (e) { + doc = null; +} + +describe('linkify-element', function () { + + /** + Set up the JavaScript document and the element for it + This code allows testing on Node.js and on Browser environments + */ + before(function (done) { + + function onDoc(doc) { + testContainer = doc.createElement('div'); + testContainer.id = 'linkify-test-container'; + + testContainer.innerHtml = + 'Hello here are some links to ftp://awesome.com/?where=this ' + + 'and localhost:8080, pretty neat right?' + + '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

'; + + doc.body.appendChild(testContainer); + done(); + } + + if (doc) { return onDoc(doc); } + // no document element, use a virtual dom to test + + jsdom.env( + 'Linkify Test', + function (errors, window) { + if (errors) { throw errors; } + doc = window.document; + return onDoc(window.document); + } + ); + }); + + it('Works with default options', function () { + testContainer.should.be.type('object'); + linkifyElement(testContainer, null, doc); + (testContainer).should.be.okay; + }); + +}); From 32c262d0f125603b57254d53e91ccf2b3afaec2d Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 1 Mar 2015 15:51:10 -0500 Subject: [PATCH 27/67] Updating dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6to5 → Babel --- gulpfile.js | 38 +++++++++++++++++++------------------- package.json | 20 ++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b867aab3..7ca326c9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -11,7 +11,7 @@ closureCompiler = require('gulp-closure-compiler'), jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), rename = require('gulp-rename'), -to5 = require('gulp-6to5'), +babel = require('gulp-babel'), // formerly 6to5 uglify = require('gulp-uglify'), wrap = require('gulp-wrap'); @@ -22,7 +22,7 @@ var paths = { spec: 'test/spec/**.js' }; -var to5format = { +var babelformat = { comments: true, indent: { style: ' ' @@ -30,25 +30,25 @@ var to5format = { }; /** - ES6 ~> 6to5 (with CJS Node Modules) + ES6 ~> babel (with CJS Node Modules) This populates the `lib` folder, allows usage with Node.js */ -gulp.task('6to5', function () { +gulp.task('babel', function () { return gulp.src(paths.src) - .pipe(to5({format: to5format})) + .pipe(babel({format: babelformat})) .pipe(gulp.dest('lib')); }); /** - ES6 to 6to5 AMD modules + ES6 to babel AMD modules */ -gulp.task('6to5-amd', function () { +gulp.task('babel-amd', function () { gulp.src(paths.src) - .pipe(to5({ + .pipe(babel({ modules: 'amd', moduleIds: true, - format: to5format + format: babelformat // moduleRoot: 'linkifyjs' })) .pipe(gulp.dest('build/amd')) // Required for building plugins separately @@ -115,9 +115,9 @@ gulp.task('build-interfaces', function () { // Browser interface gulp.src(files.js) - .pipe(to5({ + .pipe(babel({ modules: 'ignore', - format: to5format + format: babelformat })) .pipe(wrap({src: 'templates/linkify-' + interface + '.js'})) .pipe(concat('linkify-' + interface + '.js')) @@ -133,7 +133,7 @@ gulp.task('build-interfaces', function () { }); /** - NOTE - Run '6to5' and '6to5-amd' first + NOTE - Run 'babel' and 'babel-amd' first */ gulp.task('build-plugins', function () { @@ -152,9 +152,9 @@ gulp.task('build-plugins', function () { // Global plugins gulp.src('src/linkify/plugins/' + plugin + '.js') - .pipe(to5({ + .pipe(babel({ modules: 'ignore', - format: to5format + format: babelformat })) .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.js'})) .pipe(concat('linkify-plugin-' + plugin + '.js')) @@ -226,8 +226,8 @@ gulp.task('uglify', function () { }); gulp.task('build', [ - '6to5', - '6to5-amd', + 'babel', + 'babel-amd', 'build-core', 'build-interfaces', 'build-plugins' @@ -240,10 +240,10 @@ gulp.task('test-ci', ['karma-ci']); // Using with other tasks causes an error here for some reason /** - Build app and begin watching for changes + Build JS and begin watching for changes */ -gulp.task('default', ['6to5'], function () { - gulp.watch(paths.src, ['6to5']); +gulp.task('default', ['babel'], function () { + gulp.watch(paths.src, ['babel']); }); diff --git a/package.json b/package.json index 9d99f57a..c3e8acc5 100644 --- a/package.json +++ b/package.json @@ -12,28 +12,28 @@ "dependencies": {}, "devDependencies": { "amd-optimize": "0.4.x", - "chai": "1.10.x", + "chai": "^2.1.0", "closure-compiler": "0.2.x", - "glob": "4.3.x", + "glob": "^4.4.1", "gulp": "3.8.x", - "gulp-6to5": "2.0.x", + "gulp-babel": "^4.0.0", "gulp-closure-compiler": "0.2.x", - "gulp-concat": "2.4.x", + "gulp-concat": "^2.5.2", "gulp-jshint": "1.9.x", "gulp-mocha": "2.0.x", "gulp-rename": "1.2.x", - "gulp-uglify": "1.0.x", - "gulp-wrap": "0.8.x", + "gulp-uglify": "^1.1.0", + "gulp-wrap": "^0.11.0", "jquery": "2.1.x", - "jsdom": "3.0.x", + "jsdom": "^4.0.1", "jshint-stylish": "1.0.x", - "karma": "0.12.x", - "karma-browserify": "2.0.x", + "karma": "^0.12.31", + "karma-browserify": "^4.0.0", "karma-chrome-launcher": "0.1.x", "karma-mocha": "0.1.x", "karma-phantomjs-launcher": "0.1.x", "karma-sauce-launcher": "0.2.x", - "lodash": "^2.4.1", + "lodash": "^3.3.1", "mocha": "2.1.x" } } From 089dede4c19d79ad828f73efebf3bd265741feb6 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 1 Mar 2015 15:51:26 -0500 Subject: [PATCH 28/67] Fixing newline token naming issues. --- src/linkify/core/parser.js | 2 +- src/linkify/core/tokens.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/linkify/core/parser.js b/src/linkify/core/parser.js index b33ce78b..bbb5f3d4 100644 --- a/src/linkify/core/parser.js +++ b/src/linkify/core/parser.js @@ -24,7 +24,7 @@ TT_AT = TEXT_TOKENS.AT, TT_COLON = TEXT_TOKENS.COLON, TT_DOT = TEXT_TOKENS.DOT, TT_LOCALHOST = TEXT_TOKENS.LOCALHOST, -TT_NL = TEXT_TOKENS.NL, +TT_NL = TEXT_TOKENS.TNL, TT_NUM = TEXT_TOKENS.NUM, TT_PLUS = TEXT_TOKENS.PLUS, TT_POUND = TEXT_TOKENS.POUND, diff --git a/src/linkify/core/tokens.js b/src/linkify/core/tokens.js index 07106c99..d8dc3dd7 100644 --- a/src/linkify/core/tokens.js +++ b/src/linkify/core/tokens.js @@ -82,10 +82,10 @@ class LOCALHOST extends TextToken {} /** Newline token - @class NL + @class TNL @extends TextToken */ -class NL extends TextToken { +class TNL extends TextToken { constructor() { super('\n'); } } @@ -169,7 +169,7 @@ let text = { COLON, DOT, LOCALHOST, - NL, + TNL, NUM, PLUS, POUND, @@ -319,7 +319,7 @@ class TEXT extends MultiToken { @class NL @extends MultiToken */ -class NL extends MultiToken { +class MNL extends MultiToken { constructor(value) { super(value); this.type = 'nl'; @@ -399,7 +399,7 @@ class URL extends MultiToken { let multi = { Base: MultiToken, EMAIL, - NL, + MNL, TEXT, URL }; From e815449e3166df588e88e7e0f208ba9b45365022 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 1 Mar 2015 16:47:58 -0500 Subject: [PATCH 29/67] Token fixes, test debugging for linkify-element jsdom woes here unfortunately --- .gitignore | 2 ++ package.json | 4 ++-- src/linkify-element.js | 2 +- src/linkify/core/parser.js | 12 +++++------- src/linkify/core/tokens.js | 4 ++-- src/linkify/utils/options.js | 3 ++- test/spec/linkify-element-test.js | 23 +++++++++++++++++------ test/spec/linkify/core/parser-test.js | 2 +- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 70dcf3a5..7366ab44 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ node_modules build dist lib + +linkify.js diff --git a/package.json b/package.json index c3e8acc5..45946714 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,9 @@ "gulp-uglify": "^1.1.0", "gulp-wrap": "^0.11.0", "jquery": "2.1.x", - "jsdom": "^4.0.1", + "jsdom": "^3.0.0", "jshint-stylish": "1.0.x", - "karma": "^0.12.31", + "karma": "^0.12.32", "karma-browserify": "^4.0.0", "karma-chrome-launcher": "0.1.x", "karma-mocha": "0.1.x", diff --git a/src/linkify-element.js b/src/linkify-element.js index 206762c1..576820d6 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -62,7 +62,7 @@ function tokensToNodes(tokens, opts, doc) { // Requires document.createElement function linkifyElement(element, opts, doc) { - + debugger; // Can the element be linkified? if (!element || typeof element !== 'object' || element.nodeType !== HTML_NODE) { throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`); diff --git a/src/linkify/core/parser.js b/src/linkify/core/parser.js index bbb5f3d4..8e061c20 100644 --- a/src/linkify/core/parser.js +++ b/src/linkify/core/parser.js @@ -24,7 +24,7 @@ TT_AT = TEXT_TOKENS.AT, TT_COLON = TEXT_TOKENS.COLON, TT_DOT = TEXT_TOKENS.DOT, TT_LOCALHOST = TEXT_TOKENS.LOCALHOST, -TT_NL = TEXT_TOKENS.TNL, +TT_NL = TEXT_TOKENS.NL, TT_NUM = TEXT_TOKENS.NUM, TT_PLUS = TEXT_TOKENS.PLUS, TT_POUND = TEXT_TOKENS.POUND, @@ -280,9 +280,7 @@ let run = function (tokens) { return multis; }; -export default { - State, - TOKENS: MULTI_TOKENS, - run, - start: S_START -}; +let +TOKENS = MULTI_TOKENS, +start = S_START; +export { State, TOKENS, run, start}; diff --git a/src/linkify/core/tokens.js b/src/linkify/core/tokens.js index d8dc3dd7..8e7ecb45 100644 --- a/src/linkify/core/tokens.js +++ b/src/linkify/core/tokens.js @@ -169,7 +169,7 @@ let text = { COLON, DOT, LOCALHOST, - TNL, + NL: TNL, NUM, PLUS, POUND, @@ -399,7 +399,7 @@ class URL extends MultiToken { let multi = { Base: MultiToken, EMAIL, - MNL, + NL: MNL, TEXT, URL }; diff --git a/src/linkify/utils/options.js b/src/linkify/utils/options.js index 5f18c584..c2a1f110 100644 --- a/src/linkify/utils/options.js +++ b/src/linkify/utils/options.js @@ -6,7 +6,8 @@ function typeToTarget(type) { return type === 'url' ? '_blank' : null; } -function normalize(opts={}) { +function normalize(opts) { + opts = opts || {}; let newLine = opts.newLine || false; // deprecated return { attributes: opts.linkAttributes || null, diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 0b3af8d3..38516d28 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -2,7 +2,7 @@ var doc, testContainer, jsdom = require('jsdom'), -linkifyElement = require('../../lib/linkify-element'); +linkifyElement = require('../../lib/linkify-element')['default']; try { doc = document; @@ -22,10 +22,20 @@ describe('linkify-element', function () { testContainer = doc.createElement('div'); testContainer.id = 'linkify-test-container'; - testContainer.innerHtml = - 'Hello here are some links to ftp://awesome.com/?where=this ' + - 'and localhost:8080, pretty neat right?' + - '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

'; + testContainer.appendChild( + doc.createTextNode( + 'Hello here are some links to ftp://awesome.com/?where=this ' + + 'and localhost:8080, pretty neat right?' + ) + ); + + var p = doc.createElement('p'); + p.appendChild( + doc.createTextNode( + 'Here\'s a nested github.com/SoapBox/linkifyjs paragraph' + ) + ); + testContainer.appendChild(p); doc.body.appendChild(testContainer); done(); @@ -45,8 +55,9 @@ describe('linkify-element', function () { }); it('Works with default options', function () { - testContainer.should.be.type('object'); + testContainer.should.be.a('object'); linkifyElement(testContainer, null, doc); + console.log(testContainer.innerHtml); (testContainer).should.be.okay; }); diff --git a/test/spec/linkify/core/parser-test.js b/test/spec/linkify/core/parser-test.js index 6d2f1d98..3f57c3f9 100644 --- a/test/spec/linkify/core/parser-test.js +++ b/test/spec/linkify/core/parser-test.js @@ -7,7 +7,7 @@ var TEXT = MULTI_TOKENS.TEXT, URL = MULTI_TOKENS.URL, EMAIL = MULTI_TOKENS.EMAIL; -// MNL = MULTI_TOKENS.NL; // new line +// NL = MULTI_TOKENS.NL; // new line /** [0] - Original text to parse (should tokenize first) From a4873dd2fea754659ccbcd487a7156f300bca6e3 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 8 Mar 2015 19:49:53 -0400 Subject: [PATCH 30/67] Test fixes for linkify-element Works in both Node and browser environments! --- src/linkify-element.js | 19 +++++++------ test/spec/linkify-element-test.js | 44 ++++++++++++++++++------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/linkify-element.js b/src/linkify-element.js index 576820d6..b91ff89d 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -61,8 +61,8 @@ function tokensToNodes(tokens, opts, doc) { } // Requires document.createElement -function linkifyElement(element, opts, doc) { - debugger; +function linkifyElementHelper(element, opts, doc) { + // Can the element be linkified? if (!element || typeof element !== 'object' || element.nodeType !== HTML_NODE) { throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`); @@ -82,14 +82,14 @@ function linkifyElement(element, opts, doc) { switch (childElement.nodeType) { case HTML_NODE: - children.push(linkifyElement(childElement, opts, doc)); + children.push(linkifyElementHelper(childElement, opts, doc)); break; case TXT_NODE: let str = childElement.nodeValue, tokens = tokenize(str); - children = children.concat(tokensToNodes(tokens, opts, doc)); + children.push(...tokensToNodes(tokens, opts, doc)); break; @@ -109,11 +109,10 @@ function linkifyElement(element, opts, doc) { element.appendChild(children[i]); } - return linkifyElement; + return element; } -export { linkifyElement }; -export default function _linkifyElement(element, opts, doc=null) { +export default function linkifyElement(element, opts, doc=null) { doc = doc || window && window.document || global && global.document; @@ -126,5 +125,9 @@ export default function _linkifyElement(element, opts, doc=null) { } opts = options.normalize(opts); - return linkifyElement(element, opts, doc); + return linkifyElementHelper(element, opts, doc); } + +// Maintain reference to the recursive helper to save some option-normalization +// cycles +linkifyElement.helper = linkifyElementHelper; diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 38516d28..7a1ba260 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -2,7 +2,7 @@ var doc, testContainer, jsdom = require('jsdom'), -linkifyElement = require('../../lib/linkify-element')['default']; +linkifyElement = require('../../lib/linkify-element'); try { doc = document; @@ -12,30 +12,31 @@ try { describe('linkify-element', function () { + var testHtml, testHtmlLinkified; /** Set up the JavaScript document and the element for it This code allows testing on Node.js and on Browser environments */ before(function (done) { + testHtml = + 'Hello here are some links to ftp://awesome.com/?where=this and '+ + 'localhost:8080, pretty neat right? '+ + '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

'; + + testHtmlLinkified = + 'Hello here are some links to ftp://awesome.com/?where=this and ' + + 'localhost:8080, pretty neat right?

Here\'s a nested ' + + 'github.com/SoapBox/linkifyjs paragraph

'; + function onDoc(doc) { testContainer = doc.createElement('div'); testContainer.id = 'linkify-test-container'; - - testContainer.appendChild( - doc.createTextNode( - 'Hello here are some links to ftp://awesome.com/?where=this ' + - 'and localhost:8080, pretty neat right?' - ) - ); - - var p = doc.createElement('p'); - p.appendChild( - doc.createTextNode( - 'Here\'s a nested github.com/SoapBox/linkifyjs paragraph' - ) - ); - testContainer.appendChild(p); + testContainer.innerHTML = testHtml; doc.body.appendChild(testContainer); done(); @@ -54,11 +55,16 @@ describe('linkify-element', function () { ); }); + it('Has a helper function', function () { + (linkifyElement.helper).should.be.a('function'); + }); + it('Works with default options', function () { - testContainer.should.be.a('object'); - linkifyElement(testContainer, null, doc); - console.log(testContainer.innerHtml); (testContainer).should.be.okay; + testContainer.should.be.a('object'); + var result = linkifyElement(testContainer, null, doc); + result.should.eql(testContainer); // should return the same element + testContainer.innerHTML.should.eql(testHtmlLinkified); }); }); From 17053f63dbddae8199caa28683af4da22c5cdb9f Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 8 Mar 2015 19:50:54 -0400 Subject: [PATCH 31/67] Build tool enhancements Including TLDs macro for better file size/maintainability. Also fixed unnecessary closure compiler file output. --- .gitignore | 2 - gulpfile.js | 20 +- package.json | 1 + src/linkify/core/scanner.js | 4 +- src/linkify/core/tlds.js | 13 - tlds.js | 695 ++++++++++++++++++++++++++++++++++++ 6 files changed, 714 insertions(+), 21 deletions(-) delete mode 100644 src/linkify/core/tlds.js create mode 100644 tlds.js diff --git a/.gitignore b/.gitignore index 7366ab44..70dcf3a5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,5 +40,3 @@ node_modules build dist lib - -linkify.js diff --git a/gulpfile.js b/gulpfile.js index 7ca326c9..8a84a2d1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,8 @@ amdOptimize = require('amd-optimize'), glob = require('glob'), karma = require('karma').server, path = require('path'), -stylish = require('jshint-stylish'); +stylish = require('jshint-stylish'), +tlds = require('./tlds'); var // Gulp plugins concat = require('gulp-concat'), @@ -11,6 +12,7 @@ closureCompiler = require('gulp-closure-compiler'), jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), rename = require('gulp-rename'), +replace = require('gulp-replace'), babel = require('gulp-babel'), // formerly 6to5 uglify = require('gulp-uglify'), wrap = require('gulp-wrap'); @@ -29,12 +31,15 @@ var babelformat = { } }; +var tldsReplaceStr = '"' + tlds.join('|') + '".split("|")'; + /** ES6 ~> babel (with CJS Node Modules) This populates the `lib` folder, allows usage with Node.js */ gulp.task('babel', function () { return gulp.src(paths.src) + .pipe(replace('__TLDS__', tldsReplaceStr)) .pipe(babel({format: babelformat})) .pipe(gulp.dest('lib')); }); @@ -45,6 +50,7 @@ gulp.task('babel', function () { gulp.task('babel-amd', function () { gulp.src(paths.src) + .pipe(replace('__TLDS__', tldsReplaceStr)) .pipe(babel({ modules: 'amd', moduleIds: true, @@ -63,10 +69,14 @@ gulp.task('babel-amd', function () { // Closure compiler is used here since it can correctly concatenate CJS modules gulp.task('build-core', function () { - gulp.src(['lib/linkify/core/*.js', 'lib/linkify/utils/*.js', 'lib/linkify.js']) + gulp.src([ + 'lib/linkify/core/*.js', + 'lib/linkify/utils/*.js', + 'lib/linkify.js' + ]) .pipe(closureCompiler({ compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', - fileName: 'linkify.js', + fileName: 'build/linkify.js', compilerFlags: { process_common_js_modules: null, common_js_entry_module: 'lib/linkify', @@ -105,7 +115,7 @@ gulp.task('build-interfaces', function () { files.amd.push('build/amd/linkify-' + interface[i] + '.js'); } - // The last interface is the name of the dependency + // The last dependency is the name of the interface interface = interface.pop(); } else { @@ -171,8 +181,8 @@ gulp.task('build-plugins', function () { // AMD Browser plugins for (i = 0; i < plugins.length; i++) { plugin = plugins[i]; - } + }); // Build steps diff --git a/package.json b/package.json index 45946714..3619df0b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "gulp-jshint": "1.9.x", "gulp-mocha": "2.0.x", "gulp-rename": "1.2.x", + "gulp-replace": "^0.5.3", "gulp-uglify": "^1.1.0", "gulp-wrap": "^0.11.0", "jquery": "2.1.x", diff --git a/src/linkify/core/scanner.js b/src/linkify/core/scanner.js index 57a8ec95..cbfea765 100644 --- a/src/linkify/core/scanner.js +++ b/src/linkify/core/scanner.js @@ -1,3 +1,4 @@ +/* global __TLDS__ */ /** The scanner provides an interface that takes a string of text as input, and outputs an array of tokens instances that can be used for easy URL parsing. @@ -9,7 +10,8 @@ import {text as TOKENS} from './tokens'; import {CharacterState as State, stateify} from './state'; -import tlds from './tlds'; + +const tlds = __TLDS__; // macro, see gulpfile.js const REGEXP_NUM = /[0-9]/, diff --git a/src/linkify/core/tlds.js b/src/linkify/core/tlds.js deleted file mode 100644 index 3161e08f..00000000 --- a/src/linkify/core/tlds.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - NOTICE: Please ensure that these strings are sorted in alphabetical order - TODO: Divide into essential and complete builds -*/ - -// http://www.seobythesea.com/2006/01/googles-most-popular-and-least-popular-top-level-domains/ -// .co and .io have also been added to the list -// export var essential = 'au|ca|ch|co|com|de|edu|es|fr|gov|it|jp|mil|net|nl|no|org|ru|se|uk|us'.split('|'); - -// http://data.iana.org/TLD/tlds-alpha-by-domain.txt -export default ( - 'abogado|ac|academy|accountants|active|actor|ad|adult|ae|aero|af|ag|agency|ai|airforce|al|allfinanz|alsace|am|an|android|ao|aq|aquarelle|ar|archi|army|arpa|as|asia|associates|at|attorney|au|auction|audio|autos|aw|ax|axa|az|ba|band|bar|bargains|bayern|bb|bd|be|beer|berlin|best|bf|bg|bh|bi|bid|bike|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bmw|bn|bnpparibas|bo|boo|boutique|br|brussels|bs|bt|budapest|build|builders|business|buzz|bv|bw|by|bz|bzh|ca|cab|cal|camera|camp|cancerresearch|capetown|capital|caravan|cards|care|career|careers|casa|cash|cat|catering|cc|cd|center|ceo|cern|cf|cg|ch|channel|cheap|christmas|chrome|church|ci|citic|city|ck|cl|claims|cleaning|click|clinic|clothing|club|cm|cn|co|coach|codes|coffee|college|cologne|com|community|company|computer|condos|construction|consulting|contractors|cooking|cool|coop|country|cr|credit|creditcard|cricket|crs|cruises|cu|cuisinella|cv|cw|cx|cy|cymru|cz|dad|dance|dating|day|de|deals|degree|delivery|democrat|dental|dentist|desi|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|domains|durban|dvag|dz|eat|ec|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|equipment|er|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|fail|farm|fashion|feedback|fi|finance|financial|firmdale|fish|fishing|fitness|fj|fk|flights|florist|flsmidth|fly|fm|fo|foo|forsale|foundation|fr|frl|frogans|fund|furniture|futbol|ga|gal|gallery|gb|gbiz|gd|ge|gent|gf|gg|gh|gi|gift|gifts|gives|gl|glass|gle|global|globo|gm|gmail|gmo|gmx|gn|google|gop|gov|gp|gq|gr|graphics|gratis|green|gripe|gs|gt|gu|guide|guitars|guru|gw|gy|hamburg|haus|healthcare|help|here|hiphop|hiv|hk|hm|hn|holdings|holiday|homes|horse|host|hosting|house|how|hr|ht|hu|ibm|id|ie|il|im|immo|immobilien|in|industries|info|ing|ink|institute|insure|int|international|investments|io|iq|ir|irish|is|it|je|jetzt|jm|jo|jobs|joburg|jp|juegos|kaufen|ke|kg|kh|ki|kim|kitchen|kiwi|km|kn|koeln|kp|kr|krd|kred|kw|ky|kz|la|lacaixa|land|latrobe|lawyer|lb|lc|lds|lease|legal|lgbt|li|life|lighting|limited|limo|link|lk|loans|london|lotto|lr|ls|lt|ltda|lu|luxe|luxury|lv|ly|ma|madrid|maison|management|mango|market|marketing|mc|md|me|media|meet|melbourne|meme|memorial|menu|mg|mh|miami|mil|mini|mk|ml|mm|mn|mo|mobi|moda|moe|monash|money|mormon|mortgage|moscow|motorcycles|mov|mp|mq|mr|ms|mt|mu|museum|mv|mw|mx|my|mz|na|nagoya|name|navy|nc|ne|net|network|neustar|new|nexus|nf|ng|ngo|nhk|ni|ninja|nl|no|np|nr|nra|nrw|nu|nyc|nz|okinawa|om|ong|onl|ooo|org|organic|otsuka|ovh|pa|paris|partners|parts|party|pe|pf|pg|ph|pharmacy|photo|photography|photos|physio|pics|pictures|pink|pizza|pk|pl|place|plumbing|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|properties|property|ps|pt|pub|pw|py|qa|qpon|quebec|re|realtor|recipes|red|rehab|reise|reisen|reit|ren|rentals|repair|report|republican|rest|restaurant|reviews|rich|rio|rip|ro|rocks|rodeo|rs|rsvp|ru|ruhr|rw|ryukyu|sa|saarland|sarl|sb|sc|sca|scb|schmidt|schule|science|scot|sd|se|services|sexy|sg|sh|shiksha|shoes|si|singles|sj|sk|sl|sm|sn|so|social|software|sohu|solar|solutions|soy|space|spiegel|sr|st|su|supplies|supply|support|surf|surgery|suzuki|sv|sx|sy|sydney|systems|sz|taipei|tatar|tattoo|tax|tc|td|technology|tel|tf|tg|th|tienda|tips|tirol|tj|tk|tl|tm|tn|to|today|tokyo|tools|top|town|toys|tp|tr|trade|training|travel|trust|tt|tui|tv|tw|tz|ua|ug|uk|university|uno|uol|us|uy|uz|va|vacations|vc|ve|vegas|ventures|versicherung|vet|vg|vi|viajes|villas|vision|vlaanderen|vn|vodka|vote|voting|voto|voyage|vu|wales|wang|watch|webcam|website|wed|wedding|wf|whoswho|wien|wiki|williamhill|wme|work|works|world|ws|wtc|wtf|xxx|xyz|yachts|yandex|ye|yoga|yokohama|youtube|yt|za|zip|zm|zone|zw' -).split('|'); diff --git a/tlds.js b/tlds.js new file mode 100644 index 00000000..5c25b7c8 --- /dev/null +++ b/tlds.js @@ -0,0 +1,695 @@ +// To be updated with the values in this list +// http://data.iana.org/TLD/tlds-alpha-by-domain.txt +module.exports = [ + 'abogado', + 'ac', + 'academy', + 'accountants', + 'active', + 'actor', + 'ad', + 'adult', + 'ae', + 'aero', + 'af', + 'ag', + 'agency', + 'ai', + 'airforce', + 'al', + 'allfinanz', + 'alsace', + 'am', + 'an', + 'android', + 'ao', + 'aq', + 'aquarelle', + 'ar', + 'archi', + 'army', + 'arpa', + 'as', + 'asia', + 'associates', + 'at', + 'attorney', + 'au', + 'auction', + 'audio', + 'autos', + 'aw', + 'ax', + 'axa', + 'az', + 'ba', + 'band', + 'bar', + 'bargains', + 'bayern', + 'bb', + 'bd', + 'be', + 'beer', + 'berlin', + 'best', + 'bf', + 'bg', + 'bh', + 'bi', + 'bid', + 'bike', + 'bio', + 'biz', + 'bj', + 'black', + 'blackfriday', + 'bloomberg', + 'blue', + 'bm', + 'bmw', + 'bn', + 'bnpparibas', + 'bo', + 'boo', + 'boutique', + 'br', + 'brussels', + 'bs', + 'bt', + 'budapest', + 'build', + 'builders', + 'business', + 'buzz', + 'bv', + 'bw', + 'by', + 'bz', + 'bzh', + 'ca', + 'cab', + 'cal', + 'camera', + 'camp', + 'cancerresearch', + 'capetown', + 'capital', + 'caravan', + 'cards', + 'care', + 'career', + 'careers', + 'casa', + 'cash', + 'cat', + 'catering', + 'cc', + 'cd', + 'center', + 'ceo', + 'cern', + 'cf', + 'cg', + 'ch', + 'channel', + 'cheap', + 'christmas', + 'chrome', + 'church', + 'ci', + 'citic', + 'city', + 'ck', + 'cl', + 'claims', + 'cleaning', + 'click', + 'clinic', + 'clothing', + 'club', + 'cm', + 'cn', + 'co', + 'coach', + 'codes', + 'coffee', + 'college', + 'cologne', + 'com', + 'community', + 'company', + 'computer', + 'condos', + 'construction', + 'consulting', + 'contractors', + 'cooking', + 'cool', + 'coop', + 'country', + 'cr', + 'credit', + 'creditcard', + 'cricket', + 'crs', + 'cruises', + 'cu', + 'cuisinella', + 'cv', + 'cw', + 'cx', + 'cy', + 'cymru', + 'cz', + 'dad', + 'dance', + 'dating', + 'day', + 'de', + 'deals', + 'degree', + 'delivery', + 'democrat', + 'dental', + 'dentist', + 'desi', + 'diamonds', + 'diet', + 'digital', + 'direct', + 'directory', + 'discount', + 'dj', + 'dk', + 'dm', + 'dnp', + 'do', + 'domains', + 'durban', + 'dvag', + 'dz', + 'eat', + 'ec', + 'edu', + 'education', + 'ee', + 'eg', + 'email', + 'emerck', + 'energy', + 'engineer', + 'engineering', + 'enterprises', + 'equipment', + 'er', + 'es', + 'esq', + 'estate', + 'et', + 'eu', + 'eurovision', + 'eus', + 'events', + 'everbank', + 'exchange', + 'expert', + 'exposed', + 'fail', + 'farm', + 'fashion', + 'feedback', + 'fi', + 'finance', + 'financial', + 'firmdale', + 'fish', + 'fishing', + 'fitness', + 'fj', + 'fk', + 'flights', + 'florist', + 'flsmidth', + 'fly', + 'fm', + 'fo', + 'foo', + 'forsale', + 'foundation', + 'fr', + 'frl', + 'frogans', + 'fund', + 'furniture', + 'futbol', + 'ga', + 'gal', + 'gallery', + 'gb', + 'gbiz', + 'gd', + 'ge', + 'gent', + 'gf', + 'gg', + 'gh', + 'gi', + 'gift', + 'gifts', + 'gives', + 'gl', + 'glass', + 'gle', + 'global', + 'globo', + 'gm', + 'gmail', + 'gmo', + 'gmx', + 'gn', + 'google', + 'gop', + 'gov', + 'gp', + 'gq', + 'gr', + 'graphics', + 'gratis', + 'green', + 'gripe', + 'gs', + 'gt', + 'gu', + 'guide', + 'guitars', + 'guru', + 'gw', + 'gy', + 'hamburg', + 'haus', + 'healthcare', + 'help', + 'here', + 'hiphop', + 'hiv', + 'hk', + 'hm', + 'hn', + 'holdings', + 'holiday', + 'homes', + 'horse', + 'host', + 'hosting', + 'house', + 'how', + 'hr', + 'ht', + 'hu', + 'ibm', + 'id', + 'ie', + 'il', + 'im', + 'immo', + 'immobilien', + 'in', + 'industries', + 'info', + 'ing', + 'ink', + 'institute', + 'insure', + 'int', + 'international', + 'investments', + 'io', + 'iq', + 'ir', + 'irish', + 'is', + 'it', + 'je', + 'jetzt', + 'jm', + 'jo', + 'jobs', + 'joburg', + 'jp', + 'juegos', + 'kaufen', + 'ke', + 'kg', + 'kh', + 'ki', + 'kim', + 'kitchen', + 'kiwi', + 'km', + 'kn', + 'koeln', + 'kp', + 'kr', + 'krd', + 'kred', + 'kw', + 'ky', + 'kz', + 'la', + 'lacaixa', + 'land', + 'latrobe', + 'lawyer', + 'lb', + 'lc', + 'lds', + 'lease', + 'legal', + 'lgbt', + 'li', + 'life', + 'lighting', + 'limited', + 'limo', + 'link', + 'lk', + 'loans', + 'london', + 'lotto', + 'lr', + 'ls', + 'lt', + 'ltda', + 'lu', + 'luxe', + 'luxury', + 'lv', + 'ly', + 'ma', + 'madrid', + 'maison', + 'management', + 'mango', + 'market', + 'marketing', + 'mc', + 'md', + 'me', + 'media', + 'meet', + 'melbourne', + 'meme', + 'memorial', + 'menu', + 'mg', + 'mh', + 'miami', + 'mil', + 'mini', + 'mk', + 'ml', + 'mm', + 'mn', + 'mo', + 'mobi', + 'moda', + 'moe', + 'monash', + 'money', + 'mormon', + 'mortgage', + 'moscow', + 'motorcycles', + 'mov', + 'mp', + 'mq', + 'mr', + 'ms', + 'mt', + 'mu', + 'museum', + 'mv', + 'mw', + 'mx', + 'my', + 'mz', + 'na', + 'nagoya', + 'name', + 'navy', + 'nc', + 'ne', + 'net', + 'network', + 'neustar', + 'new', + 'nexus', + 'nf', + 'ng', + 'ngo', + 'nhk', + 'ni', + 'ninja', + 'nl', + 'no', + 'np', + 'nr', + 'nra', + 'nrw', + 'nu', + 'nyc', + 'nz', + 'okinawa', + 'om', + 'ong', + 'onl', + 'ooo', + 'org', + 'organic', + 'otsuka', + 'ovh', + 'pa', + 'paris', + 'partners', + 'parts', + 'party', + 'pe', + 'pf', + 'pg', + 'ph', + 'pharmacy', + 'photo', + 'photography', + 'photos', + 'physio', + 'pics', + 'pictures', + 'pink', + 'pizza', + 'pk', + 'pl', + 'place', + 'plumbing', + 'pm', + 'pn', + 'pohl', + 'poker', + 'porn', + 'post', + 'pr', + 'praxi', + 'press', + 'pro', + 'prod', + 'productions', + 'prof', + 'properties', + 'property', + 'ps', + 'pt', + 'pub', + 'pw', + 'py', + 'qa', + 'qpon', + 'quebec', + 're', + 'realtor', + 'recipes', + 'red', + 'rehab', + 'reise', + 'reisen', + 'reit', + 'ren', + 'rentals', + 'repair', + 'report', + 'republican', + 'rest', + 'restaurant', + 'reviews', + 'rich', + 'rio', + 'rip', + 'ro', + 'rocks', + 'rodeo', + 'rs', + 'rsvp', + 'ru', + 'ruhr', + 'rw', + 'ryukyu', + 'sa', + 'saarland', + 'sarl', + 'sb', + 'sc', + 'sca', + 'scb', + 'schmidt', + 'schule', + 'science', + 'scot', + 'sd', + 'se', + 'services', + 'sexy', + 'sg', + 'sh', + 'shiksha', + 'shoes', + 'si', + 'singles', + 'sj', + 'sk', + 'sl', + 'sm', + 'sn', + 'so', + 'social', + 'software', + 'sohu', + 'solar', + 'solutions', + 'soy', + 'space', + 'spiegel', + 'sr', + 'st', + 'su', + 'supplies', + 'supply', + 'support', + 'surf', + 'surgery', + 'suzuki', + 'sv', + 'sx', + 'sy', + 'sydney', + 'systems', + 'sz', + 'taipei', + 'tatar', + 'tattoo', + 'tax', + 'tc', + 'td', + 'technology', + 'tel', + 'tf', + 'tg', + 'th', + 'tienda', + 'tips', + 'tirol', + 'tj', + 'tk', + 'tl', + 'tm', + 'tn', + 'to', + 'today', + 'tokyo', + 'tools', + 'top', + 'town', + 'toys', + 'tp', + 'tr', + 'trade', + 'training', + 'travel', + 'trust', + 'tt', + 'tui', + 'tv', + 'tw', + 'tz', + 'ua', + 'ug', + 'uk', + 'university', + 'uno', + 'uol', + 'us', + 'uy', + 'uz', + 'va', + 'vacations', + 'vc', + 've', + 'vegas', + 'ventures', + 'versicherung', + 'vet', + 'vg', + 'vi', + 'viajes', + 'villas', + 'vision', + 'vlaanderen', + 'vn', + 'vodka', + 'vote', + 'voting', + 'voto', + 'voyage', + 'vu', + 'wales', + 'wang', + 'watch', + 'webcam', + 'website', + 'wed', + 'wedding', + 'wf', + 'whoswho', + 'wien', + 'wiki', + 'williamhill', + 'wme', + 'work', + 'works', + 'world', + 'ws', + 'wtc', + 'wtf', + 'xxx', + 'xyz', + 'yachts', + 'yandex', + 'ye', + 'yoga', + 'yokohama', + 'youtube', + 'yt', + 'za', + 'zip', + 'zm', + 'zone', + 'zw', +]; From d83762e0ec6570338f35c450b4a73c8b77b75461 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 8 Mar 2015 21:24:38 -0400 Subject: [PATCH 32/67] jQuery interface fixes and tests jQuery is working in both Node and Browser environments! --- package.json | 3 ++ src/linkify-element.js | 9 ++++- src/linkify-jquery.js | 44 ++++++++++++++++---- test/ci.conf.js | 2 +- test/index.js | 2 +- test/spec/html-options.js | 24 +++++++++++ test/spec/linkify-element-test.js | 32 ++++++--------- test/spec/linkify-jquery-test.js | 67 +++++++++++++++++++++++++++++++ 8 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 test/spec/html-options.js create mode 100644 test/spec/linkify-jquery-test.js diff --git a/package.json b/package.json index 3619df0b..e0eaf424 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,8 @@ "karma-sauce-launcher": "0.2.x", "lodash": "^3.3.1", "mocha": "2.1.x" + }, + "optionalDependencies": { + "jquery": "^2.1.3" } } diff --git a/src/linkify-element.js b/src/linkify-element.js index b91ff89d..e0384038 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -112,9 +112,11 @@ function linkifyElementHelper(element, opts, doc) { return element; } -export default function linkifyElement(element, opts, doc=null) { +function linkifyElement(element, opts, doc=null) { - doc = doc || window && window.document || global && global.document; + try { + doc = doc || window && window.document || global && global.document; + } catch (e) { /* do nothing for now */ } if (!doc) { throw new Error( @@ -131,3 +133,6 @@ export default function linkifyElement(element, opts, doc=null) { // Maintain reference to the recursive helper to save some option-normalization // cycles linkifyElement.helper = linkifyElementHelper; +linkifyElement.normalize = options.normalize; + +export default linkifyElement; diff --git a/src/linkify-jquery.js b/src/linkify-jquery.js index 3d0d2271..21ffa838 100644 --- a/src/linkify-jquery.js +++ b/src/linkify-jquery.js @@ -1,19 +1,46 @@ import jQuery from 'jquery'; -import { linkifyElement } from './linkify-element'; -export default apply; +import linkifyElement from './linkify-element'; + +let doc; + +try { + doc = document; +} catch (e) { + doc = null; +} // Applies the plugin to jQuery function apply($, doc=null) { - function jqLinkify(options) { + $.fn = $.fn || {}; + + try { + doc = doc || window && window.document || global && global.document; + } catch (e) { /* do nothing for now */ } + + if (!doc) { + throw new Error( + 'Cannot find document implementation. ' + + 'If you are in a non-browser environment like Node.js, ' + + 'pass the document implementation as the third argument to linkifyElement.' + ); + } + + if (typeof $.fn.linkify === 'function') { + // Already applied + return; + } + + function jqLinkify(opts) { + opts = linkifyElement.normalize(opts); return this.each(function () { - linkifyElement(this, options, doc); + linkifyElement.helper(this, opts, doc); }); } $.fn.linkify = jqLinkify; - $(window).on('load', function () { + $(doc).ready(function () { $('[data-linkify]').each(function () { let $this = $(this), @@ -38,6 +65,9 @@ function apply($, doc=null) { }); } -if (typeof jQuery !== 'undefined') { - apply(jQuery); +// Apply it right away if possible +if (typeof jQuery !== 'undefined' && doc) { + apply(jQuery, doc); } + +export default apply; diff --git a/test/ci.conf.js b/test/ci.conf.js index ec74d3e1..11dd45e9 100644 --- a/test/ci.conf.js +++ b/test/ci.conf.js @@ -38,7 +38,7 @@ module.exports = function (config) { // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG sauceLabs: { - testName: 'Linkify Browser Tests' + testName: 'Linkify Browser Tests' }, logLevel: config.LOG_WARN, diff --git a/test/index.js b/test/index.js index 33321ca6..d9976e4d 100644 --- a/test/index.js +++ b/test/index.js @@ -3,4 +3,4 @@ require('./init'); global.__base = __dirname.replace(/test$/, 'lib/'); // Require test files -glob.sync('./spec/**/*.js', {cwd: __dirname}).map(require); +glob.sync('./spec/**/*-test.js', {cwd: __dirname}).map(require); diff --git a/test/spec/html-options.js b/test/spec/html-options.js new file mode 100644 index 00000000..cb2951a0 --- /dev/null +++ b/test/spec/html-options.js @@ -0,0 +1,24 @@ +// HTML to use with linkify-element and linkify-jquery +module.exports = { + original: + 'Hello here are some links to ftp://awesome.com/?where=this and '+ + 'localhost:8080, pretty neat right? '+ + '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

', + linkified: + 'Hello here are some links to ftp://awesome.com/?where=this and ' + + 'localhost:8080, pretty neat right?

Here\'s a nested ' + + 'github.com/SoapBox/linkifyjs paragraph

', + linkifiedAlt: + 'Hello here are some links to ftp://awesome.com/?where=this and ' + + 'localhost:8080, pretty neat right?

Here\'s a nested ' + + 'github.com/SoapBox/linkifyjs paragraph

', + altOptions: {} +}; diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 7a1ba260..20d3d185 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -2,7 +2,8 @@ var doc, testContainer, jsdom = require('jsdom'), -linkifyElement = require('../../lib/linkify-element'); +linkifyElement = require('../../lib/linkify-element'), +htmlOptions = require('./html-options'); try { doc = document; @@ -12,31 +13,16 @@ try { describe('linkify-element', function () { - var testHtml, testHtmlLinkified; /** Set up the JavaScript document and the element for it This code allows testing on Node.js and on Browser environments */ before(function (done) { - testHtml = - 'Hello here are some links to ftp://awesome.com/?where=this and '+ - 'localhost:8080, pretty neat right? '+ - '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

'; - - testHtmlLinkified = - 'Hello here are some links to ftp://awesome.com/?where=this and ' + - 'localhost:8080, pretty neat right?

Here\'s a nested ' + - 'github.com/SoapBox/linkifyjs paragraph

'; - function onDoc(doc) { testContainer = doc.createElement('div'); - testContainer.id = 'linkify-test-container'; - testContainer.innerHTML = testHtml; + testContainer.id = 'linkify-element-test-container'; + testContainer.innerHTML = htmlOptions.original; doc.body.appendChild(testContainer); done(); @@ -64,7 +50,15 @@ describe('linkify-element', function () { testContainer.should.be.a('object'); var result = linkifyElement(testContainer, null, doc); result.should.eql(testContainer); // should return the same element - testContainer.innerHTML.should.eql(testHtmlLinkified); + testContainer.innerHTML.should.eql(htmlOptions.linkified); + }); + + it('Works with overriden options', function () { + (testContainer).should.be.okay; + testContainer.should.be.a('object'); + var result = linkifyElement(testContainer, htmlOptions.altOptions, doc); + result.should.eql(testContainer); // should return the same element + testContainer.innerHTML.should.eql(htmlOptions.linkifiedAlt); }); }); diff --git a/test/spec/linkify-jquery-test.js b/test/spec/linkify-jquery-test.js new file mode 100644 index 00000000..cc0b924d --- /dev/null +++ b/test/spec/linkify-jquery-test.js @@ -0,0 +1,67 @@ +/*jshint -W030 */ +var +$, doc, testContainer, +jsdom = require('jsdom'), +applyLinkify = require('../../lib/linkify-jquery'), +htmlOptions = require('./html-options'); + +try { + doc = document; + $ = require('jquery'); // should be available through Browserify +} catch (e) { + doc = null; + $ = null; +} + +describe('linkify-jquery', function () { + + /** + Set up the JavaScript document and the element for it + This code allows testing on Node.js and on Browser environments + */ + before(function (done) { + + function onDoc($, doc) { + + // Add the linkify plugin to jQuery + applyLinkify($, doc); + + testContainer = doc.createElement('div'); + testContainer.id = 'linkify-jquery-test-container'; + testContainer.innerHTML = htmlOptions.original; + + doc.body.appendChild(testContainer); + done(); + } + + if (doc) { return onDoc($, doc); } + // no document element, use a virtual dom to test + + jsdom.env( + 'Linkify Test', + ['http://code.jquery.com/jquery.js'], + function (errors, window) { + if (errors) { throw errors; } + doc = window.document; + $ = window.$; // this is pretty weird + return onDoc(window.$, window.document); + } + ); + }); + + it('Works with default options', function () { + var $container = $('#linkify-jquery-test-container'); + ($container.length).should.be.eql(1); + var result = $container.linkify(); + (result === $container).should.be.true; // should return the same element + $container.html().should.eql(htmlOptions.linkified); + }); + + it('Works with overriden options', function () { + var $container = $('#linkify-jquery-test-container'); + ($container.length).should.be.eql(1); + var result = $container.linkify(htmlOptions.altOptions); + (result === $container).should.be.true; // should return the same element + $container.html().should.eql(htmlOptions.linkifiedAlt); + }); +}); From 3eef0f73209d1cfb5a8194e497defb6845e01d9b Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 8 Mar 2015 21:59:38 -0400 Subject: [PATCH 33/67] Linkify Build fixes Closure compiler woes. --- gulpfile.js | 9 ++++++--- templates/linkify.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 8a84a2d1..bec5c65f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,7 +76,7 @@ gulp.task('build-core', function () { ]) .pipe(closureCompiler({ compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', - fileName: 'build/linkify.js', + fileName: 'build/.closure-output.js', compilerFlags: { process_common_js_modules: null, common_js_entry_module: 'lib/linkify', @@ -85,6 +85,11 @@ gulp.task('build-core', function () { } })) .pipe(wrap({src: 'templates/linkify.js'})) + .pipe(rename(function (path) { + // Required due to closure compiler + path.dirname = '.'; + path.basename = 'linkify'; + })) .pipe(gulp.dest('build')); }); @@ -255,5 +260,3 @@ gulp.task('test-ci', ['karma-ci']); gulp.task('default', ['babel'], function () { gulp.watch(paths.src, ['babel']); }); - - diff --git a/templates/linkify.js b/templates/linkify.js index 39a9370b..bf2e8044 100644 --- a/templates/linkify.js +++ b/templates/linkify.js @@ -2,5 +2,5 @@ "use strict"; // Output from the Closure Compiler <%= contents %> -window.linkify = module$$linkify; +window.linkify = module$lib$linkify; })(); From 83116a654d3a4af187d412e6f8cb9c02800ca8d4 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 13 Mar 2015 20:02:32 -0400 Subject: [PATCH 34/67] Find by type and additional linkify tests The new tests take care of basic linkify functionality (find, tokenize, test, etc.) --- src/linkify-jquery.js | 1 - src/linkify.js | 6 ++- test/spec/linkify-test.js | 80 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 test/spec/linkify-test.js diff --git a/src/linkify-jquery.js b/src/linkify-jquery.js index 21ffa838..e006f0a8 100644 --- a/src/linkify-jquery.js +++ b/src/linkify-jquery.js @@ -60,7 +60,6 @@ function apply($, doc=null) { let $target = target === 'this' ? $this : $this.find(target); $target.linkify(options); - }); }); } diff --git a/src/linkify.js b/src/linkify.js index c9c7d573..51be1d77 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -15,14 +15,16 @@ let tokenize = function (str) { /** Returns a list of linkable items in the given string. */ -let find = function (str) { +let find = function (str, type=null) { let tokens = tokenize(str), filtered = []; for (let i = 0; i < tokens.length; i++) { - if (tokens[i].isLink) { + if (tokens[i].isLink && ( + !type || tokens[i].type === type + )) { filtered.push(tokens[i].toObject()); } } diff --git a/test/spec/linkify-test.js b/test/spec/linkify-test.js new file mode 100644 index 00000000..04a88cf8 --- /dev/null +++ b/test/spec/linkify-test.js @@ -0,0 +1,80 @@ +var +linkify = require('../../lib/linkify'), +MultiToken = linkify.parser.TOKENS.Base; + +var tokensTest = [ + 'The string is the URL https://github.com/ but www.gihub.com/search?utf8=✓ works too with the email test@example.com the end.', + ['text', 'url', 'text', 'url', 'text', 'email', 'text'], + [{ + type: 'url', + value: 'https://github.com/', + href: 'https://github.com/' + }, { + type: 'url', + value: 'www.gihub.com/search?utf8=✓', + href: 'http://www.gihub.com/search?utf8=✓' + }, { + type: 'email', + value: 'test@example.com', + href: 'mailto:test@example.com' + }], +]; + +describe('linkify', function () { + it('Has all required methods and properties', function () { + + // Functions + linkify.tokenize.should.be.a('function'); + linkify.tokenize.length.should.be.eql(1); + linkify.find.should.be.a('function'); + linkify.find.length.should.be.eql(1); // type is optional + linkify.test.should.be.a('function'); + linkify.test.length.should.be.eql(1); // type is optional + + // Properties + linkify.options.should.be.a('object'); + linkify.parser.should.be.a('object'); + linkify.scanner.should.be.a('object'); + + }); +}); + +describe('linkify.tokenize', function () { +}); + +describe('linkify.find', function () { + +}); + +describe('linkify.test', function () { + /* + For each element, + + * [0] is the input string + * [1] is the expected return value + * [2] (optional) the type of link to look for + */ + var tests = [ + ['Herp derp', false], + ['Herp derp', false, 'email'], + ['Herp derp', false, 'asdf'], + ['https://google.com/?q=yey', true], + ['https://google.com/?q=yey', true, 'url'], + ['https://google.com/?q=yey', false, 'email'], + ['test+4@uwaterloo.ca', true], + ['test+4@uwaterloo.ca', false, 'url'], + ['test+4@uwaterloo.ca', true, 'email'], + ['t.co', true], + ['t.co g.co', false], // can only be one + ['test@g.co t.co', false] // can only be one + ]; + + it('Correctly tests each example string', function () { + var test; + for (var i = 0; i < tests.length; i++) { + test = tests[i]; + linkify.test(test[0], test[2]).should.be.eql(test[1]); + } + }); + +}); From 36d5fde0bd01b8ac49c745132172ec7fe6926baa Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 18:39:57 -0400 Subject: [PATCH 35/67] Code coverage Via Istanbul. Includes better code coverage for linkify and element interfaces. --- .jshintrc | 9 +++-- gulpfile.js | 28 +++++++++++--- package.json | 2 + src/linkify-element.js | 4 +- src/linkify/core/scanner.js | 1 - src/linkify/core/tokens.js | 4 +- templates/linkify-element.js | 2 +- test/spec/html-options.js | 12 ++++-- test/spec/linkify-element-test.js | 6 ++- test/spec/linkify-jquery-test.js | 26 ++++++++++++- test/spec/linkify/core/tokens/multi-test.js | 42 ++++++++++++++++++++- 11 files changed, 112 insertions(+), 24 deletions(-) diff --git a/.jshintrc b/.jshintrc index a411bb8d..4ae6cedf 100644 --- a/.jshintrc +++ b/.jshintrc @@ -4,11 +4,12 @@ "node": true, "globals": { "__base": false, - "describe": false, - "it": false, + "__TLDS__": false, + "after": false, + "afterEach": false, "before": false, "beforeEach": false, - "after": false, - "afterEach": false + "describe": false, + "it": false } } diff --git a/gulpfile.js b/gulpfile.js index bec5c65f..e7eb484b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,6 +9,7 @@ tlds = require('./tlds'); var // Gulp plugins concat = require('gulp-concat'), closureCompiler = require('gulp-closure-compiler'), +istanbul = require('gulp-istanbul'), jshint = require('gulp-jshint'), mocha = require('gulp-mocha'), rename = require('gulp-rename'), @@ -19,6 +20,12 @@ wrap = require('gulp-wrap'); var paths = { src: 'src/**/*.js', + lib: 'lib/**/*.js', + libCore: [ + 'lib/linkify/core/*.js', + 'lib/linkify/utils/*.js', + 'lib/linkify.js' + ], amd: 'build/amd/**/*.js', test: 'test/index.js', spec: 'test/spec/**.js' @@ -69,11 +76,7 @@ gulp.task('babel-amd', function () { // Closure compiler is used here since it can correctly concatenate CJS modules gulp.task('build-core', function () { - gulp.src([ - 'lib/linkify/core/*.js', - 'lib/linkify/utils/*.js', - 'lib/linkify.js' - ]) + gulp.src(paths.libCore) .pipe(closureCompiler({ compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', fileName: 'build/.closure-output.js', @@ -210,6 +213,21 @@ gulp.task('mocha', function () { .pipe(mocha()); }); +/** + Code coverage reort for mocha tests +*/ +gulp.task('coverage', function (cb) { + gulp.src(paths.lib) + .pipe(istanbul()) // Covering files + .pipe(istanbul.hookRequire()) // Force `require` to return covered files + .on('finish', function () { + gulp.src(paths.test, {read: false}) + .pipe(mocha()) + .pipe(istanbul.writeReports()) // Creating the reports after tests runned + .on('end', cb); + }); +}); + gulp.task('karma', function () { return karma.start({ configFile: __dirname + '/test/dev.conf.js', diff --git a/package.json b/package.json index e0eaf424..861b18d4 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "amd-optimize": "0.4.x", "chai": "^2.1.0", "closure-compiler": "0.2.x", + "coveralls": "^2.11.2", "glob": "^4.4.1", "gulp": "3.8.x", "gulp-babel": "^4.0.0", "gulp-closure-compiler": "0.2.x", "gulp-concat": "^2.5.2", + "gulp-istanbul": "^0.6.0", "gulp-jshint": "1.9.x", "gulp-mocha": "2.0.x", "gulp-rename": "1.2.x", diff --git a/src/linkify-element.js b/src/linkify-element.js index e0384038..bcf103b5 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -4,9 +4,7 @@ import {tokenize, options} from './linkify'; -let -HTML_NODE = 1, -TXT_NODE = 3; +let HTML_NODE = 1, TXT_NODE = 3; /** Given an array of MultiTokens, return an array of Nodes that are either diff --git a/src/linkify/core/scanner.js b/src/linkify/core/scanner.js index cbfea765..9091d7bf 100644 --- a/src/linkify/core/scanner.js +++ b/src/linkify/core/scanner.js @@ -1,4 +1,3 @@ -/* global __TLDS__ */ /** The scanner provides an interface that takes a string of text as input, and outputs an array of tokens instances that can be used for easy URL parsing. diff --git a/src/linkify/core/tokens.js b/src/linkify/core/tokens.js index 8e7ecb45..960d8408 100644 --- a/src/linkify/core/tokens.js +++ b/src/linkify/core/tokens.js @@ -315,8 +315,8 @@ class TEXT extends MultiToken { } /** - Represents a line break - @class NL + Multi-linebreak token - represents a line break + @class MNL @extends MultiToken */ class MNL extends MultiToken { diff --git a/templates/linkify-element.js b/templates/linkify-element.js index 1c9dce4a..22e67905 100644 --- a/templates/linkify-element.js +++ b/templates/linkify-element.js @@ -2,5 +2,5 @@ "use strict"; var tokenize = linkify.tokenize, options = linkify.options; <%= contents %> -window.linkifyElement = exec; +window.linkifyElement = linkifyElement; })(window.linkify); diff --git a/test/spec/html-options.js b/test/spec/html-options.js index cb2951a0..f470ec18 100644 --- a/test/spec/html-options.js +++ b/test/spec/html-options.js @@ -15,10 +15,14 @@ module.exports = { linkifiedAlt: 'Hello here are some links to ftp://awesome.com/?where=this and ' + + 'target="_blank" rel="nofollow">ftp://awesome.com/?where=this and ' + 'localhost:8080, pretty neat right?

Here\'s a nested ' + 'github.com/SoapBox/linkifyjs paragraph

', - altOptions: {} + 'target="_blank" rel="nofollow">github.com/SoapBox/linkifyjs paragraph

', + altOptions: { + linkAttributes: { + rel: 'nofollow' + } + } }; diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 20d3d185..080c6225 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -22,7 +22,6 @@ describe('linkify-element', function () { function onDoc(doc) { testContainer = doc.createElement('div'); testContainer.id = 'linkify-element-test-container'; - testContainer.innerHTML = htmlOptions.original; doc.body.appendChild(testContainer); done(); @@ -41,6 +40,11 @@ describe('linkify-element', function () { ); }); + beforeEach(function () { + // Make sure we start out with a fresh DOM every time + testContainer.innerHTML = htmlOptions.original; + }); + it('Has a helper function', function () { (linkifyElement.helper).should.be.a('function'); }); diff --git a/test/spec/linkify-jquery-test.js b/test/spec/linkify-jquery-test.js index cc0b924d..f9edd0a3 100644 --- a/test/spec/linkify-jquery-test.js +++ b/test/spec/linkify-jquery-test.js @@ -28,7 +28,6 @@ describe('linkify-jquery', function () { testContainer = doc.createElement('div'); testContainer.id = 'linkify-jquery-test-container'; - testContainer.innerHTML = htmlOptions.original; doc.body.appendChild(testContainer); done(); @@ -38,7 +37,13 @@ describe('linkify-jquery', function () { // no document element, use a virtual dom to test jsdom.env( - 'Linkify Test', + 'Linkify Test' + + ''+ + '
Have a link to:\n github.com!
' + + '
' + + 'Another test@gmail.com email as well as a http://t.co link.' + + '
' + + '', ['http://code.jquery.com/jquery.js'], function (errors, window) { if (errors) { throw errors; } @@ -49,6 +54,23 @@ describe('linkify-jquery', function () { ); }); + beforeEach(function () { + // Make sure we start out with a fresh DOM every time + testContainer.innerHTML = htmlOptions.original; + }); + + it('Works with the DOM Data API', function () { + $('header').first().html().should.be.eql( + 'Have a link to:
github.com!' + ); + $('#linkify-test-div').html().should.be.eql( + 'Another test@gmail.com email as well as a ' + + 'http://t.co link.' + ); + }); + it('Works with default options', function () { var $container = $('#linkify-jquery-test-container'); ($container.length).should.be.eql(1); diff --git a/test/spec/linkify/core/tokens/multi-test.js b/test/spec/linkify/core/tokens/multi-test.js index af2eab0e..df14d7f1 100644 --- a/test/spec/linkify/core/tokens/multi-test.js +++ b/test/spec/linkify/core/tokens/multi-test.js @@ -113,6 +113,16 @@ describe('MULTI_TOKENS', function () { }); }); + describe('#hasProtocol()', function () { + it('Tests true when there is a protocol', function () { + url1.hasProtocol().should.be.ok; + }); + it('Tests false when there is no protocol', function () { + url2.hasProtocol().should.not.be.ok; + url3.hasProtocol().should.not.be.ok; + }); + }); + }); describe('EMAIL', function () { @@ -181,7 +191,7 @@ describe('MULTI_TOKENS', function () { new TEXT_TOKENS.DOMAIN('World'), new TEXT_TOKENS.SYM('!') ]; - text = new MULTI_TOKENS.NL(textTokens); + text = new MULTI_TOKENS.TEXT(textTokens); }); describe('#isLink', function () { @@ -197,4 +207,34 @@ describe('MULTI_TOKENS', function () { }); }); + /** + Static multitoken testing function + */ + describe('#test', function () { + var textToken, multiTokenOne, multiTokenTwo; + + before(function () { + textToken = new TEXT_TOKENS.DOMAIN('hi'); + multiTokenOne = new MULTI_TOKENS.Base([textToken]); + multiTokenTwo = new MULTI_TOKENS.TEXT([textToken]); + }); + + it('Tests false for non-multitokens', function () { + MULTI_TOKENS.Base.test().should.not.be.ok; + MULTI_TOKENS.Base.test(null).should.not.be.ok; + MULTI_TOKENS.Base.test(undefined).should.not.be.ok; + MULTI_TOKENS.Base.test(true).should.not.be.ok; + MULTI_TOKENS.Base.test(false).should.not.be.ok; + MULTI_TOKENS.Base.test('').should.not.be.ok; + MULTI_TOKENS.Base.test('wat').should.not.be.ok; + MULTI_TOKENS.Base.test(function () {}).should.not.be.ok; + MULTI_TOKENS.Base.test(textToken).should.not.be.ok; + }); + + it('Tests true for multitokens', function () { + MULTI_TOKENS.Base.test(multiTokenOne).should.be.ok; + MULTI_TOKENS.Base.test(multiTokenTwo).should.be.ok; + }); + }); + }); From 010241b7583f41c2cf9e226393fae8204e3ddadd Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 20:17:00 -0400 Subject: [PATCH 36/67] Improved gulp file stream handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let’s make sure the build process is deterministic! --- gulpfile.js | 82 +++++++++++++++++-------------- package.json | 3 +- src/linkify/core/parser.js | 2 +- test/spec/html/options.js | 14 ++++++ test/spec/html/original.html | 1 + test/spec/linkify-element-test.js | 6 +-- test/spec/linkify-jquery-test.js | 19 +++---- 7 files changed, 73 insertions(+), 54 deletions(-) create mode 100644 test/spec/html/options.js create mode 100644 test/spec/html/original.html diff --git a/gulpfile.js b/gulpfile.js index e7eb484b..63d649f6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,6 +2,7 @@ var gulp = require('gulp'), amdOptimize = require('amd-optimize'), glob = require('glob'), karma = require('karma').server, +merge = require('merge-stream'), path = require('path'), stylish = require('jshint-stylish'), tlds = require('./tlds'); @@ -56,7 +57,7 @@ gulp.task('babel', function () { */ gulp.task('babel-amd', function () { - gulp.src(paths.src) + return gulp.src(paths.src) .pipe(replace('__TLDS__', tldsReplaceStr)) .pipe(babel({ modules: 'amd', @@ -74,9 +75,9 @@ gulp.task('babel-amd', function () { // Build core linkify.js // Closure compiler is used here since it can correctly concatenate CJS modules -gulp.task('build-core', function () { +gulp.task('build-core', ['babel'], function () { - gulp.src(paths.libCore) + return gulp.src(paths.libCore) .pipe(closureCompiler({ compilerPath: 'node_modules/closure-compiler/lib/vendor/compiler.jar', fileName: 'build/.closure-output.js', @@ -99,7 +100,9 @@ gulp.task('build-core', function () { // Build root linkify interfaces (files located at the root src folder other // than linkify.js) // Depends on build-core -gulp.task('build-interfaces', function () { +gulp.task('build-interfaces', ['babel-amd'], function () { + + var stream, streams = []; // Core linkify functionality as plugins var interface, interfaces = [ @@ -132,7 +135,7 @@ gulp.task('build-interfaces', function () { } // Browser interface - gulp.src(files.js) + stream = gulp.src(files.js) .pipe(babel({ modules: 'ignore', format: babelformat @@ -141,19 +144,26 @@ gulp.task('build-interfaces', function () { .pipe(concat('linkify-' + interface + '.js')) .pipe(gulp.dest('build')); + streams.push(stream); + // AMD interface - gulp.src(files.amd) + stream = gulp.src(files.amd) .pipe(wrap({src: 'templates/linkify-' + interface + '.amd.js'})) .pipe(concat('linkify-' + interface + '.amd.js')) .pipe(gulp.dest('build')); + + streams.push(stream); } + return merge.apply(this, streams); }); /** NOTE - Run 'babel' and 'babel-amd' first */ -gulp.task('build-plugins', function () { +gulp.task('build-plugins', ['babel-amd'], function () { + + var stream, streams = []; // Get the filenames of all available plugins var @@ -169,7 +179,7 @@ gulp.task('build-plugins', function () { plugin = plugins[i]; // Global plugins - gulp.src('src/linkify/plugins/' + plugin + '.js') + stream = gulp.src('src/linkify/plugins/' + plugin + '.js') .pipe(babel({ modules: 'ignore', format: babelformat @@ -177,29 +187,34 @@ gulp.task('build-plugins', function () { .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.js'})) .pipe(concat('linkify-plugin-' + plugin + '.js')) .pipe(gulp.dest('build')); + streams.push(stream); // AMD plugins - gulp.src('build/amd/linkify/plugins/' + plugin + '.js') + stream = gulp.src('build/amd/linkify/plugins/' + plugin + '.js') .pipe(wrap({src: 'templates/linkify/plugins/' + plugin + '.amd.js'})) .pipe(concat('linkify-plugin-' + plugin + '.amd.js')) .pipe(gulp.dest('build')); + streams.push(stream); } - // AMD Browser plugins - for (i = 0; i < plugins.length; i++) { - plugin = plugins[i]; - } - + return merge.apply(this, streams); }); // Build steps +gulp.task('build', [ + 'babel', + 'babel-amd', + 'build-core', + 'build-interfaces', + 'build-plugins' +], function (cb) { cb(); }); /** Lint using jshint */ gulp.task('jshint', function () { - gulp.src([paths.src, paths.test, paths.spec]) + return gulp.src([paths.src, paths.test, paths.spec]) .pipe(jshint()) .pipe(jshint.reporter(stylish)) .pipe(jshint.reporter('fail')); @@ -208,7 +223,7 @@ gulp.task('jshint', function () { /** Run mocha tests */ -gulp.task('mocha', function () { +gulp.task('mocha', ['build'], function () { return gulp.src(paths.test, {read: false}) .pipe(mocha()); }); @@ -216,8 +231,8 @@ gulp.task('mocha', function () { /** Code coverage reort for mocha tests */ -gulp.task('coverage', function (cb) { - gulp.src(paths.lib) +gulp.task('coverage', ['build'], function (cb) { + return gulp.src(paths.lib) .pipe(istanbul()) // Covering files .pipe(istanbul.hookRequire()) // Force `require` to return covered files .on('finish', function () { @@ -228,28 +243,28 @@ gulp.task('coverage', function (cb) { }); }); -gulp.task('karma', function () { +gulp.task('karma', ['build'], function () { return karma.start({ configFile: __dirname + '/test/dev.conf.js', singleRun: true }); }); -gulp.task('karma-chrome', function () { - karma.start({ +gulp.task('karma-chrome', ['build'], function () { + return karma.start({ configFile: __dirname + '/test/chrome.conf.js', }); }); -gulp.task('karma-ci', function () { - karma.start({ +gulp.task('karma-ci', ['build'], function () { + return karma.start({ configFile: __dirname + '/test/ci.conf.js', singleRun: true }); }); -gulp.task('uglify', function () { - gulp.src('build/*.js') +gulp.task('uglify', ['build'], function () { + return gulp.src('build/*.js') .pipe(gulp.dest('dist')) // non-minified copy .pipe(rename(function (path) { path.extname = '.min.js'; @@ -258,23 +273,14 @@ gulp.task('uglify', function () { .pipe(gulp.dest('dist')); }); -gulp.task('build', [ - 'babel', - 'babel-amd', - 'build-core', - 'build-interfaces', - 'build-plugins' -]); - -gulp.task('dist', ['build', 'uglify']); - -gulp.task('test', ['jshint', 'build', 'mocha']); -gulp.task('test-ci', ['karma-ci']); +gulp.task('dist', ['uglify']); +gulp.task('test', ['build', 'jshint', 'mocha']); +gulp.task('test-ci', ['build', 'karma-ci']); // Using with other tasks causes an error here for some reason /** Build JS and begin watching for changes */ gulp.task('default', ['babel'], function () { - gulp.watch(paths.src, ['babel']); + return gulp.watch(paths.src, ['babel']); }); diff --git a/package.json b/package.json index 861b18d4..8d5f4d3a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": {}, "devDependencies": { "amd-optimize": "0.4.x", + "brfs": "^1.4.0", "chai": "^2.1.0", "closure-compiler": "0.2.x", "coveralls": "^2.11.2", @@ -27,7 +28,6 @@ "gulp-replace": "^0.5.3", "gulp-uglify": "^1.1.0", "gulp-wrap": "^0.11.0", - "jquery": "2.1.x", "jsdom": "^3.0.0", "jshint-stylish": "1.0.x", "karma": "^0.12.32", @@ -37,6 +37,7 @@ "karma-phantomjs-launcher": "0.1.x", "karma-sauce-launcher": "0.2.x", "lodash": "^3.3.1", + "merge-stream": "^0.1.7", "mocha": "2.1.x" }, "optionalDependencies": { diff --git a/src/linkify/core/parser.js b/src/linkify/core/parser.js index 8e061c20..5a425b60 100644 --- a/src/linkify/core/parser.js +++ b/src/linkify/core/parser.js @@ -283,4 +283,4 @@ let run = function (tokens) { let TOKENS = MULTI_TOKENS, start = S_START; -export { State, TOKENS, run, start}; +export { State, TOKENS, run, start }; diff --git a/test/spec/html/options.js b/test/spec/html/options.js new file mode 100644 index 00000000..f33e2e5a --- /dev/null +++ b/test/spec/html/options.js @@ -0,0 +1,14 @@ +// HTML to use with linkify-element and linkify-jquery +var fs = require('fs'); + +module.exports = { + original: fs.readFileSync(__dirname + '/original.html', 'utf8').trim(), + linkified: fs.readFileSync(__dirname + '/linkified.html', 'utf8').trim(), + linkifiedAlt: fs.readFileSync(__dirname + '/linkified-alt.html', 'utf8').trim(), + extra: fs.readFileSync(__dirname + '/extra.html', 'utf8').trim(), // for jQuery plugin tests + altOptions: { + linkAttributes: { + rel: 'nofollow' + } + } +}; diff --git a/test/spec/html/original.html b/test/spec/html/original.html new file mode 100644 index 00000000..5f0729b7 --- /dev/null +++ b/test/spec/html/original.html @@ -0,0 +1 @@ +Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 080c6225..aba7fb01 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -1,9 +1,8 @@ /*jshint -W030 */ var -doc, testContainer, -jsdom = require('jsdom'), +doc, testContainer, jsdom, linkifyElement = require('../../lib/linkify-element'), -htmlOptions = require('./html-options'); +htmlOptions = require('./html/options'); try { doc = document; @@ -29,6 +28,7 @@ describe('linkify-element', function () { if (doc) { return onDoc(doc); } // no document element, use a virtual dom to test + jsdom = require('jsdom'); jsdom.env( 'Linkify Test', diff --git a/test/spec/linkify-jquery-test.js b/test/spec/linkify-jquery-test.js index f9edd0a3..9d895e16 100644 --- a/test/spec/linkify-jquery-test.js +++ b/test/spec/linkify-jquery-test.js @@ -1,9 +1,8 @@ /*jshint -W030 */ var -$, doc, testContainer, -jsdom = require('jsdom'), +$, doc, testContainer, jsdom, applyLinkify = require('../../lib/linkify-jquery'), -htmlOptions = require('./html-options'); +htmlOptions = require('./html/options'); try { doc = document; @@ -23,8 +22,11 @@ describe('linkify-jquery', function () { function onDoc($, doc) { + doc.body.innerHTML = htmlOptions.extra; + // Add the linkify plugin to jQuery applyLinkify($, doc); + $(doc).trigger('ready'); testContainer = doc.createElement('div'); testContainer.id = 'linkify-jquery-test-container'; @@ -36,14 +38,9 @@ describe('linkify-jquery', function () { if (doc) { return onDoc($, doc); } // no document element, use a virtual dom to test + jsdom = require('jsdom'); jsdom.env( - 'Linkify Test' + - ''+ - '
Have a link to:\n github.com!
' + - '
' + - 'Another test@gmail.com email as well as a http://t.co link.' + - '
' + - '', + 'Linkify Test', ['http://code.jquery.com/jquery.js'], function (errors, window) { if (errors) { throw errors; } @@ -61,7 +58,7 @@ describe('linkify-jquery', function () { it('Works with the DOM Data API', function () { $('header').first().html().should.be.eql( - 'Have a link to:
github.com!' + 'Have a link to:
github.com!' ); $('#linkify-test-div').html().should.be.eql( 'Another Date: Sun, 15 Mar 2015 20:18:17 -0400 Subject: [PATCH 37/67] Better jQuery/HTML testing options This makes it easier to edit sample HTML used in the test files - these are the tests that verify that linkify-element and linkify-jquery work --- src/linkify-element.js | 2 +- src/linkify-jquery.js | 5 ++--- test/conf.js | 6 +++++- test/spec/html-options.js | 28 ---------------------------- test/spec/html/extra.html | 2 ++ test/spec/html/linkified-alt.html | 1 + test/spec/html/linkified.html | 1 + 7 files changed, 12 insertions(+), 33 deletions(-) delete mode 100644 test/spec/html-options.js create mode 100644 test/spec/html/extra.html create mode 100644 test/spec/html/linkified-alt.html create mode 100644 test/spec/html/linkified.html diff --git a/src/linkify-element.js b/src/linkify-element.js index bcf103b5..73d14006 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -4,7 +4,7 @@ import {tokenize, options} from './linkify'; -let HTML_NODE = 1, TXT_NODE = 3; +const HTML_NODE = 1, TXT_NODE = 3; /** Given an array of MultiTokens, return an array of Nodes that are either diff --git a/src/linkify-jquery.js b/src/linkify-jquery.js index e006f0a8..57eef812 100644 --- a/src/linkify-jquery.js +++ b/src/linkify-jquery.js @@ -52,12 +52,11 @@ function apply($, doc=null) { format: data.linkifyFormat, formatHref: data.linkifyFormatHref, newLine: data.linkifyNewline, // deprecated - nl2br: data.linkifyNl2br, + nl2br: !!data.linkifyNlbr, tagName: data.linkifyTagname, target: data.linkifyTarget, linkClass: data.linkifyLinkclass, }; - let $target = target === 'this' ? $this : $this.find(target); $target.linkify(options); }); @@ -65,7 +64,7 @@ function apply($, doc=null) { } // Apply it right away if possible -if (typeof jQuery !== 'undefined' && doc) { +if (typeof __karma__ === 'undefined' && typeof jQuery !== 'undefined' && doc) { apply(jQuery, doc); } diff --git a/test/conf.js b/test/conf.js index c56b3461..e67b8703 100644 --- a/test/conf.js +++ b/test/conf.js @@ -36,7 +36,11 @@ module.exports = { browserify: { debug: false, - // transform: [ 'brfs' ] + ignore: ['jsdom'], + transform: ['brfs'], + configure: function (bundle) { + bundle.ignore('jsdom'); + }, }, // test results reporter to use diff --git a/test/spec/html-options.js b/test/spec/html-options.js deleted file mode 100644 index f470ec18..00000000 --- a/test/spec/html-options.js +++ /dev/null @@ -1,28 +0,0 @@ -// HTML to use with linkify-element and linkify-jquery -module.exports = { - original: - 'Hello here are some links to ftp://awesome.com/?where=this and '+ - 'localhost:8080, pretty neat right? '+ - '

Here\'s a nested github.com/SoapBox/linkifyjs paragraph

', - linkified: - 'Hello here are some links to ftp://awesome.com/?where=this and ' + - 'localhost:8080, pretty neat right?

Here\'s a nested ' + - 'github.com/SoapBox/linkifyjs paragraph

', - linkifiedAlt: - 'Hello here are some links to ftp://awesome.com/?where=this and ' + - 'localhost:8080, pretty neat right?

Here\'s a nested ' + - 'github.com/SoapBox/linkifyjs paragraph

', - altOptions: { - linkAttributes: { - rel: 'nofollow' - } - } -}; diff --git a/test/spec/html/extra.html b/test/spec/html/extra.html new file mode 100644 index 00000000..5ee8b305 --- /dev/null +++ b/test/spec/html/extra.html @@ -0,0 +1,2 @@ +
Have a link to: +github.com!
Another test@gmail.com email as well as a http://t.co link.
diff --git a/test/spec/html/linkified-alt.html b/test/spec/html/linkified-alt.html new file mode 100644 index 00000000..f70f148f --- /dev/null +++ b/test/spec/html/linkified-alt.html @@ -0,0 +1 @@ +Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

diff --git a/test/spec/html/linkified.html b/test/spec/html/linkified.html new file mode 100644 index 00000000..80b45f33 --- /dev/null +++ b/test/spec/html/linkified.html @@ -0,0 +1 @@ +Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

From 6308e2c7baf82a56e65e8059ec908b4805070739 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 20:18:39 -0400 Subject: [PATCH 38/67] Updated documentation Almost there, just a few more API docs to go --- README.md | 266 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 183 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index b59b4975..5e134da6 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,9 @@ __Jump to__ - [AMD Modules](#amd-modules) - [Browser](#browser) - [API](#api) - - [`string`](#string) - - [`jquery`](#string) - - [Options](#options) +- [Plugin API](#plugin-api) +- [Caveats](#caveats) - [Contributing](#contributing) - [Authors](#authors) - [License](#license) ## Features @@ -42,34 +40,65 @@ Add [linkify](#) and [linkify-jquery](#) to your HTML following jQuery: ```html - - +``` - var links = linkify.find('Any links to github.com here?'); - console.log(links); - // [{type: 'url', value: 'github.com', href: 'http://github.com'}] +#### Find all links and convert them to anchor tags - // Find links and emails in paragraphs and `#sidebar` - // and converts them to anchors - $('p').linkify(); - $('#sidebar').linkify({ - target: "_blank" - }); +```js +$('p').linkify(); +$('#sidebar').linkify({ + target: "_blank" +}); +``` - }) })(jQuery); - +#### Find all links in the given string + +```js +linkify.find('Any links to github.com here? If not, contact test@example.com'); ``` -### Node.js/Browserify +Returns the following array + +```js +[ + { + type: 'url', + value: 'github.com', + href: 'http://github.com' + }, + { + type: 'email', + value: 'test@example.com', + href: 'mailto:test@example.com' + } +] +``` + + +See [all available options](#options) + + +### Node.js/io.js/Browserify ```js var linkify = require('linkifyjs'); -var linkifyInterface = require('linkifyjs/'); -require('linkifyjs/plugin/')(linkify); +var linkifyStr = require('linkifyjs/string'); +require('linkifyjs/plugin/hashtag')(linkify); // optional +``` + +#### Example string usage -linkify.find('github.com'); -linkifyInterface(target, options); // varies +```js +linkifyStr('The site github.com is #awesome.', { + defaultProtocol: 'https' +}); +``` + +Returns the following string + +```js +'The site github.com is #awesome.' ``` ### AMD @@ -77,28 +106,45 @@ linkifyInterface(target, options); // varies ```html - - - + +``` - require(['linkify-'], function (linkifyInterface) { - var linkified = linkifyInterface(target, options); // varies - console.log(linkified); - }); +```js +require(['linkify'], function (linkify) { + linkify.test('github.com'); // true + linkify.test('github.com', 'email'); // false +}); + +require(['linkify-element'], function (linkifyElement) { + + // Linkify the #sidebar element + linkifyElement(document.getElementById('#sidebar'), { + linkClass: 'my-link' + }); + + // Linkify all paragraph tags + document.getElementsByTagName('p').map(linkifyElement); + +}); - ``` +Note that if you are using `linkify-jquery.amd.js`, a `jquery` module must be defined. + ### Browser ```html + - - + + +``` + +```js +linkify.test('dev@example.com'); // true +var htmlStr = linkifyStr('Check out soapboxhq.com it is great!'); +$('p').linkify(); ``` ## Downloads @@ -108,6 +154,8 @@ linkifyInterface(target, options); // varies **Interfaces** _(recommended - include at least one)_ * **[string](#linkify-string)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.js) +* **[jquery](#linkify-jquery)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-jquery.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-jquery.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-jquery.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-jquery.amd.js) +* **[element](#linkify-element)** _(Included with linkify-jquery)_
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.amd.js) **Plugins** _(optional)_ @@ -116,16 +164,29 @@ linkifyInterface(target, options); // varies ## API +**Jump To** + +* [Standard](#standard-linkify) +* [`string`](#linkify-string) +* [`jquery`](#linkify-jquery) + - [DOM Data API](#dom-data-api) +* [`element`](#linkify-element) +* [Options](#options) +* [Plugins](#plugins) + +### Standard linkify + +#### Installation + +##### Node.js/io.js/Browserify + ```js -// Node.js/Browserify usage var linkify = require('linkifyjs'); ``` -```html - - +##### AMD - +```html ``` -#### linkify.find _(str)_ +##### Global +```html + +``` + +#### Methods + +##### linkify.find _(str)_ Finds all links in the given string **Params** -* `String` **`str`** Search string +* _`String`_ **`str`** Search string **Returns** _`Array`_ List of links where each element is a hash with properties `type`, `value`, and `href` ```js linkify.find('For help with GitHub.com, please email support@github.com'); -/** // returns +``` + +Returns the array + +```js [{ type: 'url', value: 'GitHub.com', @@ -156,10 +228,9 @@ linkify.find('For help with GitHub.com, please email support@github.com'); value: 'support@github.com', href: 'mailto:support@github.com' }] -*/ ``` -#### linkify.test _(str)_ +##### linkify.test _(str)_ Is the given string a link? Not to be used for strict validation - See [Caveats](#) @@ -176,7 +247,7 @@ linkify.test('google.com', 'email'); // false #### linkify.tokenize _(str)_ -Internal method used to perform lexicographical analysis on the given string and output the resulting array tokens. +Internal method used to perform lexicographical analysis on the given string and output the resulting token array. **Params** @@ -184,72 +255,85 @@ Internal method used to perform lexicographical analysis on the given string and **Returns** _`Array`_ -### Interfaces -#### linkify-jquery +### linkify-jquery Provides the Linkify jQuery plugin. +#### Installation + +##### Node.js/io.js/Browserify ```js -// TODO: How do you build a Browserify jQuery plugin?? var $ = require('jquery'); require('linkifyjs/jquery')($); ``` -```html - +##### AMD - - - +Note that `linkify-jquery` requires a `jquery` module. - - +```html + - - +``` + +```js +require(['jquery'], function ($) { // ... }); - ``` -**Usage** +##### Global + +```html + + + +``` + +#### Usage ```js +var options = { /* ... */ }; $(selector).linkify(options); ``` -**DOM Data API** +**Params** + +* `Object` [**`options`**] [Options hash](#options) + +See [all available options](#options). + +#### DOM Data API + +The jQuery plugin also provides a DOM data/HTML API - no extra JavaScript required! ```html - +
...
- + ... ``` -**Params** +[Additional data options](#options) are available. -* `Object` [**`options`**] [Options hash](#) +### linkify-string +Interface for replacing links within native strings with anchor tags. Note that this function will ***not*** parse HTML strings properly - use [`linkify-element`](#linkify-element) or [`linkify-jquery`](#linkify-jquery) instead. -#### linkify-string +#### Installation -Interface for replacing links within native strings with anchor tags. Note that this function will **not** parse HTML strings - use [linkify-dom](#) or [linkify-jquery](#) instead. +##### Node.js/io.js/Browserify ```js -// Node.js/Browserify usage -var linkifyStr = require('linkifyjs/string'),; +var linkifyStr = require('linkifyjs/string'); ``` -```html - - - +##### AMD - +```html ``` +##### Global + +```html + + +``` + **Usage** ```js var options = {/* ... */}; -linkifyStr('For help with GitHub.com, please email support@github.com'); -// returns "For help with GitHub.com, please email support@github.com +var str = 'For help with GitHub.com, please email support@github.com'; +linkifyStr(str, options); +// or +str.linkify(options); ``` -or +Returns ```js -var options = {/* ... */}; -'For help with GitHub.com, please email support@github.com'.linkify(options); +'For help with GitHub.com, please email support@github.com' ``` **Params** @@ -284,7 +376,7 @@ var options = {/* ... */}; ### Plugins -Plugins provide no new interfaces but add additional detection functionality to Linkify. A plugic plugin API is currently in the works. +Plugins provide no new interfaces but add additional detection functionality to Linkify. A custom plugin API is currently in the works. #### hashtag @@ -366,6 +458,14 @@ str.linkify(options); ``` +## Plugin API + +Coming soon + +## Caveats + +* + ## Contributing From eb66d3fea7928da971fbbd4a4671c74d92900fb8 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 21:11:55 -0400 Subject: [PATCH 39/67] Fixed jQuery interface build step All seems to be working for now! --- gulpfile.js | 10 +++++----- src/linkify-element.js | 2 +- templates/linkify-jquery.amd.js | 6 ++++++ templates/linkify-jquery.js | 6 ++++++ test/index.html | 14 +++++++++++--- 5 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 templates/linkify-jquery.amd.js create mode 100644 templates/linkify-jquery.js diff --git a/gulpfile.js b/gulpfile.js index 63d649f6..975f1304 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -108,7 +108,7 @@ gulp.task('build-interfaces', ['babel-amd'], function () { var interface, interfaces = [ 'string', 'element', - // ['element', 'jquery'] // jQuery interface requires both element and jquery + ['element', 'jquery'] // jQuery interface requires both element and jquery ]; var files = {js: null, amd: null}; @@ -122,8 +122,8 @@ gulp.task('build-interfaces', ['babel-amd'], function () { files.js = []; files.amd = []; for (var j = 0; j < interface.length; j++) { - files.js.push('src/linkify-' + interface[i] + '.js'); - files.amd.push('build/amd/linkify-' + interface[i] + '.js'); + files.js.push('src/linkify-' + interface[j] + '.js'); + files.amd.push('build/amd/linkify-' + interface[j] + '.js'); } // The last dependency is the name of the interface @@ -140,16 +140,16 @@ gulp.task('build-interfaces', ['babel-amd'], function () { modules: 'ignore', format: babelformat })) - .pipe(wrap({src: 'templates/linkify-' + interface + '.js'})) .pipe(concat('linkify-' + interface + '.js')) + .pipe(wrap({src: 'templates/linkify-' + interface + '.js'})) .pipe(gulp.dest('build')); streams.push(stream); // AMD interface stream = gulp.src(files.amd) - .pipe(wrap({src: 'templates/linkify-' + interface + '.amd.js'})) .pipe(concat('linkify-' + interface + '.amd.js')) + .pipe(wrap({src: 'templates/linkify-' + interface + '.amd.js'})) .pipe(gulp.dest('build')); streams.push(stream); diff --git a/src/linkify-element.js b/src/linkify-element.js index 73d14006..4dda0df3 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -85,7 +85,7 @@ function linkifyElementHelper(element, opts, doc) { case TXT_NODE: let - str = childElement.nodeValue, + str = childElement.nodeValue.replace(/^\s+|\s+$/g, ''), // trim tokens = tokenize(str); children.push(...tokensToNodes(tokens, opts, doc)); diff --git a/templates/linkify-jquery.amd.js b/templates/linkify-jquery.amd.js new file mode 100644 index 00000000..40efa42a --- /dev/null +++ b/templates/linkify-jquery.amd.js @@ -0,0 +1,6 @@ +<%= contents %> +require(['jquery', 'linkify-jquery'], function ($, apply) { + if (typeof $.fn.linkify !== 'function') { + apply($); + } +}); diff --git a/templates/linkify-jquery.js b/templates/linkify-jquery.js new file mode 100644 index 00000000..c792fc83 --- /dev/null +++ b/templates/linkify-jquery.js @@ -0,0 +1,6 @@ +;(function (jQuery, linkify) { +"use strict"; +var tokenize = linkify.tokenize, options = linkify.options; +<%= contents %> +window.linkifyElement = linkifyElement; +})(window.jQuery, window.linkify); diff --git a/test/index.html b/test/index.html index 4f28d377..3c5d8cc3 100644 --- a/test/index.html +++ b/test/index.html @@ -8,14 +8,22 @@ - - + + -

+ + +

You let's get all up in the http://element.co/?wat=this and the #swag

- + ``` ```js @@ -132,7 +132,7 @@ require(['linkify-element'], function (linkifyElement) { Note that if you are using `linkify-jquery.amd.js`, a `jquery` module must be defined. -### Browser +### Browser globals ```html @@ -195,7 +195,7 @@ var linkify = require('linkifyjs'); ``` -##### Global +##### Browser globals ```html ``` @@ -284,7 +284,7 @@ require(['jquery'], function ($) { }); ``` -##### Global +##### Browser globals ```html @@ -343,7 +343,7 @@ var linkifyStr = require('linkifyjs/string'); ``` -##### Global +##### Browser globals ```html diff --git a/package.json b/package.json index 0eabf0de..ae358be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkifyjs", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.3", "description": "Intelligent URL recognition, made easy", "repository": { "type" : "git", From 818ccdc3517f5290a3a7363c90b554d5163607f4 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 21:58:55 -0400 Subject: [PATCH 43/67] Updating dev dependencies Also including better dependencies badge --- .npmignore | 2 ++ README.md | 2 +- package.json | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.npmignore b/.npmignore index ae689a8d..ad4a0eec 100644 --- a/.npmignore +++ b/.npmignore @@ -3,7 +3,9 @@ amd assets bower_components build +coverage demo +dist src templates test diff --git a/README.md b/README.md index ac97b9f5..d212d2cb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Linkify -[![Node Dependencies](https://david-dm.org/SoapBox/jQuery-linkify/dev-status.png)](https://david-dm.org/SoapBox/jQuery-linkify#info=devDependencies&view=table) +[![Dependency Status](https://gemnasium.com/SoapBox/jQuery-linkify.svg)](https://gemnasium.com/SoapBox/jQuery-linkify) Linkify is a small yet comprehensive JavaScript plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. diff --git a/package.json b/package.json index ae358be0..5405df26 100644 --- a/package.json +++ b/package.json @@ -15,34 +15,34 @@ "license": "MIT", "dependencies": {}, "devDependencies": { - "amd-optimize": "0.4.x", + "amd-optimize": "^0.4.3", "brfs": "^1.4.0", - "chai": "^2.1.0", - "closure-compiler": "0.2.x", + "chai": "^2.1.1", + "closure-compiler": "^0.2.6", "coveralls": "^2.11.2", - "glob": "^4.4.1", - "gulp": "3.8.x", + "glob": "^5.0.3", + "gulp": "^3.8.11", "gulp-babel": "^4.0.0", - "gulp-closure-compiler": "0.2.x", + "gulp-closure-compiler": "^0.2.14", "gulp-concat": "^2.5.2", "gulp-istanbul": "^0.6.0", - "gulp-jshint": "1.9.x", - "gulp-mocha": "2.0.x", - "gulp-rename": "1.2.x", + "gulp-jshint": "^1.9.2", + "gulp-mocha": "^2.0.0", + "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.3", "gulp-uglify": "^1.1.0", "gulp-wrap": "^0.11.0", "jsdom": "^3.0.0", - "jshint-stylish": "1.0.x", + "jshint-stylish": "^1.0.1", "karma": "^0.12.32", "karma-browserify": "^4.0.0", - "karma-chrome-launcher": "0.1.x", - "karma-mocha": "0.1.x", - "karma-phantomjs-launcher": "0.1.x", - "karma-sauce-launcher": "0.2.x", - "lodash": "^3.3.1", + "karma-chrome-launcher": "^0.1.7", + "karma-mocha": "^0.1.10", + "karma-phantomjs-launcher": "^0.1.4", + "karma-sauce-launcher": "^0.2.10", + "lodash": "^3.5.0", "merge-stream": "^0.1.7", - "mocha": "2.1.x" + "mocha": "^2.2.1" }, "optionalDependencies": { "jquery": "^2.1.3" From c666b73461f1c2dc60d3672e75435442a1aebb7f Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 22:07:37 -0400 Subject: [PATCH 44/67] Badges galore! NPM version and code coverage status (unknown so far) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d212d2cb..e5a1d51d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Linkify +[![npm version](https://badge.fury.io/js/linkifyjs.svg)](http://badge.fury.io/js/linkifyjs) +[![Coverage Status](https://coveralls.io/repos/SoapBox/jQuery-linkify/badge.svg)](https://coveralls.io/r/SoapBox/jQuery-linkify) [![Dependency Status](https://gemnasium.com/SoapBox/jQuery-linkify.svg)](https://gemnasium.com/SoapBox/jQuery-linkify) Linkify is a small yet comprehensive JavaScript plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. From 343d73d3e596f433e9468723751cf93f6397c2b7 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 15 Mar 2015 23:35:20 -0400 Subject: [PATCH 45/67] Test/compilation fixes, Travis, readme --- .travis.yml | 7 ++++++- README.md | 4 ++-- gulpfile.js | 3 ++- package.json | 3 ++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 18ae2d89..20e0a3f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,9 @@ language: node_js node_js: - - "0.11" + - "0.12" - "0.10" + - "iojs" + +script: + - npm test + - npm run coverage diff --git a/README.md b/README.md index e5a1d51d..e8ba4aaf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Linkify -[![npm version](https://badge.fury.io/js/linkifyjs.svg)](http://badge.fury.io/js/linkifyjs) -[![Coverage Status](https://coveralls.io/repos/SoapBox/jQuery-linkify/badge.svg)](https://coveralls.io/r/SoapBox/jQuery-linkify) +[![npm version](https://badge.fury.io/js/linkifyjs.svg)](https://www.npmjs.com/package/linkifyjs) [![Dependency Status](https://gemnasium.com/SoapBox/jQuery-linkify.svg)](https://gemnasium.com/SoapBox/jQuery-linkify) +[![Coverage Status](https://coveralls.io/repos/SoapBox/jQuery-linkify/badge.svg)](https://coveralls.io/r/SoapBox/jQuery-linkify) Linkify is a small yet comprehensive JavaScript plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. diff --git a/gulpfile.js b/gulpfile.js index 975f1304..1e6b526b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -85,6 +85,7 @@ gulp.task('build-core', ['babel'], function () { process_common_js_modules: null, common_js_entry_module: 'lib/linkify', common_js_module_path_prefix: path.join(__dirname, 'lib'), + compilation_level: 'SIMPLE_OPTIMIZATIONS', formatting: 'PRETTY_PRINT' } })) @@ -232,7 +233,7 @@ gulp.task('mocha', ['build'], function () { Code coverage reort for mocha tests */ gulp.task('coverage', ['build'], function (cb) { - return gulp.src(paths.lib) + gulp.src(paths.lib) .pipe(istanbul()) // Covering files .pipe(istanbul.hookRequire()) // Force `require` to return covered files .on('finish', function () { diff --git a/package.json b/package.json index 5405df26..c5860460 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "main": "index.js", "scripts": { "prepublish": "node_modules/.bin/gulp build", - "test": "node_modules/.bin/gulp test" + "test": "node_modules/.bin/gulp coverage", + "coverage": "./node_modules/coveralls/bin/coveralls.js < coverage/lcov.info" }, "author": "SoapBox Innovations (@SoapBoxHQ)", "license": "MIT", From 73bbd298ee2d6ae48a29db2dca61b8476852f99b Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 16 Mar 2015 12:01:17 -0400 Subject: [PATCH 46/67] NPM version bump, additional docs Trying to get a super solid README --- README.md | 205 ++++++++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 132 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index e8ba4aaf..e4963019 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![npm version](https://badge.fury.io/js/linkifyjs.svg)](https://www.npmjs.com/package/linkifyjs) [![Dependency Status](https://gemnasium.com/SoapBox/jQuery-linkify.svg)](https://gemnasium.com/SoapBox/jQuery-linkify) +[![Build Status](https://travis-ci.org/SoapBox/jQuery-linkify.svg)](https://travis-ci.org/SoapBox/jQuery-linkify) [![Coverage Status](https://coveralls.io/repos/SoapBox/jQuery-linkify/badge.svg)](https://coveralls.io/r/SoapBox/jQuery-linkify) Linkify is a small yet comprehensive JavaScript plugin for finding URLs in plain-text and converting them to HTML links. It works with all valid URLs and email addresses. @@ -85,8 +86,8 @@ See [all available options](#options) ```js var linkify = require('linkifyjs'); -var linkifyStr = require('linkifyjs/string'); require('linkifyjs/plugin/hashtag')(linkify); // optional +var linkifyStr = require('linkifyjs/string'); ``` #### Example string usage @@ -153,6 +154,10 @@ $('p').linkify(); **[linkify](#api)** _(required)_
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify.amd.js) +**Plugins** _(optional)_ + +* **[hashtag](#linkify-plugin-hashtag)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.js) + **Interfaces** _(recommended - include at least one)_ * **[string](#linkify-string)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-string.amd.js) @@ -160,10 +165,6 @@ $('p').linkify(); * **[element](#linkify-element)** _(Included with linkify-jquery)_
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-element.amd.js) -**Plugins** _(optional)_ - -* **[hashtag](#linkify-plugin-hashtag)**
[`.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.min.js) · [`.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.js) · [`.amd.min.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.min.js) · [`.amd.js`](https://github.com/nfrasser/linkify-shim/raw/master/linkify-plugin-hashtag.amd.js) - ## API **Jump To** @@ -176,7 +177,7 @@ $('p').linkify(); * [Options](#options) * [Plugins](#plugins) -### Standard linkify +### Standard `linkify` #### Installation @@ -204,15 +205,23 @@ var linkify = require('linkifyjs'); #### Methods -##### linkify.find _(str)_ +##### `linkify.find` _(`str` [, `type`])_ Finds all links in the given string **Params** * _`String`_ **`str`** Search string +* _`String`_ [**`type`**] (Optional) only find links of the given type + +**Returns** _`Array`_ List of links where each element is a hash with properties `type`, `value`, and `href`. -**Returns** _`Array`_ List of links where each element is a hash with properties `type`, `value`, and `href` +* `type` is the type of entity found. Possible values are + - `'url'` + - `'email'` + - `'hashtag'` (with Hashtag plugin) +* `value` is the original entity substring. +* `href` should be the value of this link's `href` attribute. ```js linkify.find('For help with GitHub.com, please email support@github.com'); @@ -221,24 +230,28 @@ linkify.find('For help with GitHub.com, please email support@github.com'); Returns the array ```js -[{ +[ + { type: 'url', value: 'GitHub.com', href: 'http://github.com', -}, { + }, + { type: 'email', value: 'support@github.com', href: 'mailto:support@github.com' -}] + } +] ``` -##### linkify.test _(str)_ +##### `linkify.test` _(`str` [, `type`])_ Is the given string a link? Not to be used for strict validation - See [Caveats](#) **Params** -* `String` **`str`** Test string +* _`String`_ **`str`** Test string +* _`String`_ [**`type`**] (Optional) returns `true` only if the link is of the given type (see `linkify.find`), **Returns** _`Boolean`_ @@ -247,29 +260,32 @@ linkify.test('google.com'); // true linkify.test('google.com', 'email'); // false ``` -#### linkify.tokenize _(str)_ +#### `linkify.tokenize` _(`str`)_ Internal method used to perform lexicographical analysis on the given string and output the resulting token array. **Params** -* `String` **`str`** +* _`String`_ **`str`** **Returns** _`Array`_ -### linkify-jquery +### `linkify-jquery` Provides the Linkify jQuery plugin. #### Installation ##### Node.js/io.js/Browserify + ```js var $ = require('jquery'); -require('linkifyjs/jquery')($); +require('linkifyjs/jquery')($, document); ``` +Where the second argument is your `window.document` implementation (not required for Browserify). + ##### AMD Note that `linkify-jquery` requires a `jquery` module. @@ -303,7 +319,7 @@ $(selector).linkify(options); **Params** -* `Object` [**`options`**] [Options hash](#options) +* _`Object`_ [**`options`**] [Options hash](#options) See [all available options](#options). @@ -321,7 +337,7 @@ The jQuery plugin also provides a DOM data/HTML API - no extra JavaScript requir [Additional data options](#options) are available. -### linkify-string +### `linkify-string` Interface for replacing links within native strings with anchor tags. Note that this function will ***not*** parse HTML strings properly - use [`linkify-element`](#linkify-element) or [`linkify-jquery`](#linkify-jquery) instead. @@ -352,7 +368,7 @@ var linkifyStr = require('linkifyjs/string'); ``` -**Usage** +#### Usage ```js var options = {/* ... */}; @@ -370,8 +386,8 @@ Returns **Params** -* `String` **`str`** String to linkify -* `Object` [**`options`**] [Options hash](#) +* _`String`_ **`str`** String to linkify +* _`Object`_ [**`options`**] [Options hash](#) **Returns** _`String`_ Linkified string @@ -380,85 +396,129 @@ Returns Plugins provide no new interfaces but add additional detection functionality to Linkify. A custom plugin API is currently in the works. -#### hashtag +**Note:** Plugins should be included before interfaces. -Adds basic support for Twitter-style hashtags. +#### General Installation + +##### Node.js/io.js/Browserify ```js -// Node.js/Browserify -var linkify = require('linkifyjs'); -require('linkifyjs/plugins/hashtag')(linkify); +var linkify = require('linkifyjs') +require('linkifyjs/plugin/')(linkify); ``` -```html - - - +##### AMD - +```html - - + +``` + +##### Browser globals + +```html + + ``` -**Usage** +#### `hashtag` Plugin + +Adds basic support for Twitter-style hashtags. + +```js +var linkify = require('linkifyjs'); +require('linkifyjs/plugins/hashtag')(linkify); +``` ```js var options = {/* ... */}; -var str = "Linkify is #super #rad"; +var str = "Linkify is #super #rad2015"; linkify.find(str); -/* [ - {type: 'hashtag', value: "#super", href: "#super"}, - {type: 'hashtag', value: "#rad", href: "#rad"} -] */ +``` -// If the linkifyStr interface has also been included -linkifyStr(str) +Returns the following array +```js +[ + { + type: 'hashtag', + value: "#super", + href: "#super" + }, + { + type: 'hashtag', + value: "#rad2015", + href: "#rad2015" + } +] ``` - ## Options Linkify is applied with the following default options. Below is a description of each. ```js var options = { - tagName: 'span', - defaultProtocol: 'https', - target: '_parent', - nl2br: true, - linkClass: 'a-new-link', - linkAttributes: { - rel: 'nofollow' - }, - format: function (link, type) { - if (type === 'hashtag') { - link = link.toLowerCase(); - } - return link; - }, - formatHref: function (link, type) { - if (type === 'hashtag') { - link = 'https://twitter.com/hashtag/' + link.replace('#', ''); - } - return link; + defaultProtocol: 'http', + format: null, + formatHref: null, + linkAttributes: null, + linkClass: 'linkified', + nl2br: false, + tagName: 'a', + target: function (type) { + return type === 'url' ? '_blank' : null; } }; +``` -// jQuery -$('selector').linkify(options); +### Usage -// String -linkifyStr(str, options); -str.linkify(options); +```js +linkifyStr(str, options); // or `str.linkify(options)` +linkifyElement(document.getElementById(id), options); +$(selector).linkify(options); ``` +#### `defaultProtocol` + +**Type**: `String` +**Default**: `'http'` +**Values**: `'http'`, `'https'`, `'ftp'`, `'ftps'`, etc. +**Data API**: `data-linkify-default-protocol` + +Protocol that should be used in `href` attributes for URLs without a protocol (e.g., `github.com`). + +#### `format` + +**Type**: `Function` +**Default**: `null` + +Format the text displayed by a linkified entity. e.g., truncate a long URL. + +```js +'http://github.com/SoapBox/linkifyjs/search/?q=this+is+a+really+long+query+string'.linkify({ + format: function (value, type) { + if (type === 'url' && value.length > 50) { + value = value.slice(0, 50) + '…'; + } + return value; + } +}); +``` + +#### `formatHref` + +#### `nl2br` + +#### `tagName` + +#### `target` + +#### `linkAttributes` + +#### `linkClass` ## Plugin API @@ -466,10 +526,7 @@ Coming soon ## Caveats -* - ## Contributing - ## Authors Linkify is handcrafted with Love by [SoapBox Innovations, Inc](http://soapboxhq.com). diff --git a/package.json b/package.json index c5860460..05e379ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkifyjs", - "version": "2.0.0-alpha.3", + "version": "2.0.0-alpha.4", "description": "Intelligent URL recognition, made easy", "repository": { "type" : "git", From cb1e6c1f1e13574f5bd272ce1ab2e62365425936 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 16 Mar 2015 12:04:00 -0400 Subject: [PATCH 47/67] travis.yml fixes Tabs were breaking things --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 20e0a3f9..aa4223b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: node_js node_js: - - "0.12" - - "0.10" - - "iojs" + - 0.12 + - 0.10 + - iojs script: - - npm test - - npm run coverage + - npm test + - npm run coverage From 219c70abccd4f2ee3fd31906986b7bb8ff56e4f9 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 16 Mar 2015 12:07:04 -0400 Subject: [PATCH 48/67] Wrapping Travis node versions in quotes --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa4223b3..c41f6774 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: - - 0.12 - - 0.10 - - iojs + - "0.12" + - "0.10" + - "iojs" script: - npm test From 8d1eb20f22ed5520cc1c87a94a871544db82c966 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 16 Mar 2015 13:02:15 -0400 Subject: [PATCH 49/67] Readme fixes, Travis Moved Travis coverage report to after_script --- .travis.yml | 5 ++--- README.md | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c41f6774..29458440 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,5 @@ node_js: - "0.10" - "iojs" -script: - - npm test - - npm run coverage +script: npm test +after_script: npm run coverage diff --git a/README.md b/README.md index e4963019..3eb4cf9b 100644 --- a/README.md +++ b/README.md @@ -483,17 +483,17 @@ $(selector).linkify(options); #### `defaultProtocol` -**Type**: `String` -**Default**: `'http'` -**Values**: `'http'`, `'https'`, `'ftp'`, `'ftps'`, etc. -**Data API**: `data-linkify-default-protocol` +**Type**: `String`
+**Default**: `'http'`
+**Values**: `'http'`, `'https'`, `'ftp'`, `'ftps'`, etc.
+**Data API**: `data-linkify-default-protocol`
Protocol that should be used in `href` attributes for URLs without a protocol (e.g., `github.com`). #### `format` -**Type**: `Function` -**Default**: `null` +**Type**: `Function`
+**Default**: `null`
Format the text displayed by a linkified entity. e.g., truncate a long URL. From 92766794a495222d19f899200d0e96c784ba073d Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 16 Mar 2015 15:29:11 -0400 Subject: [PATCH 50/67] Small readme update, prepublish script prepublish now deletes the existing `lib` folder so that the Node library is generated from scratch. --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3eb4cf9b..38b163c6 100644 --- a/README.md +++ b/README.md @@ -492,7 +492,7 @@ Protocol that should be used in `href` attributes for URLs without a protocol (e #### `format` -**Type**: `Function`
+**Type**: `Function (String value, String type)`
**Default**: `null`
Format the text displayed by a linkified entity. e.g., truncate a long URL. diff --git a/package.json b/package.json index 05e379ce..6a1a0f8e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "main": "index.js", "scripts": { - "prepublish": "node_modules/.bin/gulp build", + "prepublish": "rm -rf lib/* && node_modules/.bin/gulp build", "test": "node_modules/.bin/gulp coverage", "coverage": "./node_modules/coveralls/bin/coveralls.js < coverage/lcov.info" }, From ba00660fd4fc9bc393e0d3c661c88e7388d0244e Mon Sep 17 00:00:00 2001 From: nfrasser Date: Tue, 17 Mar 2015 22:12:15 -0400 Subject: [PATCH 51/67] Tiny readme update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proper getElementById documentation I’ve also decided to move all he docs to gh-pages --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38b163c6..231ee279 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ require(['linkify'], function (linkify) { require(['linkify-element'], function (linkifyElement) { // Linkify the #sidebar element - linkifyElement(document.getElementById('#sidebar'), { + linkifyElement(document.getElementById('sidebar'), { linkClass: 'my-link' }); From ced72c475a45330808f323d0a799e125afb00d95 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 29 Mar 2015 23:28:23 -0400 Subject: [PATCH 52/67] Updated gitignore for demo site branch So that I can switch branches in peace --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 70dcf3a5..2d65b643 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,9 @@ node_modules build dist lib + +# Specific to plugin website branch +.sass-cache +_site +_sass +js From 7e67b317c56581d9dffd485051dcdd697b49016c Mon Sep 17 00:00:00 2001 From: nfrasser Date: Mon, 30 Mar 2015 21:32:38 -0400 Subject: [PATCH 53/67] Gulp tasks of legacy and benchmarks Includes build templates --- gulpfile.js | 22 ++++++++++++++++++++-- templates/linkify-benchmark.js | 6 ++++++ templates/linkify-legacy.js | 2 ++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 templates/linkify-benchmark.js create mode 100644 templates/linkify-legacy.js diff --git a/gulpfile.js b/gulpfile.js index 1e6b526b..5c8c332b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -233,6 +233,7 @@ gulp.task('mocha', ['build'], function () { Code coverage reort for mocha tests */ gulp.task('coverage', ['build'], function (cb) { + // IMPORTANT: return not required here (and will actually cause bugs!) gulp.src(paths.lib) .pipe(istanbul()) // Covering files .pipe(istanbul.hookRequire()) // Force `require` to return covered files @@ -264,8 +265,25 @@ gulp.task('karma-ci', ['build'], function () { }); }); -gulp.task('uglify', ['build'], function () { - return gulp.src('build/*.js') +// Build the deprecated legacy interface +gulp.task('build-legacy', ['build'], function () { + return gulp.src(['build/linkify.js', 'build/linkify-jquery.js']) + .pipe(concat('jquery.linkify.js')) + .pipe(wrap({src: 'templates/linkify-legacy.js'})) + .pipe(gulp.dest('build/dist')); +}); + +// Build a file that can be used for easy headless benchmarking +gulp.task('build-benchmark', ['build-legacy'], function () { + return gulp.src('build/dist/jquery.linkify.js') + .pipe(concat('linkify-benchmark.js')) + .pipe(wrap({src: 'templates/linkify-benchmark.js'})) + .pipe(uglify()) + .pipe(gulp.dest('build/benchmark')); +}); + +gulp.task('uglify', ['build', 'build-legacy'], function () { + return gulp.src(['build/*.js', 'build/dist/jquery.linkify.js']) .pipe(gulp.dest('dist')) // non-minified copy .pipe(rename(function (path) { path.extname = '.min.js'; diff --git a/templates/linkify-benchmark.js b/templates/linkify-benchmark.js new file mode 100644 index 00000000..b20d78c7 --- /dev/null +++ b/templates/linkify-benchmark.js @@ -0,0 +1,6 @@ +module.exports = function (jQuery, win, doc) { +win.jQuery = jQuery; +(function (window, document) { + <%= contents %> +})(win, doc); +}; diff --git a/templates/linkify-legacy.js b/templates/linkify-legacy.js new file mode 100644 index 00000000..ddb5e1a7 --- /dev/null +++ b/templates/linkify-legacy.js @@ -0,0 +1,2 @@ +;console.warn('dist/jquery.linkify.js is deprecated. Use linkify.js and linkify-jquery.js instead.'); +<%= contents %> From 179823afadaf280a0fa1faec29657ab5965ec9c1 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 3 Apr 2015 20:19:25 -0400 Subject: [PATCH 54/67] Added new `events` option to linkify-element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows you to add event listeners to links created with linkify-element and linkify-jquery. Example usage: ``` ‘github.com’.linkify({ events: { click: function () { alert(‘Clicked!’); } } }); ``` --- src/linkify-element.js | 13 ++++++++++++- src/linkify-jquery.js | 1 + src/linkify/utils/options.js | 1 + test/spec/html/options.js | 8 ++++++++ test/spec/linkify-element-test.js | 20 ++++++++++++++++++-- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/linkify-element.js b/src/linkify-element.js index 73d14006..bd82703e 100644 --- a/src/linkify-element.js +++ b/src/linkify-element.js @@ -28,7 +28,8 @@ function tokensToNodes(tokens, opts, doc) { formatted = options.resolve(opts.format, token.toString(), token.type), href = token.toHref(opts.defaultProtocol), formattedHref = options.resolve(opts.formatHref, href, token.type), - attributesHash = options.resolve(opts.attributes, token.type); + attributesHash = options.resolve(opts.attributes, token.type), + events = options.resolve(opts.events, token.type); // Build the link let link = doc.createElement(tagName); @@ -45,6 +46,16 @@ function tokensToNodes(tokens, opts, doc) { } } + if (events) { + for (let event in events) { + if (link.addEventListener) { + link.addEventListener(event, events[event]); + } else if (link.attachEvent) { + link.attachEvent('on' + event, events[event]); + } + } + } + link.appendChild(doc.createTextNode(formatted)); result.push(link); diff --git a/src/linkify-jquery.js b/src/linkify-jquery.js index 57eef812..de91398a 100644 --- a/src/linkify-jquery.js +++ b/src/linkify-jquery.js @@ -49,6 +49,7 @@ function apply($, doc=null) { options = { linkAttributes: data.linkifyAttributes, defaultProtocol: data.linkifyDefaultProtocol, + events: data.linkifyEvents, format: data.linkifyFormat, formatHref: data.linkifyFormatHref, newLine: data.linkifyNewline, // deprecated diff --git a/src/linkify/utils/options.js b/src/linkify/utils/options.js index c2a1f110..1815c093 100644 --- a/src/linkify/utils/options.js +++ b/src/linkify/utils/options.js @@ -12,6 +12,7 @@ function normalize(opts) { return { attributes: opts.linkAttributes || null, defaultProtocol: opts.defaultProtocol || 'http', + events: opts.events || null, format: opts.format || noop, formatHref: opts.formatHref || noop, newLine: opts.newLine || false, // deprecated diff --git a/test/spec/html/options.js b/test/spec/html/options.js index f33e2e5a..dd3a6c5e 100644 --- a/test/spec/html/options.js +++ b/test/spec/html/options.js @@ -9,6 +9,14 @@ module.exports = { altOptions: { linkAttributes: { rel: 'nofollow' + }, + events: { + click: function () { + throw 'Clicked!'; + }, + mouseover: function () { + throw 'Hovered!'; + } } } }; diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index aba7fb01..0490acd9 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -1,6 +1,6 @@ /*jshint -W030 */ var -doc, testContainer, jsdom, +doc, testContainer, jsdom, Ev, linkifyElement = require('../../lib/linkify-element'), htmlOptions = require('./html/options'); @@ -26,7 +26,11 @@ describe('linkify-element', function () { done(); } - if (doc) { return onDoc(doc); } + if (doc) { + Ev = window.Event; + return onDoc(doc); + } + // no document element, use a virtual dom to test jsdom = require('jsdom'); @@ -35,6 +39,7 @@ describe('linkify-element', function () { function (errors, window) { if (errors) { throw errors; } doc = window.document; + Ev = window.Event; return onDoc(window.document); } ); @@ -63,6 +68,17 @@ describe('linkify-element', function () { var result = linkifyElement(testContainer, htmlOptions.altOptions, doc); result.should.eql(testContainer); // should return the same element testContainer.innerHTML.should.eql(htmlOptions.linkifiedAlt); + + /* + // These don't work across all test suites :( + (function () { + testContainer.getElementsByTagName('a')[0].dispatchEvent(new Ev('click')); + }).should.throw('Clicked!'); + + (function () { + testContainer.getElementsByTagName('a')[0].dispatchEvent(new Ev('mouseover')); + }).should.throw('Hovered!'); + */ }); }); From 410a90e45430a591991147f96d8d3f2e842f3dd7 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 3 Apr 2015 20:30:52 -0400 Subject: [PATCH 55/67] Legacy build files now save into the correct folder --- gulpfile.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 5c8c332b..f413921c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -283,13 +283,23 @@ gulp.task('build-benchmark', ['build-legacy'], function () { }); gulp.task('uglify', ['build', 'build-legacy'], function () { - return gulp.src(['build/*.js', 'build/dist/jquery.linkify.js']) + var task = gulp.src('build/*.js') .pipe(gulp.dest('dist')) // non-minified copy .pipe(rename(function (path) { path.extname = '.min.js'; })) .pipe(uglify()) .pipe(gulp.dest('dist')); + + var taskLegacy = gulp.src('build/dist/jquery.linkify.js') + .pipe(gulp.dest('dist/dist')) // non-minified copy + .pipe(rename(function (path) { + path.extname = '.min.js'; + })) + .pipe(uglify()) + .pipe(gulp.dest('dist/dist')); + + return merge.apply(this, [task, taskLegacy]); }); gulp.task('dist', ['uglify']); From 8a40469adebf5dd654f83264234d477d52ac335f Mon Sep 17 00:00:00 2001 From: nfrasser Date: Fri, 3 Apr 2015 21:24:09 -0400 Subject: [PATCH 56/67] Removing old demo folder --- demo/apple-touch-icon-precomposed.png | Bin 8403 -> 0 bytes demo/css/main.css | 225 --------------- demo/favicon.ico | Bin 5430 -> 0 bytes demo/img/logo.png | Bin 17784 -> 0 bytes demo/img/logo@2x.png | Bin 36464 -> 0 bytes demo/index.html | 393 -------------------------- demo/js/main.js | 66 ----- 7 files changed, 684 deletions(-) delete mode 100644 demo/apple-touch-icon-precomposed.png delete mode 100644 demo/css/main.css delete mode 100644 demo/favicon.ico delete mode 100644 demo/img/logo.png delete mode 100644 demo/img/logo@2x.png delete mode 100644 demo/index.html delete mode 100644 demo/js/main.js diff --git a/demo/apple-touch-icon-precomposed.png b/demo/apple-touch-icon-precomposed.png deleted file mode 100644 index 6acba28f096da73e1566601e68baf66713decda4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8403 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 li > a { - font-weight: 300; -} - -.btn-xl { - padding: 15px 24px; - font-size: 27px; - line-height: 2; - border-radius: 9px; -} - -header, footer, .main-section-container { - background-color: #ffffff; - border: 3px solid #eee; - border-left: none; - border-right: none; -} - -header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1031; - text-align: center; - border-top: none; -} - -footer { - border-bottom: none; -} - -.table-responsive > .table > thead > tr > th, -.table-responsive > .table > tbody > tr > th, -.table-responsive > .table > tfoot > tr > th, -.table-responsive > .table > thead > tr > td, -.table-responsive > .table > tbody > tr > td, -.table-responsive > .table > tfoot > tr > td { - max-width: 300px; - white-space: normal; -} - -.page-title { - margin: 0; - font-size: 50px; - line-height: 2; - height: 0; - overflow: hidden; - - -webkit-transition: all 0.3s ease; - -moz-transition: all 0.3s ease; - -ms-transition: all 0.3s ease; - -o-transition: all 0.3s ease; - transition: all 0.3s ease; -} - -.page-title.active { - font-size: 90px; - line-height: 0.6; - padding-top: 30px; - height: 180px; -} - -.page-title > small { - font-size: 35%; -} - -.navbar-fixed-top { - top: 130px; - border-bottom: 3px solid #eee; - -webkit-transition: top 0.3s ease; - -moz-transition: top 0.3s ease; - -ms-transition: top 0.3s ease; - -o-transition: top 0.3s ease; - transition: top 0.3s ease; -} -.navbar-fixed-top.top { - top: 0; -} - -.main-section-container { - padding-top: 20px; - padding-bottom: 20px; - margin-top: 575px; -} - -.page-tagline { - font-size: 40px; - color: #aaaaaa; - text-align: center; -} - -.main { - padding-top: 30px; - padding-bottom: 30px; - border-bottom: 2px solid #e5e5e5; - background-color: #f5f5f5; -} - -.main:last-of-type { - border-bottom: none; -} - -.demo-well { - background: #ffffff; -} - -footer { - padding-top: 20px; - padding-bottom: 20px; - text-align: center; -} - -.prettyprint { - color: #535353; - background-color: #f8f8f8; -} - -.pln, -.typ, -.pun, -.opn, -.clo { - color: #535353; -} -.str, -.atv { - color: #f18900; -} -.kwd, -.tag { - color: #446fb0; -} -.com, -.dec, -.var, -.linenums { - color: #a2a2a2; -} -.lit, -.atn { - color: #738d00; -} -.fun { - color: #8757ad; -} - -.prettyprint .linenums li { - list-style-type: decimal !important; - word-break: break-word; - background-color: #f8f8f8; -} - -pre.prettyprint { - border-color: #dddddd; - position: relative; -} -pre.prettyprint:before { - content: attr(data-lang); - position: absolute; - right: 0; - top: 5px; - font-size: 10px; - height: 20px; - padding: 0 8px; - line-height: 2; - font-family: 'Oswald', sans-serif; - text-transform: uppercase; - font-weight: normal; - font-style: italic; - background-color: #e5e5e5; -} -@media (min-width: 768px) { - .page-title { - height: 100px; - } - .navbar-fixed-top.top { - top: 103px; - } -} - -@media only screen and (-webkit-min-device-pixel-ratio: 1.5), -only screen and (-o-min-device-pixel-ratio: 3/2), -only screen and (min--moz-device-pixel-ratio: 1.5), -only screen and (min-device-pixel-ratio: 1.5) { - body { - background: url(../img/logo@2x.png); - background-size: 650px 400px; - background-repeat:no-repeat; - background-position: center 175px; - background-attachment: fixed; - background-color: #f5f5f5; - } -} diff --git a/demo/favicon.ico b/demo/favicon.ico deleted file mode 100644 index a30f3ffa86b01055fb4a77b4a2ff8b3d624acc31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmds*ZD?Fo8pls|*3IrBPOJE#i*{NUigod=A{G&&h@jwyf^Q)9OB?8-h%Zp=;!9tm z*6La-f(QyK3Kl^Zk)$t4+DY1`Y11Zcnlyb&I!T)*X_|D>X_DLN+?@UWuXBdm>r4yo zeh>#P_ug}!=lMV9dHJ7n$Cxtn2QzDy;Xcc(pJvP)W6U(}djIDijcMiiX2uXP4{&Kb z_%R;+S!>O2?V6gJx`wOl|K%69uJV2V+AG;RdGh27&CSg{+qP}9yLa!N=er=z3e`^LsbZ~OM`wymvgU}$J)X|c5(J9ccfBy$zy~)YR#D)zUvci|Fu!%!} zzh}=L+uGVXp2=h$4C`F(Ih{`5i>*veO^p?PGMThZO-+GK;bYS$u77tnPMkQgMA!!o z9I(B;y_R>|*Vku{964h5?b|1L`H(q({`|d8r_>$nd%#RrS65rsulU(RhYs1phYtsL z@?rPx-D^*uKK-1tarEfXw>2-^=p}#l@89oyNRN+?-|h6G_p!0Dd0>wUyRNRz9z1w3 zz?JRrrEzL)wbSSM^C)ikJ?sPV7^SY}IbC;<^#pzdAkazCa)P>P7KM z9$)=;yOYtK7+u0bsx9 zY&q=kogRLjIQa5GI`Tmr!uwa$+icd+9mbYSXJ==?55%ntA7)ZR>m?(6acLeqOHC~a zW0iV_W1htaPd+#fln=Xh?V6NbakTO+yTmw%m*XgMJV~tu*e>7LIET$gOW_q8r#>pE zr4iKxds877vgvA#SiVorzLcN3oDW*FYFM>~?g?z&Q4HfsW5n`ia^Ec|?Ls12H&E-zJFb9BLw^ zJ<7#F{^_0kG{Hdy9w>!VY#hI*@7S@UMfq-MXt3%z`2eQ|>sK6zM;}~2q&m}n!#?&4 zAIL#Nm=B6!MaRI7vtKt0+x0DWa_~xqm^l*mvI^x`=oM^ujhWuks{Ra7R z>S{W8Tjhf|Li6f6U}uFd8|q-<`4RG*5c#FOeB#Y<}uK72YF)+_dmOpWlhM%fXU>f1qk4CAV?MaA?q z#@s4=@WoS#gM8O{Tk#h@ey=a3>&CT*vW9N?5WeVPoqZp2a;5In+#;|%;e{{q)kSa5 zD5V!3C#P}hcp3VeSx*D`{TRP)3d>yX8D74de5~ip)`4y#e6S3gzlw)1hx0p`V;R<_ zt&Jy_PsNfIxmYsc#a6`AWh=^4WmU0c@9LE+d#jSMhK5vmRWf}|EH(dza&Ow76FFll zri@u`%T`!jde)dX|As_**~~;b9gmMC%Rj_YJRM8M)A3Z?OL&Q?irjKx>ZkuH^tZ$; zuEPHm{D%?!AsU?j=(U)8zT6!-_2nh9)b;D^7nAV8AoW!^-$(6Gt53nhL-d{pzpd0f z=g!&ebH4Vp{5Pb|=fR#7)<^zndOoM`XZ0YxAAIZbP4kHMZr2myD*VXe$5T!p+$Z3; ztn^&(i3jNGN9n7D(q;dtr5EOM=J`=vBOBOWK&>y3t~djk@A7^8lkA|ske-+)ZosyW z?n{adeDJMo;H!tOCtZB_Qgd*~%Y0Ypd#UiP0M6R{8OTR&mE_Q`(YL#F@$W~*%GfL3 z(U@Wd)+JntFH4C-5J%32KZu{DtFsHS8i!+l(04mA*18JcKQ2bP!$VKPX%pgO=?a55 zuG3n?*>I))D|jrd6MjbaJ~%ZaUKFq5M>YFioovYlF-oIf^gAD)=Hp+QwfW?~iZvh6 z*-IQvY}%P$gAE^BqxgAe7+258b)lQ%TcDEfi5scMF4cX71?0#Ym4d= zmq}y~P-m6or%(J&?mrW^D~9ZQ$Kbrhtm`Oo{>6TS%&^v}ZzA?Mt>It7Aa4er)({I{ z{wR*rQGXO)djNJ@SZfD<2IsA?4CkIoeCxrdR@PS$p8MaPjcFS*W_7PIRSm{`w8ofE jXBhL=Y-3X8#-!svUwYnimod%P8MCd*n1KNUZy57mr{!SF diff --git a/demo/img/logo.png b/demo/img/logo.png deleted file mode 100644 index 457137661d2b3923721405634d83c2ad8468fc4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17784 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

;* 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 diff --git a/demo/img/logo@2x.png b/demo/img/logo@2x.png deleted file mode 100644 index 194f332cd52abad3306a97f48fa06260ce40dd09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36464 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 diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index a5db0278..00000000 --- a/demo/index.html +++ /dev/null @@ -1,393 +0,0 @@ - - - - - - - Linkify - a jQuery plugin by @SoapBoxHQ - - - - - - - - - - - - - - - -

-

- Linkify
- a jQuery plugin -

-
- - - -
-
-

- Intelligent URL recognition, made easy
-

-

- - - View GitHub project - -

-

- Download 1.1: - - Minified - - · - - Source - -

-
- -
- -
- -

- Linkify is a jQuery plugin for finding URLs in plain-text - and converting them to HTML links. It works with all valid - URLs and email addresses. -

- -

Demo

-
- -

1. Linkify some paragraphs

- -
-
-
-

- About a year ago Graham and I went to Google IO (https://developers.google.com/events/io/) to learn about some upcoming technology and meet some tech folk in the valley. The experience was great. We met a bunch of great people and got our hands on some new technology — check out this page for more on our experience http://digitalmediazone.ryerson.ca/toronto-incubator/brennans-experience-at-google-io/experience. Beyond everything else, the best thing we got out of that conference was a technology/development mentor & a new startup development process. - -

-

- As soapboxhq.com grew, we tweaked our development and deployment process as needed. At the very start we used cheap hosting providers such as ca.godaddy.com and learned to deal with their limitations. We knew there were other ways of doing things, but they seemed to add complex rules and process. This worked for us, so why fix it? -

-

- We then met Ian (http://iandouglas.com/about/) at Google IO, who agreed to share some of his insight from scaling over and over again. Ian is a senior web developer/architect working over at Sendgrid. Ian is awesome and we really take his advice to heart. He deserves the credit for a lot of what you see below (including the joke I shamelessly stole from him). - -

- -

- To see the rest of this post, visit http://soapboxhq.com/blog/startup-development-process-how-we-develop/ or email soapbox-dev-team@example.com. -

-
-
-
- -
-
- -
- -

2. Linkify won't break your existing HTML

-

Try linkifying this entire section

- -
-
-
-

Demonstrating a few HTML features

-

Originally from sheldonbrown.com/web_sample1.html

-

- HTML is really a very simple language. It consists of ordinary text, with commands that are enclosed by "<" and ">" characters, or between an "&" and a ";". -

-

- You don't really need to know much HTML to create a page, because you can copy bits of HTML from other pages (such as google.com that do what you want, then change the text! -

-

- This page shows on the left as it appears in your browser, and the corresponding HTML code appears on the right. The HTML commands are linked to explanations of what they do. w3.org also has some great info on HTML standards. -

- -

Line Breaks

-

- HTML doesn't normally use line breaks (http://en.wikipedia.org/wiki/Newline) for ordinary text. A white space of any size is treated as a single space. This is because the author of the page has no way of knowing the size of the reader's screen, or what size type they will have their browser set for. -

-

- If you want to put a line break at a particular place, you can use the "<br>" command, or, for a paragraph break, the "<p>" command, which will insert a blank line. The heading command ("<h4></h4>") puts a blank line above and below the heading text. Learn more about headings here: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements. -

- -

This is a size "1" heading

-

This is a size "2" heading

-

This is a size "3" heading

-

This is a size "4" heading

-

This is a size "5" heading

-

This is a size "6" heading

- -
- -

- - Check out this site for more! - -

-
-
- -
- -
-
-
- - -

3. See if this plugin works for your URL

- -
-
- -
-
- -
-
- -
- -
Output
-
- - Press "Linkify" to view the result - -
- -

Examples

-
- -

Basic Usage

-

- To detect links within any set of elements, just call $(selector).linkify() on document load. -

- -
Code
- -
-<p id="paragraph1">Check out this link to http://google.com</p>
-<p id="paragraph2">You can also email support@example.com to view more.</p>
-...
-<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
-<script src="js/jquery.linkify.min.js"></script>
-
- -
-$(window).on('load', function () {
-  $('p').linkify();
-});
-
- -
Output
- -
-<p id="paragraph1">
-  Check out this link to
-  <a href="http://google.com" class="linkified" target="_blank">
-    http://google.com
-  </a>
-</p>
-<p id="paragraph2">
-  You can also email
-  <a href="mailto:support@example.com" class="linkified">
-    support@example.com
-  </a>
-  to view more.
-</p>
-
- -
- -

Usage via HTML attributes

-

- Linkify also provides a DOM data- API. - The following code will find links in the - #linkify-example paragraph element: -

- -
-<p id="linkify-example" data-linkify="this">
-  Lorem ipsum dolor sit amet, consectetur adipisicing
-  elit, sed do eiusmod tempor incididunt ut labore et
-  dolore magna aliqua.
-</p>
-
- -

- Specify a selector instead of this to instead linkify every element with that selector. The example below linkifies every paragraph and .plain-text element in the bodytag: -

- -
-<body data-linkify="p, .plain-text">
-  ...
-</body>
-
- -

Options

-

- Linkify is applied with the following default options. Below is a - description of each. -

- -
-$('selector').linkify({
-  tagName: 'a',
-  target: '_blank',
-  newLine: '\n',
-  linkClass: null,
-  linkAttributes: null
-});
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OptionTypeDefaultDescription - Data Attribute (used on the same element as - data-linkify) -
tagNameString"a" - The tag that should be used to wrap each URL. This is - useful for cases where a tags might be - innapropriate, or might syntax problems (e.g., finding - URLs inside an a tag). - - data-linkify-tagname -
targetString"_blank"target attribute for each linkified tag.data-linkify-target
newLineString"\n" - The character to replace new lines with. Replace with - "<br>" to space out multi-line user - content. - data-linkify-newline
linkClassStringnull - The class to be added to each linkified tag. An extra .linkified class ensures that each link will be clickable, regardless of the value of tagName. Linkify won't attempt finding links in .linkified elements. - data-linkify-linkclass
linkAttributesObjectnull - HTML attributes to add to each linkified tag. In the - following example, the tabindex and - rel attributes will be added to each link. - -
-$('p').linkify({
-  linkAttributes: {
-    tabindex: 0,
-    rel: 'nofollow'
-  }
-});
-
- -
N/A
-
-
- - - - - - - - - - - - - diff --git a/demo/js/main.js b/demo/js/main.js deleted file mode 100644 index d46949fe..00000000 --- a/demo/js/main.js +++ /dev/null @@ -1,66 +0,0 @@ -(function ($) { - - var headerThreshold = 422; - - $(window).on('scroll', function () { - - var $this = $(this), - isActive = !!$this.data('isHeaderActive'), - scroll = $(this).scrollTop(); - - if (isActive && scroll >= headerThreshold) { - $('#page-title').removeClass('active'); - $('#navbar').addClass('top'); - $this.data('isHeaderActive', false); - } else if (!isActive && scroll < headerThreshold) { - $('#page-title').addClass('active'); - $('#navbar').removeClass('top'); - $this.data('isHeaderActive', true); - } - }); - - $(window).on('load', function() { - - $('.linkifier').text('Linkify') - .on('click', function () { - - var $this = $(this), - $targets, - id = $this.attr('id'); - - if (!id) { - return; - } - - $targets = $('[data-linkify-demo-target="' + id + '"]'); - $targets.linkify(); - - - $this.prop('disabled', true) - .text('Linkified!') - .off('click'); - - }); - - // Set the value of the textfield to the referrer URL - $('#demo-3-input').val( - 'Linkify the following URL: ' + ( - document.referrer || 'https://github.com/SoapBox/jQuery-linkify/' - ) - ); - - $('#demo-3-linkifier').on('click', function () { - var $this = $(this), - $input = $($this.attr('data-linkify-demo-input')), - $output = $($this.attr('data-linkify-demo-output')); - - $output.html($input.val()); - $output.linkify({newLine: '
\n'}); - }); - - prettyPrint(); - $(window).trigger('scroll'); - - }); - -})(jQuery); From c653bcb4a7f50eaf940139ba40cf122a903f1798 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 31 May 2015 01:51:03 -0700 Subject: [PATCH 57/67] Tweaking dev files for Saucelabs CI Including Travis CI and Karma config. Saucelabs account is nfrasser. Currently not working. The tests don't seem to be running at all --- .travis.yml | 5 ++++- gulpfile.js | 2 +- test/ci.conf.js | 16 ++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29458440..42bfe57c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,9 @@ node_js: - "0.12" - "0.10" - "iojs" - +env: + global: + - secure: LhH+mMqOktTe6cIt97PGKBfgUjZM8vRd0qddyg61FSxg7a3WrHQoHE8WdRioJ9+DDzpu/NSTsHEUFUpGN+kSRw1UY4tsNLH6HoBQnqrNN4tVOeefudJpdeteOKZrJ8r8TaA/eO7sAgXO2T+RLJ8+qTbhx8FVZtLaCAgkrS0w9Qk= + - secure: Okwm1aAR3oo09AhHDsjFSq1UGlIUtWYYvYeoolJScC/UVFGMiK9oC4fzRtUHv3kXcnshDlcVDrr/Q5JL9Qx6E+tosPJp+tioaqE8X4IDbVk7PPs/ToOOEmWnGvxkgmfCGSDuneG8RVhILkhls3fbm0z+rRWlvJkjefeA96T6zps= script: npm test after_script: npm run coverage diff --git a/gulpfile.js b/gulpfile.js index f413921c..dbe2cd3a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -304,7 +304,7 @@ gulp.task('uglify', ['build', 'build-legacy'], function () { gulp.task('dist', ['uglify']); gulp.task('test', ['build', 'jshint', 'mocha']); -gulp.task('test-ci', ['build', 'karma-ci']); +gulp.task('test-ci', ['karma-ci']); // Using with other tasks causes an error here for some reason /** diff --git a/test/ci.conf.js b/test/ci.conf.js index 11dd45e9..f07581c3 100644 --- a/test/ci.conf.js +++ b/test/ci.conf.js @@ -11,7 +11,6 @@ module.exports = function (config) { sl_chrome: { base: 'SauceLabs', browserName: 'chrome', - platform: 'Windows 7', version: '35' }, sl_firefox: { @@ -30,6 +29,12 @@ module.exports = function (config) { browserName: 'internet explorer', platform: 'Windows 8.1', version: '11' + }, + sl_ie_8: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 7', + version: '8' } }; @@ -40,11 +45,10 @@ module.exports = function (config) { sauceLabs: { testName: 'Linkify Browser Tests' }, - - logLevel: config.LOG_WARN, - browsers: ['Chrome'], + customLaunchers: customLaunchers, + browsers: Object.keys(customLaunchers), autoWatch: false, - singleRun: true - + singleRun: true, + logLevel: config.LOG_WARN })); }; From 7a1f03317c276932279f90dfdb248f5276f4c942 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sun, 31 May 2015 02:07:26 -0700 Subject: [PATCH 58/67] Refactoring linkify-test tests for maintainability - One assertion per example - beforeEach should set up each example uniquely - cleanup for each example in afterEach Will eventually refactor all tests in this fashion --- test/spec/linkify-test.js | 115 +++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 44 deletions(-) diff --git a/test/spec/linkify-test.js b/test/spec/linkify-test.js index 04a88cf8..96cac69b 100644 --- a/test/spec/linkify-test.js +++ b/test/spec/linkify-test.js @@ -21,60 +21,87 @@ var tokensTest = [ ]; describe('linkify', function () { - it('Has all required methods and properties', function () { - - // Functions - linkify.tokenize.should.be.a('function'); - linkify.tokenize.length.should.be.eql(1); - linkify.find.should.be.a('function'); - linkify.find.length.should.be.eql(1); // type is optional - linkify.test.should.be.a('function'); - linkify.test.length.should.be.eql(1); // type is optional - - // Properties - linkify.options.should.be.a('object'); - linkify.parser.should.be.a('object'); - linkify.scanner.should.be.a('object'); + describe('tokenize', function () { + it('is a function', function () { + linkify.tokenize.should.be.a('function'); + }); + it('takes a single argument', function () { + linkify.tokenize.length.should.be.eql(1); + }); }); -}); -describe('linkify.tokenize', function () { -}); - -describe('linkify.find', function () { + describe('find', function () { + it('is a function', function () { + linkify.find.should.be.a('function'); + }); + it('takes a single argument', function () { + linkify.find.length.should.be.eql(1); // type is optional + }); + }); -}); + describe('test', function () { + /* + For each element, -describe('linkify.test', function () { - /* - For each element, + * [0] is the input string + * [1] is the expected return value + * [2] (optional) the type of link to look for + */ + var tests = [ + ['Herp derp', false], + ['Herp derp', false, 'email'], + ['Herp derp', false, 'asdf'], + ['https://google.com/?q=yey', true], + ['https://google.com/?q=yey', true, 'url'], + ['https://google.com/?q=yey', false, 'email'], + ['test+4@uwaterloo.ca', true], + ['test+4@uwaterloo.ca', false, 'url'], + ['test+4@uwaterloo.ca', true, 'email'], + ['t.co', true], + ['t.co g.co', false], // can only be one + ['test@g.co t.co', false] // can only be one + ]; - * [0] is the input string - * [1] is the expected return value - * [2] (optional) the type of link to look for - */ - var tests = [ - ['Herp derp', false], - ['Herp derp', false, 'email'], - ['Herp derp', false, 'asdf'], - ['https://google.com/?q=yey', true], - ['https://google.com/?q=yey', true, 'url'], - ['https://google.com/?q=yey', false, 'email'], - ['test+4@uwaterloo.ca', true], - ['test+4@uwaterloo.ca', false, 'url'], - ['test+4@uwaterloo.ca', true, 'email'], - ['t.co', true], - ['t.co g.co', false], // can only be one - ['test@g.co t.co', false] // can only be one - ]; + it('is a function', function () { + linkify.test.should.be.a('function'); + }); + it('takes a single argument', function () { + linkify.test.length.should.be.eql(1); // type is optional + }); - it('Correctly tests each example string', function () { - var test; + var test, testName; + /* jshint loopfunc: true */ for (var i = 0; i < tests.length; i++) { test = tests[i]; - linkify.test(test[0], test[2]).should.be.eql(test[1]); + testName = 'Correctly tests the string "' + test[0] + '"'; + testName += ' as `' + (test[1] ? 'true' : 'false') + '`'; + if (test[2]) { + testName += ' (' + test[2] + ')'; + } + testName += '.'; + + it(testName, function () { + linkify.test(test[0], test[2]).should.be.eql(test[1]); + }); } }); + describe('options', function () { + it('is an object', function () { + linkify.options.should.be.a('object'); + }); + }); + + describe('parser', function () { + it('is an object', function () { + linkify.parser.should.be.a('object'); + }); + }); + + describe('scanner', function () { + it('is an object', function () { + linkify.scanner.should.be.a('object'); + }); + }); }); From 2542d02ed8d06af6d8169c6ff66ae707f1b2f119 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 01:05:22 -0700 Subject: [PATCH 59/67] Updated test names, browser compatibility The tests should now all pass on Chrome, Firefox, and the latest IE. Also includes full Saucelabs integration! --- .travis.yml | 4 +++- package.json | 10 ++++++---- src/linkify-jquery.js | 6 ++++-- test/ci.conf.js | 10 ++-------- test/firefox.conf.js | 17 +++++++++++++++++ test/spec/html/extra.html | 2 +- test/spec/html/linkified-alt.html | 2 ++ test/spec/html/linkified.html | 3 +++ test/spec/html/options.js | 13 +++++++++++-- test/spec/linkify-element-test.js | 8 ++++---- test/spec/linkify-jquery-test.js | 12 +++++++++--- test/spec/linkify/core/parser-test.js | 2 +- test/spec/linkify/core/scanner-test.js | 2 +- test/spec/linkify/core/state/character-test.js | 2 +- test/spec/linkify/core/state/stateify-test.js | 2 +- test/spec/linkify/core/state/token-test.js | 2 +- test/spec/linkify/core/tokens/multi-test.js | 2 +- test/spec/linkify/core/tokens/text-test.js | 2 +- test/spec/linkify/plugins/hashtag-test.js | 2 +- 19 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 test/firefox.conf.js diff --git a/.travis.yml b/.travis.yml index 42bfe57c..06c68e19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,7 @@ env: global: - secure: LhH+mMqOktTe6cIt97PGKBfgUjZM8vRd0qddyg61FSxg7a3WrHQoHE8WdRioJ9+DDzpu/NSTsHEUFUpGN+kSRw1UY4tsNLH6HoBQnqrNN4tVOeefudJpdeteOKZrJ8r8TaA/eO7sAgXO2T+RLJ8+qTbhx8FVZtLaCAgkrS0w9Qk= - secure: Okwm1aAR3oo09AhHDsjFSq1UGlIUtWYYvYeoolJScC/UVFGMiK9oC4fzRtUHv3kXcnshDlcVDrr/Q5JL9Qx6E+tosPJp+tioaqE8X4IDbVk7PPs/ToOOEmWnGvxkgmfCGSDuneG8RVhILkhls3fbm0z+rRWlvJkjefeA96T6zps= -script: npm test +script: + - npm test + - npm run test-ci after_script: npm run coverage diff --git a/package.json b/package.json index 6a1a0f8e..fec78f61 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,14 @@ "version": "2.0.0-alpha.4", "description": "Intelligent URL recognition, made easy", "repository": { - "type" : "git", - "url" : "https://github.com/SoapBox/jQuery-linkify.git" + "type": "git", + "url": "https://github.com/SoapBox/jQuery-linkify.git" }, "main": "index.js", "scripts": { "prepublish": "rm -rf lib/* && node_modules/.bin/gulp build", "test": "node_modules/.bin/gulp coverage", + "test-ci": "node_modules/.bin/gulp test-ci", "coverage": "./node_modules/coveralls/bin/coveralls.js < coverage/lcov.info" }, "author": "SoapBox Innovations (@SoapBoxHQ)", @@ -28,7 +29,7 @@ "gulp-concat": "^2.5.2", "gulp-istanbul": "^0.6.0", "gulp-jshint": "^1.9.2", - "gulp-mocha": "^2.0.0", + "gulp-mocha": "^2.1.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.3", "gulp-uglify": "^1.1.0", @@ -38,10 +39,11 @@ "karma": "^0.12.32", "karma-browserify": "^4.0.0", "karma-chrome-launcher": "^0.1.7", + "karma-firefox-launcher": "^0.1.6", "karma-mocha": "^0.1.10", "karma-phantomjs-launcher": "^0.1.4", "karma-sauce-launcher": "^0.2.10", - "lodash": "^3.5.0", + "lodash": "^3.9.3", "merge-stream": "^0.1.7", "mocha": "^2.2.1" }, diff --git a/src/linkify-jquery.js b/src/linkify-jquery.js index de91398a..e9d82083 100644 --- a/src/linkify-jquery.js +++ b/src/linkify-jquery.js @@ -42,10 +42,12 @@ function apply($, doc=null) { $(doc).ready(function () { $('[data-linkify]').each(function () { + let $this = $(this), data = $this.data(), target = data.linkify, + nl2br = data.linkifyNlbr, options = { linkAttributes: data.linkifyAttributes, defaultProtocol: data.linkifyDefaultProtocol, @@ -53,7 +55,7 @@ function apply($, doc=null) { format: data.linkifyFormat, formatHref: data.linkifyFormatHref, newLine: data.linkifyNewline, // deprecated - nl2br: !!data.linkifyNlbr, + nl2br: !!nl2br && nl2br !== 0 && nl2br !== 'false', tagName: data.linkifyTagname, target: data.linkifyTarget, linkClass: data.linkifyLinkclass, @@ -65,7 +67,7 @@ function apply($, doc=null) { } // Apply it right away if possible -if (typeof __karma__ === 'undefined' && typeof jQuery !== 'undefined' && doc) { +if (typeof jQuery !== 'undefined' && doc) { apply(jQuery, doc); } diff --git a/test/ci.conf.js b/test/ci.conf.js index f07581c3..3fc84370 100644 --- a/test/ci.conf.js +++ b/test/ci.conf.js @@ -18,24 +18,18 @@ module.exports = function (config) { browserName: 'firefox', version: '30' }, - sl_ios_safari: { - base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.9', - version: '7.1' - }, sl_ie_11: { base: 'SauceLabs', browserName: 'internet explorer', platform: 'Windows 8.1', version: '11' - }, + }/*, sl_ie_8: { base: 'SauceLabs', browserName: 'internet explorer', platform: 'Windows 7', version: '8' - } + }*/ }; config.set(extend(base, { diff --git a/test/firefox.conf.js b/test/firefox.conf.js new file mode 100644 index 00000000..b8018e18 --- /dev/null +++ b/test/firefox.conf.js @@ -0,0 +1,17 @@ +// Karma Chrome configuration +// Just opens Google Chrome for testing + +var +base = require('./conf'), +extend = require('lodash').extend; + +module.exports = function (config) { + + config.set(extend(base, { + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + browsers: ['Firefox'] + })); +}; diff --git a/test/spec/html/extra.html b/test/spec/html/extra.html index 5ee8b305..cc5792e5 100644 --- a/test/spec/html/extra.html +++ b/test/spec/html/extra.html @@ -1,2 +1,2 @@ -
Have a link to: +
Have a link to: github.com!
Another test@gmail.com email as well as a http://t.co link.
diff --git a/test/spec/html/linkified-alt.html b/test/spec/html/linkified-alt.html index f70f148f..f604f684 100644 --- a/test/spec/html/linkified-alt.html +++ b/test/spec/html/linkified-alt.html @@ -1 +1,3 @@ Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

+Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

+Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

diff --git a/test/spec/html/linkified.html b/test/spec/html/linkified.html index 80b45f33..911afd6b 100644 --- a/test/spec/html/linkified.html +++ b/test/spec/html/linkified.html @@ -1 +1,4 @@ Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

+Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

+Hello here are some links to ftp://awesome.com/?where=this and localhost:8080, pretty neat right?

Here's a nested github.com/SoapBox/linkifyjs paragraph

+ diff --git a/test/spec/html/options.js b/test/spec/html/options.js index dd3a6c5e..4a411488 100644 --- a/test/spec/html/options.js +++ b/test/spec/html/options.js @@ -3,8 +3,17 @@ var fs = require('fs'); module.exports = { original: fs.readFileSync(__dirname + '/original.html', 'utf8').trim(), - linkified: fs.readFileSync(__dirname + '/linkified.html', 'utf8').trim(), - linkifiedAlt: fs.readFileSync(__dirname + '/linkified-alt.html', 'utf8').trim(), + + // These are split into arrays by line, where each line represents a + // different attribute ordering (based on the rendering engine) + // Each line is semantically identical. + linkified: fs.readFileSync(__dirname + '/linkified.html', 'utf8') + .split('\n') + .map(function (line) { return line.trim(); }), + linkifiedAlt: fs.readFileSync(__dirname + '/linkified-alt.html', 'utf8') + .split('\n') + .map(function (line) { return line.trim(); }), + extra: fs.readFileSync(__dirname + '/extra.html', 'utf8').trim(), // for jQuery plugin tests altOptions: { linkAttributes: { diff --git a/test/spec/linkify-element-test.js b/test/spec/linkify-element-test.js index 0490acd9..6db89918 100644 --- a/test/spec/linkify-element-test.js +++ b/test/spec/linkify-element-test.js @@ -58,16 +58,16 @@ describe('linkify-element', function () { (testContainer).should.be.okay; testContainer.should.be.a('object'); var result = linkifyElement(testContainer, null, doc); - result.should.eql(testContainer); // should return the same element - testContainer.innerHTML.should.eql(htmlOptions.linkified); + result.should.equal(testContainer); // should return the same element + htmlOptions.linkified.should.include(testContainer.innerHTML); }); it('Works with overriden options', function () { (testContainer).should.be.okay; testContainer.should.be.a('object'); var result = linkifyElement(testContainer, htmlOptions.altOptions, doc); - result.should.eql(testContainer); // should return the same element - testContainer.innerHTML.should.eql(htmlOptions.linkifiedAlt); + result.should.equal(testContainer); // should return the same element + htmlOptions.linkifiedAlt.should.include(testContainer.innerHTML); /* // These don't work across all test suites :( diff --git a/test/spec/linkify-jquery-test.js b/test/spec/linkify-jquery-test.js index 9d895e16..6ed7402f 100644 --- a/test/spec/linkify-jquery-test.js +++ b/test/spec/linkify-jquery-test.js @@ -14,6 +14,9 @@ try { describe('linkify-jquery', function () { + // Sometimes jQuery is slow to load + this.timeout(10000); + /** Set up the JavaScript document and the element for it This code allows testing on Node.js and on Browser environments @@ -56,7 +59,8 @@ describe('linkify-jquery', function () { testContainer.innerHTML = htmlOptions.original; }); - it('Works with the DOM Data API', function () { + // This works but is inconsisten across browsers + xit('Works with the DOM Data API', function () { $('header').first().html().should.be.eql( 'Have a link to:
github.com!' ); @@ -72,15 +76,17 @@ describe('linkify-jquery', function () { var $container = $('#linkify-jquery-test-container'); ($container.length).should.be.eql(1); var result = $container.linkify(); + // `should` is not defined on jQuery objects (result === $container).should.be.true; // should return the same element - $container.html().should.eql(htmlOptions.linkified); + htmlOptions.linkified.should.include($container.html()); }); it('Works with overriden options', function () { var $container = $('#linkify-jquery-test-container'); ($container.length).should.be.eql(1); var result = $container.linkify(htmlOptions.altOptions); + // `should` is not defined on jQuery objects (result === $container).should.be.true; // should return the same element - $container.html().should.eql(htmlOptions.linkifiedAlt); + htmlOptions.linkifiedAlt.should.include($container.html()); }); }); diff --git a/test/spec/linkify/core/parser-test.js b/test/spec/linkify/core/parser-test.js index 3f57c3f9..01ce0e35 100644 --- a/test/spec/linkify/core/parser-test.js +++ b/test/spec/linkify/core/parser-test.js @@ -94,7 +94,7 @@ var tests = [ // END: New linkify tests ]; -describe('parser#run()', function () { +describe('linkify/core/parser#run()', function () { function makeTest(test) { return it('Tokenizes the string "' + test[0] + '"', function () { diff --git a/test/spec/linkify/core/scanner-test.js b/test/spec/linkify/core/scanner-test.js index b6dcec96..0327bb6e 100644 --- a/test/spec/linkify/core/scanner-test.js +++ b/test/spec/linkify/core/scanner-test.js @@ -62,7 +62,7 @@ var tests = [ ['123-456', [DOMAIN], ['123-456']] ]; -describe('scanner#run()', function () { +describe('linkify/core/scanner#run()', function () { function makeTest(test) { return it('Tokenizes the string "' + test[0] + '"', function () { diff --git a/test/spec/linkify/core/state/character-test.js b/test/spec/linkify/core/state/character-test.js index 14a574d0..0f3ed77c 100644 --- a/test/spec/linkify/core/state/character-test.js +++ b/test/spec/linkify/core/state/character-test.js @@ -3,7 +3,7 @@ var TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, CharacterState = require('../../../../../lib/linkify/core/state').CharacterState; -describe('CharacterState', function () { +describe('linkify/core/state/CharacterState', function () { var S_START, S_DOT, S_NUM; before(function () { diff --git a/test/spec/linkify/core/state/stateify-test.js b/test/spec/linkify/core/state/stateify-test.js index b4f19824..c8ff89e1 100644 --- a/test/spec/linkify/core/state/stateify-test.js +++ b/test/spec/linkify/core/state/stateify-test.js @@ -3,7 +3,7 @@ TOKENS = require('../../../../../lib/linkify/core/tokens').text, State = require('../../../../../lib/linkify/core/state').CharacterState, stateify = require('../../../../../lib/linkify/core/state').stateify; -describe('stateify', function () { +describe('linkify/core/state/stateify', function () { var S_START; before(function () { diff --git a/test/spec/linkify/core/state/token-test.js b/test/spec/linkify/core/state/token-test.js index eb8a20e4..d49073d8 100644 --- a/test/spec/linkify/core/state/token-test.js +++ b/test/spec/linkify/core/state/token-test.js @@ -3,7 +3,7 @@ var TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, TokenState = require('../../../../../lib/linkify/core/state').TokenState; -describe('TokenState', function () { +describe('linkify/core/state/TokenState', function () { var TS_START; before(function () { diff --git a/test/spec/linkify/core/tokens/multi-test.js b/test/spec/linkify/core/tokens/multi-test.js index df14d7f1..73db3476 100644 --- a/test/spec/linkify/core/tokens/multi-test.js +++ b/test/spec/linkify/core/tokens/multi-test.js @@ -3,7 +3,7 @@ var TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text, MULTI_TOKENS = require('../../../../../lib/linkify/core/tokens').multi; -describe('MULTI_TOKENS', function () { +describe('linkify/core/tokens/MULTI_TOKENS', function () { describe('URL', function () { var diff --git a/test/spec/linkify/core/tokens/text-test.js b/test/spec/linkify/core/tokens/text-test.js index 7d6ee278..f84fc7f4 100644 --- a/test/spec/linkify/core/tokens/text-test.js +++ b/test/spec/linkify/core/tokens/text-test.js @@ -1,6 +1,6 @@ var TEXT_TOKENS = require('../../../../../lib/linkify/core/tokens').text; -describe('TEXT_TOKENS', function () { +describe('linkify/core/tokens#TEXT_TOKENS', function () { // Test for two commonly-used tokens diff --git a/test/spec/linkify/plugins/hashtag-test.js b/test/spec/linkify/plugins/hashtag-test.js index 28999a66..79f74928 100644 --- a/test/spec/linkify/plugins/hashtag-test.js +++ b/test/spec/linkify/plugins/hashtag-test.js @@ -3,7 +3,7 @@ var linkify = require('../../../../lib/linkify'), hashtag = require('../../../../lib/linkify/plugins/hashtag'); -describe('Linkify Hashtag Plugin', function () { +describe('linkify/plugins/hashtag', function () { it('Cannot parse hashtags before applying the plugin', function () { linkify.find('There is a #hashtag #YOLO-2015 and #1234 and #%^&*( should not work') From d1f7046aa3f3ca6a444afd0f6ab60f0374f998ea Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 17:49:39 -0700 Subject: [PATCH 60/67] Alternate test script so complete suite is not always run The CI SauceLabs tests should only run once a branch has been merged. --- .travis.yml | 4 +--- test/run.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100755 test/run.sh diff --git a/.travis.yml b/.travis.yml index 06c68e19..3111c9e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,5 @@ env: global: - secure: LhH+mMqOktTe6cIt97PGKBfgUjZM8vRd0qddyg61FSxg7a3WrHQoHE8WdRioJ9+DDzpu/NSTsHEUFUpGN+kSRw1UY4tsNLH6HoBQnqrNN4tVOeefudJpdeteOKZrJ8r8TaA/eO7sAgXO2T+RLJ8+qTbhx8FVZtLaCAgkrS0w9Qk= - secure: Okwm1aAR3oo09AhHDsjFSq1UGlIUtWYYvYeoolJScC/UVFGMiK9oC4fzRtUHv3kXcnshDlcVDrr/Q5JL9Qx6E+tosPJp+tioaqE8X4IDbVk7PPs/ToOOEmWnGvxkgmfCGSDuneG8RVhILkhls3fbm0z+rRWlvJkjefeA96T6zps= -script: - - npm test - - npm run test-ci +script: ./test/run after_script: npm run coverage diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 00000000..4b1a9e2c --- /dev/null +++ b/test/run.sh @@ -0,0 +1,12 @@ +cd $(dirname "${BASH_SOURCE[0]}../") + +if [[ `echo $TRAVIS_BRANCH` = "master" ]]; then + # Run basic and SauceLabs tests + echo "Running complete test suite..." + npm test || exit 1 + npm run test-ci || exit 1 +else + # Run basic tests + echo "Running basic tests..." + npm test || exit 1 +fi From 833bd594220ad75dfcda91fc911d81b2b8aa46d9 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 17:54:55 -0700 Subject: [PATCH 61/67] Fixed test run script filename in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3111c9e5..27d4939b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ env: global: - secure: LhH+mMqOktTe6cIt97PGKBfgUjZM8vRd0qddyg61FSxg7a3WrHQoHE8WdRioJ9+DDzpu/NSTsHEUFUpGN+kSRw1UY4tsNLH6HoBQnqrNN4tVOeefudJpdeteOKZrJ8r8TaA/eO7sAgXO2T+RLJ8+qTbhx8FVZtLaCAgkrS0w9Qk= - secure: Okwm1aAR3oo09AhHDsjFSq1UGlIUtWYYvYeoolJScC/UVFGMiK9oC4fzRtUHv3kXcnshDlcVDrr/Q5JL9Qx6E+tosPJp+tioaqE8X4IDbVk7PPs/ToOOEmWnGvxkgmfCGSDuneG8RVhILkhls3fbm0z+rRWlvJkjefeA96T6zps= -script: ./test/run +script: ./test/run.sh after_script: npm run coverage From 6065e5142a0fc162c81a692994d1e88e865057b6 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 18:01:55 -0700 Subject: [PATCH 62/67] Moved test runner file for better node compatibility node 0.10 seems to be having trouble running the script from the correct directory --- .npmignore | 1 + .travis.yml | 2 +- test/run.sh => run-tests.sh | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) rename test/run.sh => run-tests.sh (86%) diff --git a/.npmignore b/.npmignore index ad4a0eec..0c0fd4ca 100644 --- a/.npmignore +++ b/.npmignore @@ -17,3 +17,4 @@ test bower.json gulpfile.js testem.json +run-tests.sh diff --git a/.travis.yml b/.travis.yml index 27d4939b..957940d3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ env: global: - secure: LhH+mMqOktTe6cIt97PGKBfgUjZM8vRd0qddyg61FSxg7a3WrHQoHE8WdRioJ9+DDzpu/NSTsHEUFUpGN+kSRw1UY4tsNLH6HoBQnqrNN4tVOeefudJpdeteOKZrJ8r8TaA/eO7sAgXO2T+RLJ8+qTbhx8FVZtLaCAgkrS0w9Qk= - secure: Okwm1aAR3oo09AhHDsjFSq1UGlIUtWYYvYeoolJScC/UVFGMiK9oC4fzRtUHv3kXcnshDlcVDrr/Q5JL9Qx6E+tosPJp+tioaqE8X4IDbVk7PPs/ToOOEmWnGvxkgmfCGSDuneG8RVhILkhls3fbm0z+rRWlvJkjefeA96T6zps= -script: ./test/run.sh +script: ./run-tests.sh after_script: npm run coverage diff --git a/test/run.sh b/run-tests.sh similarity index 86% rename from test/run.sh rename to run-tests.sh index 4b1a9e2c..b94727b4 100755 --- a/test/run.sh +++ b/run-tests.sh @@ -1,5 +1,3 @@ -cd $(dirname "${BASH_SOURCE[0]}../") - if [[ `echo $TRAVIS_BRANCH` = "master" ]]; then # Run basic and SauceLabs tests echo "Running complete test suite..." From 508443ab975165b5f3f64d6b0e37d385fe5a2406 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 19:22:30 -0700 Subject: [PATCH 63/67] Updating .npmignore, better legacy template --- .npmignore | 4 ++++ templates/linkify-legacy.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 0c0fd4ca..28c2981c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,7 @@ # All compiled code will be in the `lib` folders +.sass-cache +_sass +_site amd assets bower_components @@ -6,6 +9,7 @@ build coverage demo dist +js src templates test diff --git a/templates/linkify-legacy.js b/templates/linkify-legacy.js index ddb5e1a7..fec99b83 100644 --- a/templates/linkify-legacy.js +++ b/templates/linkify-legacy.js @@ -1,2 +1,2 @@ -;console.warn('dist/jquery.linkify.js is deprecated. Use linkify.js and linkify-jquery.js instead.'); +;typeof console !== 'undefined' && console.warn('dist/jquery.linkify.js is deprecated. Use linkify.js and linkify-jquery.js instead.'); <%= contents %> From b0dbdfca9cad38e15beee99457e5402716b73cd9 Mon Sep 17 00:00:00 2001 From: nfrasser Date: Sat, 6 Jun 2015 21:57:24 -0700 Subject: [PATCH 64/67] Full IE 9+, partial IE8 support Had to rewrite all the test files to use expect assertions instead of should to get this even working on IE8. Seems solid though! --- .jshintrc | 1 + gulpfile.js | 8 +- package.json | 4 +- src/linkify.js | 6 + src/linkify/core/state.js | 2 +- src/linkify/core/tokens.js | 28 +--- test/ci.conf.js | 12 ++ test/conf.js | 2 + test/index.html | 2 +- test/init.js | 8 +- test/shim.js | 149 ++++++++++++++++++ test/spec/html/options.js | 8 +- test/spec/linkify-element-test.js | 18 +-- test/spec/linkify-jquery-test.js | 60 ++++++- test/spec/linkify-string-test.js | 8 +- test/spec/linkify-test.js | 24 +-- test/spec/linkify/core/parser-test.js | 12 +- test/spec/linkify/core/scanner-test.js | 12 +- .../spec/linkify/core/state/character-test.js | 30 ++-- test/spec/linkify/core/state/stateify-test.js | 42 ++--- test/spec/linkify/core/state/token-test.js | 16 -- test/spec/linkify/core/tokens/multi-test.js | 76 +++------ test/spec/linkify/core/tokens/text-test.js | 4 +- test/spec/linkify/plugins/hashtag-test.js | 16 +- 24 files changed, 355 insertions(+), 193 deletions(-) create mode 100644 test/shim.js diff --git a/.jshintrc b/.jshintrc index 4ae6cedf..e4cf1f59 100644 --- a/.jshintrc +++ b/.jshintrc @@ -9,6 +9,7 @@ "afterEach": false, "before": false, "beforeEach": false, + "expect": false, "describe": false, "it": false } diff --git a/gulpfile.js b/gulpfile.js index dbe2cd3a..0d2cf8d4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -48,7 +48,10 @@ var tldsReplaceStr = '"' + tlds.join('|') + '".split("|")'; gulp.task('babel', function () { return gulp.src(paths.src) .pipe(replace('__TLDS__', tldsReplaceStr)) - .pipe(babel({format: babelformat})) + .pipe(babel({ + loose: 'all', + format: babelformat + })) .pipe(gulp.dest('lib')); }); @@ -60,6 +63,7 @@ gulp.task('babel-amd', function () { return gulp.src(paths.src) .pipe(replace('__TLDS__', tldsReplaceStr)) .pipe(babel({ + loose: 'all', modules: 'amd', moduleIds: true, format: babelformat @@ -138,6 +142,7 @@ gulp.task('build-interfaces', ['babel-amd'], function () { // Browser interface stream = gulp.src(files.js) .pipe(babel({ + loose: 'all', modules: 'ignore', format: babelformat })) @@ -182,6 +187,7 @@ gulp.task('build-plugins', ['babel-amd'], function () { // Global plugins stream = gulp.src('src/linkify/plugins/' + plugin + '.js') .pipe(babel({ + loose: 'all', modules: 'ignore', format: babelformat })) diff --git a/package.json b/package.json index fec78f61..dcba437c 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "devDependencies": { "amd-optimize": "^0.4.3", "brfs": "^1.4.0", - "chai": "^2.1.1", "closure-compiler": "^0.2.6", "coveralls": "^2.11.2", + "expect": "^0.3.1", "glob": "^5.0.3", "gulp": "^3.8.11", "gulp-babel": "^4.0.0", @@ -48,6 +48,6 @@ "mocha": "^2.2.1" }, "optionalDependencies": { - "jquery": "^2.1.3" + "jquery": "^1.11.1" } } diff --git a/src/linkify.js b/src/linkify.js index 51be1d77..4782482b 100644 --- a/src/linkify.js +++ b/src/linkify.js @@ -2,6 +2,12 @@ import * as options from './linkify/utils/options'; import * as scanner from './linkify/core/scanner'; import * as parser from './linkify/core/parser'; +if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; +} + /** Converts a string into tokens that represent linkable and non-linkable bits @method tokenize diff --git a/src/linkify/core/state.js b/src/linkify/core/state.js index 563d3675..0e6a12b3 100644 --- a/src/linkify/core/state.js +++ b/src/linkify/core/state.js @@ -152,7 +152,7 @@ class TokenState extends BaseState { @return {Boolean} */ test(token, tokenClass) { - return tokenClass.test(token); + return token instanceof tokenClass; } } diff --git a/src/linkify/core/tokens.js b/src/linkify/core/tokens.js index 960d8408..e51d3aac 100644 --- a/src/linkify/core/tokens.js +++ b/src/linkify/core/tokens.js @@ -28,16 +28,6 @@ class TextToken { toString() { return this.v + ''; } - - /** - Is the given value an instance of this Token? - @method test - @static - @param {Mixed} value - */ - static test(value) { - return value instanceof this; - } } /** @@ -190,8 +180,7 @@ let text = { // Is the given token a valid domain token? // Should nums be included here? function isDomainToken(token) { - return DOMAIN.test(token) || - TLD.test(token); + return token instanceof DOMAIN || token instanceof TLD; } /** @@ -271,17 +260,6 @@ class MultiToken { href: this.toHref(protocol) }; } - - /** - Is the given value an instance of this Token? - @method test - @static - @param {Mixed} value - */ - static test(token) { - return token instanceof this; - } - } /** @@ -358,14 +336,14 @@ class URL extends MultiToken { // Make the first part of the domain lowercase // Lowercase protocol - while (PROTOCOL.test(tokens[i])) { + while (tokens[i] instanceof PROTOCOL) { hasProtocol = true; result.push(tokens[i].toString().toLowerCase()); i++; } // Skip slash-slash - while (SLASH.test(tokens[i])) { + while (tokens[i] instanceof SLASH) { hasSlashSlash = true; result.push(tokens[i].toString()); i++; diff --git a/test/ci.conf.js b/test/ci.conf.js index 3fc84370..97b2656f 100644 --- a/test/ci.conf.js +++ b/test/ci.conf.js @@ -23,6 +23,18 @@ module.exports = function (config) { browserName: 'internet explorer', platform: 'Windows 8.1', version: '11' + }, + sl_ie_10: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8', + version: '10' + }, + sl_ie_9: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 7', + version: '9' }/*, sl_ie_8: { base: 'SauceLabs', diff --git a/test/conf.js b/test/conf.js index e67b8703..a433dc08 100644 --- a/test/conf.js +++ b/test/conf.js @@ -11,6 +11,7 @@ module.exports = { // list of files / patterns to load in the browser files: [ + 'test/shim.js', 'lib/*.js', 'lib/**/*.js', 'test/init.js', @@ -27,6 +28,7 @@ module.exports = { // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { + 'test/shim.js': ['browserify'], 'lib/*.js': ['browserify'], 'lib/**/*.js': ['browserify'], 'test/init.js': ['browserify'], diff --git a/test/index.html b/test/index.html index 3c5d8cc3..e44d8c68 100644 --- a/test/index.html +++ b/test/index.html @@ -6,7 +6,7 @@ - + - -``` - -##### Browser globals -```html - -``` - -#### Methods - -##### `linkify.find` _(`str` [, `type`])_ - -Finds all links in the given string - -**Params** - -* _`String`_ **`str`** Search string -* _`String`_ [**`type`**] (Optional) only find links of the given type - -**Returns** _`Array`_ List of links where each element is a hash with properties `type`, `value`, and `href`. - -* `type` is the type of entity found. Possible values are - - `'url'` - - `'email'` - - `'hashtag'` (with Hashtag plugin) -* `value` is the original entity substring. -* `href` should be the value of this link's `href` attribute. - -```js -linkify.find('For help with GitHub.com, please email support@github.com'); -``` - -Returns the array - -```js -[ - { - type: 'url', - value: 'GitHub.com', - href: 'http://github.com', - }, - { - type: 'email', - value: 'support@github.com', - href: 'mailto:support@github.com' - } -] -``` - -##### `linkify.test` _(`str` [, `type`])_ - -Is the given string a link? Not to be used for strict validation - See [Caveats](#) - -**Params** - -* _`String`_ **`str`** Test string -* _`String`_ [**`type`**] (Optional) returns `true` only if the link is of the given type (see `linkify.find`), - -**Returns** _`Boolean`_ - -```js -linkify.test('google.com'); // true -linkify.test('google.com', 'email'); // false -``` - -#### `linkify.tokenize` _(`str`)_ - -Internal method used to perform lexicographical analysis on the given string and output the resulting token array. - -**Params** - -* _`String`_ **`str`** - -**Returns** _`Array`_ - - -### `linkify-jquery` - -Provides the Linkify jQuery plugin. - -#### Installation - -##### Node.js/io.js/Browserify - -```js -var $ = require('jquery'); -require('linkifyjs/jquery')($, document); -``` - -Where the second argument is your `window.document` implementation (not required for Browserify). - -##### AMD - -Note that `linkify-jquery` requires a `jquery` module. - -```html - - - -``` - -```js -require(['jquery'], function ($) { - // ... -}); -``` - -##### Browser globals - -```html - - - -``` - -#### Usage - -```js -var options = { /* ... */ }; -$(selector).linkify(options); -``` - -**Params** - -* _`Object`_ [**`options`**] [Options hash](#options) - -See [all available options](#options). - -#### DOM Data API - -The jQuery plugin also provides a DOM data/HTML API - no extra JavaScript required! - -```html - -
...
- - -... -``` - -[Additional data options](#options) are available. - -### `linkify-string` - -Interface for replacing links within native strings with anchor tags. Note that this function will ***not*** parse HTML strings properly - use [`linkify-element`](#linkify-element) or [`linkify-jquery`](#linkify-jquery) instead. - -#### Installation - -##### Node.js/io.js/Browserify - -```js -var linkifyStr = require('linkifyjs/string'); -``` - -##### AMD - -```html - - - -``` - -##### Browser globals - -```html - - -``` - -#### Usage - -```js -var options = {/* ... */}; -var str = 'For help with GitHub.com, please email support@github.com'; -linkifyStr(str, options); -// or -str.linkify(options); -``` - -Returns - -```js -'For help with GitHub.com, please email support@github.com' -``` - -**Params** - -* _`String`_ **`str`** String to linkify -* _`Object`_ [**`options`**] [Options hash](#) - -**Returns** _`String`_ Linkified string - - -### Plugins - -Plugins provide no new interfaces but add additional detection functionality to Linkify. A custom plugin API is currently in the works. - -**Note:** Plugins should be included before interfaces. - -#### General Installation - -##### Node.js/io.js/Browserify - -```js -var linkify = require('linkifyjs') -require('linkifyjs/plugin/')(linkify); -``` - -##### AMD - -```html - - -``` - -##### Browser globals - -```html - - -``` - -#### `hashtag` Plugin - -Adds basic support for Twitter-style hashtags. - -```js -var linkify = require('linkifyjs'); -require('linkifyjs/plugins/hashtag')(linkify); -``` - -```js -var options = {/* ... */}; -var str = "Linkify is #super #rad2015"; - -linkify.find(str); -``` - -Returns the following array - -```js -[ - { - type: 'hashtag', - value: "#super", - href: "#super" - }, - { - type: 'hashtag', - value: "#rad2015", - href: "#rad2015" - } -] -``` - -## Options - -Linkify is applied with the following default options. Below is a description of each. - -```js -var options = { - defaultProtocol: 'http', - format: null, - formatHref: null, - linkAttributes: null, - linkClass: 'linkified', - nl2br: false, - tagName: 'a', - target: function (type) { - return type === 'url' ? '_blank' : null; - } -}; -``` - -### Usage - -```js -linkifyStr(str, options); // or `str.linkify(options)` -linkifyElement(document.getElementById(id), options); -$(selector).linkify(options); -``` - -#### `defaultProtocol` - -**Type**: `String`
-**Default**: `'http'`
-**Values**: `'http'`, `'https'`, `'ftp'`, `'ftps'`, etc.
-**Data API**: `data-linkify-default-protocol`
- -Protocol that should be used in `href` attributes for URLs without a protocol (e.g., `github.com`). - -#### `format` - -**Type**: `Function (String value, String type)`
-**Default**: `null`
- -Format the text displayed by a linkified entity. e.g., truncate a long URL. - -```js -'http://github.com/SoapBox/linkifyjs/search/?q=this+is+a+really+long+query+string'.linkify({ - format: function (value, type) { - if (type === 'url' && value.length > 50) { - value = value.slice(0, 50) + '…'; - } - return value; - } -}); -``` - -#### `formatHref` - -#### `nl2br` - -#### `tagName` +## Caveats -#### `target` +The core linkify library (excluding plugins) attempts to find emails and URLs that match RFC specifications. However, it doesn't always get it right. -#### `linkAttributes` +Here are a few of the known issues. -#### `linkClass` +* Non-standard email local parts delimited by " (quote characters) + * Emails with quotes in the localpart are detected correctly, unless the quotes contain certain characters like `@`. +* Slash characters in email addresses +* Non-latin domains or TLDs are not supported (support may be added via plugin in the future) -## Plugin API +## Contributing -Coming soon +Check out [CONTRIBUTING.md](https://github.com/SoapBox/jQuery-linkify/blob/master/CONTRIBUTING.md). -## Caveats +## License -## Contributing +MIT ## Authors -Linkify is handcrafted with Love by [SoapBox Innovations, Inc](http://soapboxhq.com). + +Linkify is built with Love™ and maintained by [SoapBox Innovations Inc.](http://soapboxhq.com).