Permalink
Browse files

Initial commit

  • Loading branch information...
1 parent cb5c24a commit 5dc1dc1ff69e7ae5b9578b0b1bcd5d92a281424f @jsantell committed Oct 15, 2012
View
@@ -0,0 +1 @@
+node_modules
View
@@ -0,0 +1,19 @@
+Copyright (c) 2012 Jordan Santell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
View
@@ -0,0 +1,3 @@
+all: build
+build:
+ coffee -c -o ./ src/link-injection.coffee
View
@@ -1,4 +1,50 @@
-node-link-injection
-===================
+link-injection
+======
+Parse text for keywords and replace with links for documentation
-Parse text for keywords and replace with links for documentation
+### How it works
+
+`link-injection` parses arbitrary text and replaces all keywords with an anchor link to the keyword's reference. Excellent for creating hyperlinks in documentation to other parts of the documentation and who knows what else. What is supported:
+
+* Can parse plain text, or HTML -- will not create an anchor inside of another anchor
+* Full word matching -- if `Array` is a keyword, it will not transpose `Float32Array` into an anchor
+
+### Installing
+
+* `npm install link-injection`
+
+### Methods
+
+* `parse( text, map, options )` Parses string `text` replacing instances of map's keys with an anchor with an href to the key's value.
+
+### Options
+
+* `caseSensitive` : Whether or not the keyword match should be case-sensitive. (default: `true`)
+
+### Usage
+
+```javascript
+var
+ inject = require( 'link-inject' ),
+ html = '<div>Modern browsers are now implementing a Float32Array type, which is a typed array version of an Array, except it only holds 32-bit floating point numbers. The <a href="#Float32Array">Float32Array</a> is frequently used in 3D WebGL applications and audio processing.</div>
+ // Using local links, but can be anything -- the keys' values are put into the href attribute
+ map = {
+ 'Array' : '#Array',
+ 'Float32Array' : '#Float32Array'
+ };
+
+var output = inject.parse( html, map );
+console.log( output );
+```
+
+Outputs:
+```html
+<div>
+Modern browsers are now implementing a <a href="#Float32Array" title="Float32Array">Float32Array</a> type, which is a typed array version of an <a href="#Array" title="Array">Array</a>, except it only holds 32-bit floating point numbers. The <a href="#Float32Array">Float32Array</a> is frequently used in 3D WebGL applications and audio processing.
+</div>
+```
+
+Development
+---
+
+Run `make` to build the coffee into JavaScript. Run `npm test` from project root -- requires `mocha` to be installed globally
View
@@ -0,0 +1,67 @@
+(function() {
+ var cheerio, defaults, isAnchorNested, makeRegex, parse, _;
+
+ cheerio = require('cheerio');
+
+ _ = require('underscore');
+
+ makeRegex = function(keyword, options) {
+ var flags;
+ flags = options.caseSensitive ? 'g' : 'gi';
+ return new RegExp("\\b(" + keyword + ")\\b", flags);
+ };
+
+ isAnchorNested = function($el) {
+ var $parent;
+ if ($el.name === 'a') return false;
+ $parent = $el.parent();
+ while (!($parent === $el || $parent === null)) {
+ if ($parent.name === 'a') return false;
+ $parent = $parent.parent();
+ }
+ return true;
+ };
+
+ defaults = {
+ caseSensitive: true,
+ repeat: 0
+ };
+
+ parse = function(text, map, options) {
+ var $, descend, keywords, output, regexMap, replace;
+ if (text == null) text = '';
+ if (map == null) map = {};
+ options = _.extend({}, defaults, options != null ? options : {});
+ keywords = Object.keys(map);
+ regexMap = {};
+ output = '';
+ $ = cheerio.load("<div class='link-injection-tag-class'>" + text + "</div>");
+ keywords.forEach(function(keyword) {
+ return regexMap[keyword] = makeRegex(keyword, options);
+ });
+ descend = function($el) {
+ var $children;
+ if ($el[0].type === 'tag' && $el[0].name === 'a') return;
+ $children = $el.children();
+ $children.each(function() {
+ return descend($(this));
+ });
+ return $el[0].children.forEach(function(node) {
+ if (node.type === 'text') return node.data = replace(node.data);
+ });
+ };
+ replace = function(text) {
+ _.each(regexMap, function(regex, keyword) {
+ return text = text.replace(regex, "<a href='" + map[keyword] + "' title='" + keyword + "'>$1</a>");
+ });
+ return text;
+ };
+ descend($('.link-injection-tag-class'));
+ return $('.link-injection-tag-class').html();
+ };
+
+ module.exports = {
+ parse: parse
+ };
+
+}).call(this);
View
@@ -0,0 +1,33 @@
+{
+ "name" : "link-injection",
+ "description" : "Parse text for keywords and replace with links for documentation",
+ "version" : "0.1.0",
+ "main" : "link-injection.js",
+ "dependencies" : {
+ "cheerio" : ">= 0.10.1",
+ "underscore" : ">= 0.0.0"
+ },
+ "devDependencies" : {
+ "mocha" : ">= 1.6.0",
+ "chai" : ">= 1.3.0"
+ },
+ "scripts" : {
+ "test" : "mocha --reporter nyan"
+ },
+ "repository" : {
+ "type" : "git",
+ "url" : "http://github.com/jsantell/node-link-injection"
+ },
+ "keywords" : [
+ "html",
+ "parse",
+ "link",
+ "documentation"
+ ],
+ "author" : {
+ "name" : "Jordan Santell",
+ "url" : "http://github.com/jsantell"
+ },
+ "license" : "MIT",
+ "engines" : ["node >= 0.6.12"]
+}
@@ -0,0 +1,60 @@
+cheerio = require 'cheerio'
+_ = require 'underscore'
+
+makeRegex = ( keyword, options ) ->
+ flags = if options.caseSensitive then 'g' else 'gi'
+ new RegExp "\\b(#{keyword})\\b", flags
+
+# Current text must not be nested inside of an anchor
+isAnchorNested = ( $el ) ->
+ return false if $el.name is 'a'
+
+ $parent = $el.parent()
+ until $parent is $el or $parent is null
+ return false if $parent.name is 'a'
+ $parent = $parent.parent()
+
+ return true
+
+defaults =
+ caseSensitive: true
+ repeat: 0
+
+parse = ( text, map, options ) ->
+ text ?= ''
+ map ?= {}
+ options = _.extend {}, defaults, options ? {}
+ keywords = Object.keys map
+ regexMap = {}
+ output = ''
+
+ # Let's wrap our text in a div incase the text doesn't
+ # have any markup, or if it does and is not wrapped in an element
+ $ = cheerio.load "<div class='link-injection-tag-class'>#{text}</div>"
+
+ keywords.forEach ( keyword ) ->
+ regexMap[ keyword ] = makeRegex keyword, options
+
+ descend = ( $el ) ->
+ return if $el[0].type is 'tag' and $el[0].name is 'a'
+ $children = $el.children()
+ # Check children elements
+ $children.each () ->
+ descend $(@)
+
+ # Parse text nodes
+ $el[0].children.forEach ( node ) ->
+ if node.type is 'text'
+ node.data = replace node.data
+
+ replace = ( text ) ->
+ _.each regexMap, ( regex, keyword ) ->
+ text = text.replace regex, "<a href='#{map[ keyword ]}' title='#{keyword}'>$1</a>"
+ return text
+
+ descend $('.link-injection-tag-class')
+
+ $('.link-injection-tag-class').html()
+
+
+module.exports = { parse: parse }
@@ -0,0 +1 @@
+If caseSensitive is true, then both AudioNode and audioNode and audionode should be transposed.
@@ -0,0 +1 @@
+Here is some sample text. Node is one of my keywords and so is AudioNode. already has a link and shouldn't be duplicated. Node should not override AudioNode. And if I use Node as part of another word like CrazyNode, it shouldn't change it. <a href='#'>this is <p>going to be<p>a stupid nested AudioNode</p> that shouldn't parse</p> because its in an anchor</a>. Also want to make sure it works at the end of a tag Node
@@ -0,0 +1 @@
+<div>Here is some sample text. Node is one of my keywords and so is AudioNode. This <a href='#AudioNode'>AudioNode</a> already has a link and shouldn't be duplicated. Node should not override AudioNode. And if I use Node as part of another word like CrazyNode, it shouldn't change it. <a href='#'>this is <p>going to be<p>a stupid nested AudioNode</p> that shouldn't parse</p> because its in an anchor</a>. Also want to make sure it works at the end of a tag Node</div>
View
@@ -0,0 +1 @@
+Here is some sample text. Node is one of my keywords and so is AudioNode. already has a link and shouldn't be duplicated. Node should not override AudioNode. And if I use Node as part of another word like CrazyNode, it shouldn't change it. Also want to make sure it works at the end of a tag Node
@@ -0,0 +1 @@
+If caseSensitive is true, then both <a href='#AudioNode' title='AudioNode'>AudioNode</a> and <a href='#AudioNode' title='AudioNode'>audioNode</a> and <a href='#AudioNode' title='AudioNode'>audionode</a> should be transposed.
@@ -0,0 +1 @@
+If caseSensitive is true, then both <a href='#AudioNode' title='AudioNode'>AudioNode</a> and audioNode and audionode should be transposed.
@@ -0,0 +1 @@
+Here is some sample text. <a href='#Node' title='Node'>Node</a> is one of my keywords and so is <a href='#AudioNode' title='AudioNode'>AudioNode</a>. already has a link and shouldn't be duplicated. <a href='#Node' title='Node'>Node</a> should not override <a href='#AudioNode' title='AudioNode'>AudioNode</a>. And if I use <a href='#Node' title='Node'>Node</a> as part of another word like CrazyNode, it shouldn't change it. <a href="#">this is <p>going to be<p>a stupid nested AudioNode</p> that shouldn't parse</p> because its in an anchor</a>. Also want to make sure it works at the end of a tag <a href='#Node' title='Node'>Node</a>
@@ -0,0 +1 @@
+<div>Here is some sample text. <a href='#Node' title='Node'>Node</a> is one of my keywords and so is <a href='#AudioNode' title='AudioNode'>AudioNode</a>. This <a href="#AudioNode">AudioNode</a> already has a link and shouldn't be duplicated. <a href='#Node' title='Node'>Node</a> should not override <a href='#AudioNode' title='AudioNode'>AudioNode</a>. And if I use <a href='#Node' title='Node'>Node</a> as part of another word like CrazyNode, it shouldn't change it. <a href="#">this is <p>going to be<p>a stupid nested AudioNode</p> that shouldn't parse</p> because its in an anchor</a>. Also want to make sure it works at the end of a tag <a href='#Node' title='Node'>Node</a></div>
@@ -0,0 +1 @@
+Here is some sample text. <a href='#Node' title='Node'>Node</a> is one of my keywords and so is <a href='#AudioNode' title='AudioNode'>AudioNode</a>. already has a link and shouldn't be duplicated. <a href='#Node' title='Node'>Node</a> should not override <a href='#AudioNode' title='AudioNode'>AudioNode</a>. And if I use <a href='#Node' title='Node'>Node</a> as part of another word like CrazyNode, it shouldn't change it. Also want to make sure it works at the end of a tag <a href='#Node' title='Node'>Node</a>
View
@@ -0,0 +1,31 @@
+var
+ inject = require( '../link-injection' ),
+ chai = require( 'chai' ),
+ fs = require( 'fs' ),
+ should = chai.should(),
+ expect = chai.expect;
+
+var
+ expectedCaseSensitiveTrue = fs.readFileSync( 'test/expected/caseSensitiveTrue', 'utf-8' ),
+ expectedCaseSensitiveFalse = fs.readFileSync( 'test/expected/caseSensitiveFalse', 'utf-8' ),
+ map = {
+ 'AudioNode' : '#AudioNode',
+ 'Node' : '#Node'
+ };
+
+describe( 'Case-sensitive', function () {
+ it( 'By default, be case-sensitive', function () {
+ var
+ html = fs.readFileSync( 'test/data/caseSensitive', 'utf-8' ),
+ output = inject.parse( html, map );
+ output.should.equal( expectedCaseSensitiveTrue );
+ });
+ it( 'should transpose regardless of case if case-sensitive is false', function () {
+ var
+ html = fs.readFileSync( 'test/data/caseSensitive', 'utf-8' ),
+ output = inject.parse( html, map, {
+ caseSensitive: false
+ });
+ output.should.equal( expectedCaseSensitiveFalse );
+ });
+});
View
@@ -0,0 +1,36 @@
+var
+ inject = require( '../link-injection' ),
+ chai = require( 'chai' ),
+ fs = require( 'fs' ),
+ should = chai.should(),
+ expect = chai.expect;
+
+var
+ expectedHtmlWrap = fs.readFileSync( 'test/expected/htmlWrap.html', 'utf-8' ),
+ expectedHtmlNoWrap = fs.readFileSync( 'test/expected/htmlNoWrap.html', 'utf-8' ),
+ expectedPlainText = fs.readFileSync( 'test/expected/plainText', 'utf-8' ),
+ map = {
+ 'AudioNode' : '#AudioNode',
+ 'Node' : '#Node'
+ };
+
+describe( 'Parsing', function () {
+ it( 'should correctly parse text wrapped by HTML elements', function () {
+ var
+ html = fs.readFileSync( 'test/data/htmlWrap.html', 'utf-8' ),
+ output = inject.parse( html, map );
+ output.should.equal( expectedHtmlWrap );
+ });
+ it( 'should correctly parse text not wrapped by HTML elements, but containing elements', function () {
+ var
+ html = fs.readFileSync( 'test/data/htmlNoWrap.html', 'utf-8' ),
+ output = inject.parse( html, map );
+ output.should.equal( expectedHtmlNoWrap );
+ });
+ it( 'should correctly parse HTML elements', function () {
+ var
+ html = fs.readFileSync( 'test/data/plainText', 'utf-8' ),
+ output = inject.parse( html, map );
+ output.should.equal( expectedPlainText );
+ });
+});

0 comments on commit 5dc1dc1

Please sign in to comment.