├── .travis.yml ├── component.json ├── CHANGELOG.md ├── package.json ├── .gitignore ├── README.md ├── lib └── index.js └── test └── tests.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "li", 3 | "version": "1.2.0", 4 | "main": "./lib/index.js", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 2 | - Fixed issue where links with commas cause unexpected results (Lukepur) 3 | - Fixed issue where links with semicolons cause unexpected results (Znarkus) 4 | - Refactor library (Znarkus) 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "li", 3 | "version": "1.3.0", 4 | "description": "Parse the Links header format and return a javascript object.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jfromaniello/li.git" 12 | }, 13 | "keywords": [ 14 | "links", 15 | "http" 16 | ], 17 | "author": "José F. Romaniello (http://joseoncode.com)", 18 | "contributors": [ 19 | "Ron Waldon (http://jokeyrhy.me/)", 20 | "JAM (https://github.com/madeinjam)", 21 | "Luke Purton (https://github.com/lukepur)", 22 | "Markus Hedlund (https://github.com/Znarkus)" 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "mocha": "~1.7.4", 27 | "chai": "~1.9.0" 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | 54 | # End of https://www.gitignore.io/api/node 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jfromaniello/li.svg?branch=master)](https://travis-ci.org/jfromaniello/li) 2 | 3 | Parse and format [Link header according to RFC 5988](http://www.w3.org/Protocols/9707-link-header.html). 4 | 5 | ## Install 6 | 7 | $ npm install li 8 | 9 | Also works with bower, component.js, browserify, amd, etc. 10 | 11 | ## Usage 12 | 13 | Parse a Link header: 14 | 15 | ~~~javascript 16 | var li = require('li'); 17 | var someLinksHeader = '; rel="first", ' + 18 | '; rel="next", ' + 19 | '; rel="last"'; 20 | 21 | console.log(li.parse(someLinksHeader)); 22 | 23 | // This will print: 24 | // { 25 | // first: '/api/users?page=0&per_page=2', 26 | // next: '/api/users?page=1&per_page=2', 27 | // last: '/api/users?page=3&per_page=2' 28 | // } 29 | ~~~ 30 | 31 | Generate a Link header as follow with stringify: 32 | 33 | ~~~javascript 34 | var linksObject = { 35 | first : '/api/users?page=0&per_page=2', 36 | next : '/api/users?page=1&per_page=2', 37 | last : '/api/users?page=3&per_page=2', 38 | }; 39 | 40 | console.log(li.stringify(linksObject); 41 | 42 | // This will print the string: 43 | // ; rel="first", 44 | // ; rel="next", 45 | // ; rel="last" 46 | ~~~ 47 | 48 | ### Testing 49 | 50 | $ npm test 51 | 52 | ## License 53 | 54 | MIT 2014 - JOSE F. ROMANIELLO 55 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | (function (name, definition, context) { 2 | 3 | //try CommonJS, then AMD (require.js), then use global. 4 | 5 | if (typeof module != 'undefined' && module.exports) module.exports = definition(); 6 | else if (typeof context['define'] == 'function' && context['define']['amd']) define(definition); 7 | else context[name] = definition(); 8 | 9 | })('li', function () { 10 | // compile regular expressions ahead of time for efficiency 11 | var relsRegExp = /^;\s*([^"=]+)=(?:"([^"]+)"|([^";,]+)(?:[;,]|$))/; 12 | var sourceRegExp = /^<([^>]*)>/; 13 | var delimiterRegExp = /^\s*,\s*/; 14 | 15 | return { 16 | parse: function (linksHeader, options) { 17 | var match; 18 | var source; 19 | var rels; 20 | var extended = options && options.extended || false; 21 | var links = []; 22 | 23 | while (linksHeader) { 24 | linksHeader = linksHeader.trim(); 25 | 26 | // Parse `` 27 | source = sourceRegExp.exec(linksHeader); 28 | if (!source) break; 29 | 30 | var current = { 31 | link: source[1] 32 | }; 33 | 34 | // Move cursor 35 | linksHeader = linksHeader.slice(source[0].length); 36 | 37 | // Parse `; attr=relation` and `; attr="relation"` 38 | 39 | var nextDelimiter = linksHeader.match(delimiterRegExp); 40 | while(linksHeader && (!nextDelimiter || nextDelimiter.index > 0)) { 41 | match = relsRegExp.exec(linksHeader); 42 | if (!match) break; 43 | 44 | // Move cursor 45 | linksHeader = linksHeader.slice(match[0].length); 46 | nextDelimiter = linksHeader.match(delimiterRegExp); 47 | 48 | 49 | if (match[1] === 'rel' || match[1] === 'rev') { 50 | // Add either quoted rel or unquoted rel 51 | rels = (match[2] || match[3]).split(/\s+/); 52 | current[match[1]] = rels; 53 | } else { 54 | current[match[1]] = match[2] || match[3]; 55 | } 56 | } 57 | 58 | links.push(current); 59 | // Move cursor 60 | linksHeader = linksHeader.replace(delimiterRegExp, ''); 61 | } 62 | 63 | if (!extended) { 64 | return links.reduce(function(result, currentLink) { 65 | if (currentLink.rel) { 66 | currentLink.rel.forEach(function(rel) { 67 | result[rel] = currentLink.link; 68 | }); 69 | } 70 | return result; 71 | }, {}); 72 | } 73 | 74 | return links; 75 | }, 76 | stringify: function (params) { 77 | var grouped = Object.keys(params).reduce(function(grouped, key) { 78 | grouped[params[key]] = grouped[params[key]] || []; 79 | grouped[params[key]].push(key); 80 | return grouped; 81 | }, {}); 82 | 83 | var entries = Object.keys(grouped).reduce(function(result, link) { 84 | return result.concat('<' + link + '>; rel="' + grouped[link].join(' ') + '"'); 85 | }, []); 86 | 87 | return entries.join(', '); 88 | } 89 | }; 90 | 91 | }, this); 92 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | var should = require('chai').should(); 2 | var li = require('../lib'); 3 | 4 | var fixture = '; rel="first", ' + 5 | '; rel="next", ' + 6 | '; rel="last", ' + 7 | '; rel="self", ' + 8 | '; rel="filtered", ' + 9 | '; rel="related alternate", ' + 10 | '; rel="http://example.org/search-results", ' + 11 | '; rel="http://example.org/status-result collection", ' + 12 | '; rel="search"'; 13 | 14 | var quotfixture = '; rel=home,'+ 15 | '; rel=only one'; 16 | 17 | 18 | 19 | describe('parse-links', function () { 20 | describe('parse the links!', function(){ 21 | it('it should parse a links string into an object', function () { 22 | var parsed = li.parse(fixture); 23 | parsed.first.should.eql('/api/users?page=0&per_page=2'); 24 | parsed.next.should.eql('/api/users?page=1&per_page=2'); 25 | parsed.last.should.eql('/api/users?page=3&per_page=2'); 26 | parsed.self.should.eql('/api/users/123'); 27 | parsed.alternate.should.eql('/api/users/12345'); 28 | parsed.related.should.eql('/api/users/12345'); 29 | parsed['http://example.org/search-results'].should.eql('/api/users?name=Joe+Bloggs'); 30 | parsed['http://example.org/status-result'].should.eql('/api/users?status=registered'); 31 | parsed.collection.should.eql('/api/users?status=registered'); 32 | parsed.search.should.eql('/api/users?q=smith&fields=fname,lname'); 33 | Object.keys(parsed).length.should.eql(11); 34 | }); 35 | }); 36 | 37 | describe('parse links without quotes!', function() { 38 | it('should parse a links string without rels into an object', function () { 39 | var parsed = li.parse(quotfixture); 40 | parsed.home.should.eql('/api/users/1'); 41 | parsed.only.should.eql('/api/users/2'); 42 | parsed.one.should.eql('/api/users/2'); 43 | }); 44 | }); 45 | 46 | describe('with extra param (issue #6)', function() { 47 | it('should return the links', function() { 48 | var parsed = li.parse('; rel="next", ; rel="prev", ; rel="up"; rev="home", ; rel="ignored"'); 49 | parsed.next.should.eql('/3'); 50 | parsed.prev.should.eql('/2'); 51 | parsed.up.should.eql('/home'); 52 | }); 53 | 54 | it('should return the complete parsed object when using extended true', function() { 55 | var parsed = li.parse('; rel="next", ; rel="prev", ; rel="up"; rev="home", ; rel="ignored"', { extended: true }); 56 | parsed[0].link.should.equal('/3'); 57 | parsed[0].rel[0].should.equal('next'); 58 | 59 | parsed[1].link.should.equal('/2'); 60 | parsed[1].rel[0].should.equal('prev'); 61 | 62 | parsed[2].link.should.equal('/home'); 63 | parsed[2].rel[0].should.equal('up'); 64 | parsed[2].rev[0].should.equal('home'); 65 | }); 66 | }); 67 | 68 | describe('links without rel (issue #7)', function() { 69 | it('should return the links', function() { 70 | var parsed = li.parse('; rel="next", ; rel="prev", , ; rel="ignored"'); 71 | parsed.next.should.eql('/3'); 72 | parsed.prev.should.eql('/2'); 73 | parsed.ignored.should.eql('/void'); 74 | }); 75 | 76 | it('should return the links with extended param', function() { 77 | var parsed = li.parse('; rel="next", ; rel="prev", , ; rel="ignored"', { extended: true }); 78 | parsed[0].rel[0].should.eql('next'); 79 | parsed[0].link.should.eql('/3'); 80 | parsed[1].rel[0].should.eql('prev'); 81 | parsed[1].link.should.eql('/2'); 82 | parsed[2].link.should.eql('/home'); 83 | }); 84 | }); 85 | 86 | 87 | }); 88 | 89 | describe('stringify link object', function(){ 90 | it('should return a string with the links', function() { 91 | var linksObject = { 92 | first : '/api/users?page=0&per_page=2', 93 | next : '/api/users?page=1&per_page=2', 94 | last : '/api/users?page=3&per_page=2', 95 | self : '/api/users/123', 96 | filtered : '/api/users/123?filter=my;ids=1,2,3', 97 | 'related alternate' : '/api/users/12345', 98 | 'http://example.org/search-results' : '/api/users?name=Joe+Bloggs', 99 | 'http://example.org/status-result collection': '/api/users?status=registered', 100 | 'search' : '/api/users?q=smith&fields=fname,lname' 101 | }; 102 | var stringified = li.stringify(linksObject); 103 | stringified.should.equal(fixture); 104 | }); 105 | 106 | it('should group links', function() { 107 | var linksObject = { 108 | last : '/things?page=10&per_page=20', 109 | next : '/things?page=10&per_page=20', 110 | }; 111 | var stringified = li.stringify(linksObject); 112 | stringified.should.equal('; rel="last next"'); 113 | }); 114 | }); 115 | --------------------------------------------------------------------------------