http-link-header
Advanced tools
Comparing version 0.8.0 to 1.0.0
468
lib/link.js
@@ -1,220 +0,60 @@ | ||
var querystring = require( 'querystring' ) | ||
var trim = require( './trim' ) | ||
'use strict' | ||
/** | ||
* Link | ||
* @constructor | ||
* @return {Link} | ||
*/ | ||
function Link( value ) { | ||
var COMPATIBLE_ENCODING_PATTERN = /^utf-?8|ascii|utf-?16-?le|ucs-?2|base-?64|latin-?1$/i | ||
var WS_TRIM_PATTERN = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g | ||
var WS_CHAR_PATTERN = /\s|\uFEFF|\xA0/ | ||
var WS_FOLD_PATTERN = /\r?\n[\x20\x09]+/g | ||
var DELIMITER_PATTERN = /[;,"]/ | ||
var WS_DELIMITER_PATTERN = /[;,"]|\s/ | ||
if( !(this instanceof Link) ) { | ||
return new Link( value ) | ||
} | ||
/** @type {Array} URI references */ | ||
this.refs = [] | ||
var STATE = { | ||
IDLE: 1 << 0, | ||
URI: 1 << 1, | ||
ATTR: 1 << 2, | ||
} | ||
/** | ||
* General matching pattern | ||
* @type {RegExp} | ||
*/ | ||
Link.pattern = /(?:\<([^\>]+)\>)((\s*;\s*([a-z\*]+)=(("[^"]+")|('[^']+')|([^\,\;]+)))*)(\s*,\s*|$)/gi | ||
/** | ||
* Attribute matching pattern | ||
* @type {RegExp} | ||
*/ | ||
Link.attrPattern = /([a-z\*]+)=(?:(?:"([^"]+)")|(?:'([^']+)')|([^\,\;]+))/gi | ||
/** | ||
* Determines whether an encoding can be | ||
* natively handled with a `Buffer` | ||
* @param {String} value | ||
* @return {Boolean} | ||
*/ | ||
Link.isCompatibleEncoding = function( value ) { | ||
return /^utf-?8|ascii|utf-?16-?le|ucs-?2|base-?64|latin-?1$/i.test( value ) | ||
function trim( value ) { | ||
return value.replace( WS_TRIM_PATTERN, '' ) | ||
} | ||
/** | ||
* Format a given extended attribute and it's value | ||
* @param {String} attr | ||
* @param {Object} data | ||
* @return {String} | ||
*/ | ||
Link.formatExtendedAttribute = function( attr, data ) { | ||
var encoding = ( data.encoding || 'utf-8' ).toUpperCase() | ||
var language = data.language || 'en' | ||
var encodedValue = '' | ||
if( Buffer.isBuffer( data.value ) && Link.isCompatibleEncoding( encoding ) ) { | ||
encodedValue = data.value.toString( encoding ) | ||
} else if( Buffer.isBuffer( data.value ) ) { | ||
encodedValue = data.value.toString( 'hex' ) | ||
.replace( /[0-9a-f]{2}/gi, '%$1' ) | ||
} else { | ||
encodedValue = querystring.escape( data.value ) | ||
} | ||
return attr + '=' + encoding + '\'' + | ||
language + '\'' + encodedValue | ||
function hasWhitespace( value ) { | ||
return WS_CHAR_PATTERN.test( value ) | ||
} | ||
/** | ||
* Format a given attribute and it's value | ||
* @param {String} attr | ||
* @param {String|Object} value | ||
* @return {String} | ||
*/ | ||
Link.formatAttribute = function( attr, value ) { | ||
// NOTE: Properly test this condition | ||
if( /\*$/.test( attr ) || typeof value !== 'string' ) | ||
return Link.formatExtendedAttribute( attr, value ) | ||
// Strictly, not all values matching this | ||
// selector would need quotes, but it's better to be safe | ||
var needsQuotes = /[^a-z]/i.test( value ) | ||
if( needsQuotes ) { | ||
// We don't need to escape <SP> <,> <;> | ||
value = querystring.escape( value ) | ||
.replace( /%20/g, ' ' ) | ||
.replace( /%2C/g, ',' ) | ||
.replace( /%3B/g, ';' ) | ||
value = '"' + value + '"' | ||
function skipWhitespace( value, offset ) { | ||
while( hasWhitespace( value[offset] ) ) { | ||
offset++ | ||
} | ||
return attr + '=' + value | ||
return offset | ||
} | ||
/** | ||
* Parses an extended value and attempts to decode it | ||
* @internal | ||
* @param {String} value | ||
* @return {Object} | ||
*/ | ||
Link.parseExtendedValue = function( value ) { | ||
var parts = /([^']+)?(?:'([^']+)')?(.+)/.exec( value ) | ||
return { | ||
language: parts[2].toLowerCase(), | ||
encoding: Link.isCompatibleEncoding( parts[1] ) ? | ||
null : parts[1].toLowerCase(), | ||
value: Link.isCompatibleEncoding( parts[1] ) ? | ||
querystring.unescape( parts[3] ) : parts[3] | ||
} | ||
function needsQuotes( value ) { | ||
return WS_DELIMITER_PATTERN.test( value ) | ||
} | ||
/** | ||
* Set an attribute on a link ref | ||
* @param {Object} link | ||
* @param {String} attr | ||
* @param {String} value | ||
*/ | ||
Link.setAttr = function( link, attr, value ) { | ||
class Link { | ||
// Occurrences after the first "rel" MUST be ignored by parsers | ||
// @see RFC 5988, Section 5.3: Relation Type | ||
if( attr === 'rel' && link[ attr ] != null ) | ||
return link | ||
/** | ||
* Link | ||
* @constructor | ||
* @param {String} [value] | ||
* @returns {Link} | ||
*/ | ||
constructor( value ) { | ||
if( Array.isArray( link[ attr ] ) ) { | ||
link[ attr ].push( value ) | ||
} else if( link[ attr ] != null ) { | ||
link[ attr ] = [ link[ attr ], value ] | ||
} else { | ||
link[ attr ] = value | ||
} | ||
/** @type {Array} URI references */ | ||
this.refs = [] | ||
return link | ||
if( value ) { | ||
this.parse( value ) | ||
} | ||
} | ||
/** | ||
* Parses uri attributes | ||
*/ | ||
Link.parseParams = function( link, uri ) { | ||
var kvs = {} | ||
var params = /(.+)\?(.+)/gi.exec( uri ) | ||
if( !params ) { | ||
return link | ||
} | ||
params = params[2].split('&') | ||
for( var i = 0; i < params.length; i++ ) { | ||
var param = params[i].split('='); | ||
kvs[ param[0] ] = param[1] | ||
} | ||
Link.setAttr( link, 'params', kvs ) | ||
return link | ||
} | ||
/** | ||
* Parses out URI attributes | ||
* @internal | ||
* @param {Object} link | ||
* @param {String} parts | ||
* @return {Object} link | ||
*/ | ||
Link.parseAttrs = function( link, parts ) { | ||
var match = null | ||
var attr = '' | ||
var value = '' | ||
var attrs = '' | ||
var uriAttrs = /<(.*)>;\s*(.*)/gi.exec( parts ) | ||
if( uriAttrs ) { | ||
attrs = uriAttrs[2] | ||
link = Link.parseParams( link, uriAttrs[1] ) | ||
} | ||
while( match = Link.attrPattern.exec( attrs ) ) { | ||
attr = match[1].toLowerCase() | ||
value = match[4] || match[3] || match[2] | ||
if( /\*$/.test( attr ) ) { | ||
Link.setAttr( link, attr, Link.parseExtendedValue( value ) ) | ||
} else if( /%/.test( value ) ) { | ||
Link.setAttr( link, attr, querystring.unescape( value ) ) | ||
} else { | ||
Link.setAttr( link, attr, value ) | ||
} | ||
} | ||
return link | ||
} | ||
Link.parse = function( value ) { | ||
return new Link().parse( value ) | ||
} | ||
/** | ||
* Link prototype | ||
* @type {Object} | ||
*/ | ||
Link.prototype = { | ||
constructor: Link, | ||
/** | ||
* Get refs with given relation type | ||
* @param {String} value | ||
* @return {Array<Object>} | ||
* @returns {Array<Object>} | ||
*/ | ||
rel: function( value ) { | ||
rel( value ) { | ||
@@ -231,3 +71,3 @@ var links = [] | ||
}, | ||
} | ||
@@ -238,5 +78,5 @@ /** | ||
* @param {String} value | ||
* @return {Array<Object>} | ||
* @returns {Array<Object>} | ||
*/ | ||
get: function( attr, value ) { | ||
get( attr, value ) { | ||
@@ -255,31 +95,120 @@ attr = attr.toLowerCase() | ||
}, | ||
} | ||
set: function( link ) { | ||
set( link ) { | ||
this.refs.push( link ) | ||
return this | ||
}, | ||
} | ||
has: function( attr, value ) { | ||
has( attr, value ) { | ||
return this.get( attr, value ) != null | ||
}, | ||
} | ||
parse: function( value ) { | ||
parse( value, offset ) { | ||
// Unfold folded lines | ||
value = trim( value ) | ||
.replace( /\r?\n[\x20\x09]+/g, '' ) | ||
offset = offset || 0 | ||
value = offset ? value.slice( offset ) : value | ||
var match = null | ||
// Trim & unfold folded lines | ||
value = trim( value ).replace( WS_FOLD_PATTERN, '' ) | ||
while( match = Link.pattern.exec( value ) ) { | ||
var link = Link.parseAttrs({ uri: match[1] }, match[0] ) | ||
this.refs.push( link ) | ||
var state = STATE.IDLE | ||
var length = value.length | ||
var offset = 0 | ||
var ref = null | ||
while( offset < length ) { | ||
if( state === STATE.IDLE ) { | ||
if( hasWhitespace( value[offset] ) ) { | ||
offset++ | ||
continue | ||
} else if( value[offset] === '<' ) { | ||
var end = value.indexOf( '>', offset ) | ||
if( end === -1 ) throw new Error( 'Expected end of URI delimiter at offset ' + offset ) | ||
ref = { uri: value.slice( offset + 1, end ) } | ||
this.refs.push( ref ) | ||
offset = end | ||
state = STATE.URI | ||
} else { | ||
throw new Error( 'Unexpected character "' + value[offset] + '" at offset ' + offset ) | ||
} | ||
offset++ | ||
} else if( state === STATE.URI ) { | ||
if( hasWhitespace( value[offset] ) ) { | ||
offset++ | ||
continue | ||
} else if( value[offset] === ';' ) { | ||
state = STATE.ATTR | ||
offset++ | ||
} else if( value[offset] === ',' ) { | ||
state = STATE.IDLE | ||
offset++ | ||
} else { | ||
throw new Error( 'Unexpected character "' + value[offset] + '" at offset ' + offset ) | ||
} | ||
} else if( state === STATE.ATTR ) { | ||
if( value[offset] ===';' || hasWhitespace( value[offset] ) ) { | ||
offset++ | ||
continue | ||
} | ||
var end = value.indexOf( '=', offset ) | ||
if( end === -1 ) throw new Error( 'Expected attribute delimiter at offset ' + offset ) | ||
var attr = trim( value.slice( offset, end ) ).toLowerCase() | ||
var attrValue = '' | ||
offset = end + 1 | ||
offset = skipWhitespace( value, offset ) | ||
if( value[offset] === '"' ) { | ||
offset++ | ||
while( offset < length ) { | ||
if( value[offset] === '"' ) { | ||
offset++; break | ||
} | ||
if( value[offset] === '\\' ) { | ||
offset++ | ||
} | ||
attrValue += value[offset] | ||
offset++ | ||
} | ||
} else { | ||
var end = offset + 1 | ||
while( !DELIMITER_PATTERN.test( value[end] ) && end < length ) { | ||
end++ | ||
} | ||
attrValue = value.slice( offset, end ) | ||
offset = end | ||
} | ||
if( ref[ attr ] && Link.isSingleOccurenceAttr( attr ) ) { | ||
// Ignore multiples of attributes which may only appear once | ||
} else if( attr[ attr.length - 1 ] === '*' ) { | ||
ref[ attr ] = Link.parseExtendedValue( attrValue ) | ||
} else { | ||
attrValue = attr === 'rel' || attr === 'type' ? | ||
attrValue.toLowerCase() : attrValue | ||
if( ref[ attr ] != null ) { | ||
if( Array.isArray( ref[ attr ] ) ) { | ||
ref[ attr ].push( attrValue ) | ||
} else { | ||
ref[ attr ] = [ ref[ attr ], attrValue ] | ||
} | ||
} else { | ||
ref[ attr ] = attrValue | ||
} | ||
} | ||
switch( value[offset] ) { | ||
case ',': state = STATE.IDLE; break | ||
case ';': state = STATE.ATTR; break | ||
} | ||
offset++ | ||
} else { | ||
throw new Error( 'Unknown parser state "' + state + '"' ) | ||
} | ||
} | ||
ref = null | ||
return this | ||
}, | ||
} | ||
toString: function() { | ||
toString() { | ||
@@ -301,7 +230,114 @@ var refs = [] | ||
}, | ||
} | ||
} | ||
// Exports | ||
/** | ||
* Determines whether an encoding can be | ||
* natively handled with a `Buffer` | ||
* @param {String} value | ||
* @returns {Boolean} | ||
*/ | ||
Link.isCompatibleEncoding = function( value ) { | ||
return COMPATIBLE_ENCODING_PATTERN.test( value ) | ||
} | ||
Link.parse = function( value, offset ) { | ||
return new Link().parse( value, offset ) | ||
} | ||
Link.isSingleOccurenceAttr = function( attr ) { | ||
return attr === 'rel' || attr === 'type' || attr === 'media' || | ||
attr === 'title' || attr === 'title*' | ||
} | ||
Link.isTokenAttr = function( attr ) { | ||
return attr === 'rel' || attr === 'type' || attr === 'anchor' | ||
} | ||
Link.escapeQuotes = function( value ) { | ||
return value.replace( /"/g, '\\"' ) | ||
} | ||
/** | ||
* Parses an extended value and attempts to decode it | ||
* @internal | ||
* @param {String} value | ||
* @return {Object} | ||
*/ | ||
Link.parseExtendedValue = function( value ) { | ||
var parts = /([^']+)?(?:'([^']+)')?(.+)/.exec( value ) | ||
return { | ||
language: parts[2].toLowerCase(), | ||
encoding: Link.isCompatibleEncoding( parts[1] ) ? | ||
null : parts[1].toLowerCase(), | ||
value: Link.isCompatibleEncoding( parts[1] ) ? | ||
decodeURIComponent( parts[3] ) : parts[3] | ||
} | ||
} | ||
/** | ||
* Format a given extended attribute and it's value | ||
* @param {String} attr | ||
* @param {Object} data | ||
* @return {String} | ||
*/ | ||
Link.formatExtendedAttribute = function( attr, data ) { | ||
var encoding = ( data.encoding || 'utf-8' ).toUpperCase() | ||
var language = data.language || 'en' | ||
var encodedValue = '' | ||
if( Buffer.isBuffer( data.value ) && Link.isCompatibleEncoding( encoding ) ) { | ||
encodedValue = data.value.toString( encoding ) | ||
} else if( Buffer.isBuffer( data.value ) ) { | ||
encodedValue = data.value.toString( 'hex' ) | ||
.replace( /[0-9a-f]{2}/gi, '%$1' ) | ||
} else { | ||
encodedValue = encodeURIComponent( data.value ) | ||
} | ||
return attr + '=' + encoding + '\'' + | ||
language + '\'' + encodedValue | ||
} | ||
/** | ||
* Format a given attribute and it's value | ||
* @param {String} attr | ||
* @param {String|Object} value | ||
* @return {String} | ||
*/ | ||
Link.formatAttribute = function( attr, value ) { | ||
if( Array.isArray( value ) ) { | ||
return value.map(( item ) => { | ||
return Link.formatAttribute( attr, item ) | ||
}).join( '; ' ) | ||
} | ||
if( attr[ attr.length - 1 ] === '*' || typeof value !== 'string' ) { | ||
return Link.formatExtendedAttribute( attr, value ) | ||
} | ||
if( Link.isTokenAttr( attr ) ) { | ||
value = needsQuotes( value ) ? | ||
'"' + Link.escapeQuotes( value ) + '"' : | ||
Link.escapeQuotes( value ) | ||
} else if( needsQuotes( value ) ) { | ||
value = encodeURIComponent( value ) | ||
// We don't need to escape <SP> <,> <;> within quotes | ||
value = value | ||
.replace( /%20/g, ' ' ) | ||
.replace( /%2C/g, ',' ) | ||
.replace( /%3B/g, ';' ) | ||
value = '"' + value + '"' | ||
} | ||
return attr + '=' + value | ||
} | ||
module.exports = Link |
{ | ||
"name": "http-link-header", | ||
"version": "0.8.0", | ||
"description": "Parse & format HTTP link headers according to RFC 5988", | ||
"version": "1.0.0", | ||
"description": "Parse & format HTTP link headers according to RFC 8288", | ||
"author": "Jonas Hermsmeier <jhermsmeier@gmail.com> (https://jhermsmeier.de)", | ||
@@ -9,4 +9,6 @@ "license": "MIT", | ||
"rfc5988", | ||
"rfc8288", | ||
"rfc", | ||
"5988", | ||
"8288", | ||
"http", | ||
@@ -17,6 +19,10 @@ "link", | ||
"main": "lib/link.js", | ||
"scripts": { | ||
"benchmark": "node benchmark", | ||
"test": "mocha --ui tdd" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"matcha": "~0.7.0", | ||
"mocha": "~3.2.0" | ||
"mocha": "^5.2.0", | ||
"nanobench": "^2.1.1" | ||
}, | ||
@@ -31,9 +37,5 @@ "homepage": "https://github.com/jhermsmeier/node-http-link-header", | ||
}, | ||
"directories": { | ||
"test": "test" | ||
}, | ||
"scripts": { | ||
"benchmark": "matcha --reporter plain", | ||
"test": "mocha --ui tdd" | ||
"engines": { | ||
"node": ">=4.0.0" | ||
} | ||
} |
@@ -7,5 +7,5 @@ # HTTP Link Header | ||
Parse & format HTTP link headers according to [RFC 5988] | ||
Parse & format HTTP link headers according to [RFC 8288] | ||
[RFC 5988]: https://tools.ietf.org/html/rfc5988 | ||
[RFC 8288]: https://tools.ietf.org/html/rfc8288 | ||
@@ -24,3 +24,3 @@ ## Install via [npm](https://npmjs.com) | ||
**Parse a HTTP link header** | ||
### Parsing a HTTP link header | ||
@@ -41,3 +41,3 @@ ```js | ||
**Check whether it has a reference with a given attribute & value** | ||
### Checking whether it has a reference with a given attribute & value | ||
@@ -49,3 +49,3 @@ ```js | ||
**Retrieve a reference with a given attribute & value** | ||
### Retrieving a reference with a given attribute & value | ||
@@ -66,3 +66,3 @@ ```js | ||
**Set references** | ||
### Setting references | ||
@@ -80,8 +80,8 @@ ```js | ||
**Parse multiple headers** | ||
### Parsing multiple headers | ||
```js | ||
var links = new LinkHeader() | ||
var link = new LinkHeader() | ||
links.parse( '<example.com>; rel="example"; title="Example Website"' ) | ||
link.parse( '<example.com>; rel="example"; title="Example Website"' ) | ||
> Link { | ||
@@ -93,3 +93,3 @@ refs: [ | ||
links.parse( '<example-01.com>; rel="alternate"; title="Alternate Example Domain"' ) | ||
link.parse( '<example-01.com>; rel="alternate"; title="Alternate Example Domain"' ) | ||
> Link { | ||
@@ -102,3 +102,3 @@ refs: [ | ||
links.parse( '<example-02.com>; rel="alternate"; title="Second Alternate Example Domain"' ) | ||
link.parse( '<example-02.com>; rel="alternate"; title="Second Alternate Example Domain"' ) | ||
> Link { | ||
@@ -113,7 +113,21 @@ refs: [ | ||
**Stringify to HTTP header format** | ||
### Handling extended attributes | ||
```js | ||
link.parse( '</extended-attr-example>; rel=start; title*=UTF-8\'en\'%E2%91%A0%E2%93%AB%E2%85%93%E3%8F%A8%E2%99%B3%F0%9D%84%9E%CE%BB' ) | ||
``` | ||
```js | ||
> Link { | ||
refs: [ | ||
{ uri: '/extended-attr-example', rel: 'start', 'title*': { language: 'en', encoding: null, value: '①⓫⅓㏨♳𝄞λ' } } | ||
] | ||
} | ||
``` | ||
### Stringifying to HTTP header format | ||
```js | ||
link.toString() | ||
> '<example.com>; rel="example"; title="Example Website", <example-01.com>; rel="alternate"; title="Alternate Example Domain"' | ||
> '<example.com>; rel=example; title="Example Website", <example-01.com>; rel=alternate; title="Alternate Example Domain"' | ||
``` | ||
@@ -128,5 +142,7 @@ | ||
``` | ||
http-link-header | ||
parse .......................................... 204,355 op/s | ||
toString ....................................... 485,465 op/s | ||
# http-link-header .parse() ⨉ 1000000 | ||
ok ~1.29 s (1 s + 289696759 ns) | ||
# http-link-header #toString() ⨉ 1000000 | ||
ok ~554 ms (0 s + 553782657 ns) | ||
``` |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
14664
6
281
0
139
1