New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

feature-matrix

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

feature-matrix - npm Package Compare versions

Comparing version

to
0.2.0

dist/FeatureMatrix.css

13

package.json
{
"name": "feature-matrix",
"version": "0.1.0",
"version": "0.2.0",
"description": "A library for displaying a product's browser version requirements based on the underlying features",

@@ -8,2 +8,3 @@ "author": "Concept Safety Systems",

"scripts": {
"test": "mocha --compilers js:babel-core/register",
"build": "webpack --config=webpack.config.dev.js",

@@ -14,11 +15,19 @@ "build-prod": "webpack --config=webpack.config.prod.js",

"devDependencies": {
"autoprefixer-loader": "^3.2.0",
"babel-core": "^6.9.1",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"chai": "^3.5.0",
"css-loader": "^0.23.1",
"extract-text-webpack-plugin": "^1.0.1",
"mocha": "^2.5.3",
"node-sass": "^3.8.0",
"sass-loader": "^3.2.1",
"style-loader": "^0.13.1",
"webpack": "^1.13.1"
},
"dependencies": {
"jquery": "^1.12.4"
"jquery": "^1.12.4",
"xhr": "^2.2.0"
}
}

@@ -0,1 +1,3 @@

import parseProductVersionString from './parseProductVersionString';
export const browsers = {

@@ -7,5 +9,5 @@ 'chrome': {

'matches': [
/chrome/gi,
/google chrome/gi
]
/^chrome$/i,
],
'minVersion': 5
},

@@ -17,4 +19,3 @@ 'ie': {

'matches': [
/ie/gi,
/internet explorer/gi
/^ie$/i
],

@@ -28,4 +29,3 @@ 'minVersion': 6

'matches': [
/edge/gi,
/microsoft edge/gi
/^edge$/i,
],

@@ -39,6 +39,46 @@ 'minVersion': 12

'matches': [
/ff/gi,
/firefox/gi,
/mozilla firefox/gi
/^ff$/i,
/^firefox$/i,
],
'minVersion': 2
},
'safari': {
'icon': 'https://www.browserfeatures.io/images/safari/safari_128x128.png',
'name': 'Safari',
'shortName': 'Safari',
'matches': [
/^safari$/i,
]
},
'opera': {
'icon': 'https://www.browserfeatures.io/images/opera/opera_128x128.png',
'name': 'Opera',
'shortName': 'Opera',
'matches': [
/^opera$/i,
]
},
'ios': {
'icon': 'https://www.browserfeatures.io/images/safari-ios/safari-ios_128x128.png',
'name': 'iOS Safari',
'shortName': 'iOS',
'matches': [
/^ios_saf$/i,
]
},
'android-browser': {
'icon': 'http://cdn.rawgit.com/alrra/browser-logos/master/android/android_128x128.png',
'name': 'Android Browser',
'shortName': 'Android Browser',
'matches': [
/^android$/i,
]
},
'android-chrome': {
'icon': 'https://www.browserfeatures.io/images/chrome-android/chrome-android_128x128.png',
'name': 'Android Chrome',
'shortName': 'Android Chrome',
'matches': [
/^and_chr$/i,
]
}

@@ -50,9 +90,5 @@ };

// a match on our name
for (var key in browsers) {
if (!browsers.hasOwnProperty(key)) {
continue;
}
for (var i = 0; i < browsers[key].matches.length; ++i) {
if (browsers[key].matches[i].test(name)) {
for (let key of Object.keys(browsers)) {
for (let match of browsers[key].matches) {
if (match.test(name)) {
return key;

@@ -65,1 +101,17 @@ }

}
export function parseBrowserVersionString(str) {
let parsed = parseProductVersionString(str);
if (!parsed) {
throw new Error('unable to parse browser product/version string');
}
parsed.product = parseBrowserName(parsed.product);
if (!parsed.product) {
throw new Error('browser ' + str + ' is unknown');
}
return parsed;
}

265

src/FeatureMatrix.js

@@ -0,175 +1,185 @@

import $ from 'jquery';
import browserFeatureProviders from './featureProviders';
import { browsers } from './browsers';
import FeatureMatrixRequirements from './FeatureMatrixRequirements';
import { pivot, groupColumns } from './columns';
require('./featureMatrix.scss')
var plugins = {
'Flash': {
'blacklist': [
'Mobile Safari',
'Opera Mini',
'Android Chrome'
],
'whitelist': [
'IE',
'Chrome',
'Firefox',
'Safari',
'Opera'
]
function getBrowsers(columns) {
var browsers = [];
for (let c of columns) {
if (browsers.length == 0 || browsers[browsers.length - 1].name != c.name) {
browsers.push({
name: c.name,
icon: c.icon,
span: 1
});
} else {
browsers[browsers.length - 1].span += 1;
}
}
};
return browsers;
}
// can't use export default syntax due to UMD library shenanigans with webpack,
// we set module.exports at the bottom of the file instead.
class FeatureMatrix {
// hello fancy ES6 syntax for specifying default options
// https://gist.github.com/ericelliott/f3c2a53a1d4100539f71
constructor(mountpoint, requirements, {
supportedText = '\u2714',
partialText = '\u2714',
unsupportedText = '-',
unknownText = '?',
featureColumnLabel = 'Feature',
pluginRequirementGenerator = (plugin, version) => `Requires the installation of ${plugin} ${version}`
} = {}) {
this.mountpoint = $(mountpoint);
Object.assign(this, {
requirements, supportedText, unsupportedText, unknownText,
featureColumnLabel, pluginRequirementGenerator, partialText
});
requirements.onLoad((r) => {
this.render();
});
}
render() {
this.mountpoint.addClass("feature-matrix");
let table = $('<table>');
let columns = [];
Object.keys(browsers).forEach((browser) => {
let cols = this.requirements.getBrowserSupport(browser, this.pluginRequirementGenerator);
columns = columns.concat(cols);
});
columns = groupColumns(columns);
let rows = pivot(columns, this.requirements.features.length);
let header = $("<thead>");
let browserHeaderRow = $("<tr>");
let versionHeaderRow = $("<tr>");
header.append(browserHeaderRow);
header.append(versionHeaderRow);
browserHeaderRow.append('<th rowspan="2" class="feature">' + this.featureColumnLabel + "</th>");
getBrowsers(columns).forEach((b) => {
browserHeaderRow.append('<th colspan="' + b.span + '" class="browser"><img src="' + b.icon + '" alt="" /><br />' + b.name.replace(/\s+/g, '<br />') + "</th>")
});
// the image is purely decorative, so we use an empty alt="" attribute
columns.forEach(col => versionHeaderRow.append('<th class="version">' + col.version + "</th>"));
function pivot(columns, numRows) {
var rows = [];
table.append(header);
for (var i = 0; i < numRows; ++i) {
var feature = columns[0].features[i].name;
var featureSupport = [];
let body = $("<tbody>");
columns.forEach(function (column) {
featureSupport.push(column.features[i].support);
// first pass to pull out any conditions and map them to superscript numbers
let conditions = {};
let conditionsInOrder = [];
let nextCondition = 1;
rows.forEach((row) => {
row.support.forEach((s) => {
if (s.conditions) {
s.conditions.forEach((c) => {
if (!conditions[c]) {
conditions[c] = nextCondition++;
conditionsInOrder.push(c);
}
});
}
});
});
rows.push({
feature: feature,
support: featureSupport
});
}
const getSuperscripts = (supportColumn) => {
let superscripts = [];
if (supportColumn.conditions) {
supportColumn.conditions.forEach((c) => {
superscripts.push('' + conditions[c]);
});
}
return rows;
}
function combineColumnsIntoGroup(columns) {
if (columns.length == 1) {
return columns[0];
} else if (columns.length == 2) {
var versions = columns.map(function (c) { return c.version; }).join(", ");
return {
name: columns[0].name,
version: versions,
features: columns[0].features
return superscripts;
};
} else {
var version = columns[0].version + " - " + columns[columns.length - 1].version;
const toObj = (arr) => {
let obj = {};
if (columns[0].isOldest) {
version = "< " + columns[columns.length - 1].nextVersion;
}
for (let item of arr) {
obj[item] = true;
}
if (columns[columns.length - 1].isNewest) {
version = columns[0].version + "+";
}
return obj;
};
return {
name: columns[0].name,
version: version,
features: columns[0].features
// http://stackoverflow.com/questions/34392741/best-way-to-get-intersection-of-keys-of-two-objects
const intersect = (a, b) => {
return Object.keys(a).filter({}.hasOwnProperty.bind(b));
};
}
}
function columnsAreGroupable(a, b) {
if (a.name != b.name || a.features.length != b.features.length) {
return false;
}
const getCommonSuperscrips = (row) => {
let intersection = null;
// HACK: quick and dirty way to deeply compare objects but is very
// dependent on the order of serialization
return JSON.stringify(a.features) == JSON.stringify(b.features);
}
for (let column of row.support) {
if (intersection == null) {
intersection = toObj(getSuperscripts(column));
} else {
intersection = toObj(intersect(intersection, toObj(getSuperscripts(column))));
}
}
// takes columns of IE6, IE7, IE8, IE9, etc and groups them into columns
// of IE6-IE8, IE9 (etc) based on whether those columns all have the same
// compatibility
function groupColumns(columns) {
let grouped = [];
let currGroup = null;
return intersection;
};
columns.forEach(function (column) {
if (currGroup == null) {
currGroup = [ column ];
} else if (columnsAreGroupable(currGroup[currGroup.length - 1], column)) {
// this column has the same compatibility/browser as the previous
// one so add it in
currGroup.push(column);
} else {
// this is different, push the current group into our list and
// create a new group
grouped.push(combineColumnsIntoGroup(currGroup));
currGroup = [ column ];
}
});
const renderSuperscripts = (supportColumn, exclude) => {
let superscripts = null;
if (currGroup != null && currGroup.length > 0) {
grouped.push(combineColumnsIntoGroup(currGroup));
}
if (supportColumn instanceof Array) {
superscripts = supportColumn;
} else {
superscripts = getSuperscripts(supportColumn).filter(ss => !exclude[ss]);
}
return grouped;
}
superscripts.sort();
// can't use export default syntax due to UMD library shenanigans with webpack,
// we set module.exports later on
class FeatureMatrix {
constructor(mountpoint, requirements) {
this.mountpoint = $(mountpoint);
this.requirements = requirements;
if (superscripts.length > 0) {
return "<sup>" + superscripts.join(',') + "</sup>";
} else {
return '';
}
};
requirements.onLoad((r) => {
this.render();
});
}
rows.forEach((row) => {
let tr = $("<tr>");
render() {
let table = $("<table>");
let columns = [];
//let common = getCommonSuperscrips(row);
let common = {};
Object.keys(browsers).forEach((browser) => {
columns = columns.concat(this.requirements.getBrowserSupport(browser));
});
tr.append('<td class="feature"><span>' + row.feature + '</span>' + renderSuperscripts(Object.keys(common), {}) + "</td>");
columns = groupColumns(columns);
let rows = pivot(columns, this.requirements.features.length);
let header = $("<thead>");
header.append("<th>Feature</th>");
columns.forEach(col => header.append("<th>" + col.name + " " + col.version + "</th>"));
table.append(header);
let body = $("<tbody>");
rows.forEach((row) => {
let tr = $("<tr>");
tr.append("<td>" + row.feature + "</td>");
row.support.forEach((s) => {
switch (s.support) {
case 'supported':
tr.append("<td>Y</td>");
tr.append('<td class="support supported"><span>' + this.supportedText + "</span>" + renderSuperscripts(s, common) + "</td>");
break;
case 'partial':
tr.append('<td class="support partial"><span>' + this.partialText + "</span>" + renderSuperscripts(s, common) + "</td>");
break;
case 'unsupported':
tr.append("<td>X</td>");
tr.append('<td class="support unsupported"><span>' + this.unsupportedText + "</span>" + renderSuperscripts(s, common) + "</td>");
break;
default:
tr.append("<td>?</td>");
tr.append('<td class="support unknown"><span>' + this.unknownText + "</span>" + renderSuperscripts(s, common) + "</td>");
break;

@@ -185,2 +195,11 @@ }

this.mountpoint.append(table);
// add superscript values
let conditionsList = $('<div class="conditions">');
for (let [i, c] of conditionsInOrder.entries()) {
conditionsList.append($("<p><sup>[" + (i + 1) + "]</sup> " + c + "</li>"));
}
this.mountpoint.append(conditionsList);
}

@@ -187,0 +206,0 @@

import $ from 'jquery';
import ProductFeature from './ProductFeature';
import BrowserSupportList from './BrowserSupportList';
import browserFeatureProviders from './featureProviders';
import parseCaniuseBrowserVersion from './parseCaniuseBrowserVersion';
import { parseBrowserName, browsers } from './browsers';
import { getBrowserNotes } from './notes';

@@ -14,5 +17,13 @@ export default class FeatureMatrixRequirements {

if (requirements.forcePartialSupport) {
this.forcePartialList = new BrowserSupportList(null, requirements.forcePartialSupport);
}
if (requirements.forceUnsupported) {
this.forceUnsupportedList = new BrowserSupportList(null, requirements.forceUnsupported);
}
++this.numLookups;
$.ajax({
url: 'http://api.browserfeatures.io/v1/browser/stable',
url: 'http://cdn.rawgit.com/Fyrd/caniuse/master/sample-data.json',
error: (xhr, textStatus, errorThrown) => {

@@ -25,3 +36,3 @@ throw new Error('failed to lookup browsers');

} else {
Object.keys(data.browsers).forEach((browser) => {
Object.keys(data.stats).forEach((browser) => {
var parsed = parseBrowserName(browser);

@@ -32,3 +43,14 @@ if (!parsed) {

this.browsers[parsed] = data.browsers[browser];
var versions = [];
Object.keys(data.stats[browser]).forEach((version) => {
parseCaniuseBrowserVersion('' + version, (v) => {
if (versions.indexOf(v) == -1) {
versions.push(v);
}
});
});
versions.sort((a, b) => a - b);
this.browsers[parsed] = versions;
});

@@ -84,26 +106,67 @@

getBrowserSupport(browser) {
getBrowserNotesFromSupport(name, version, supportLevel) {
if (!this.requirements.notes) {
return [];
}
if (supportLevel == 'supported') {
return getBrowserNotes(this.requirements.notes.supported, name, version);
} else if (supportLevel == 'partial') {
return getBrowserNotes(this.requirements.notes.partiallySupported, name, version);
} else if (supportLevel == 'unsupported') {
return getBrowserNotes(this.requirements.notes.unsupported, name, version);
} else {
return getBrowserNotes(this.requirements.notes.unknown, name, version);
}
}
getBrowserSupport(browser, pluginRequirementGenerator) {
var support = [];
var maxVersion = this.browsers[browser];
var minVersion = browsers[browser].minVersion || 1;
var versions = this.browsers[browser];
var minVersion = browsers[browser].minVersion || versions[0];
var maxVersion = versions[versions.length - 1];
for (var i = minVersion; i <= maxVersion; ++i) {
versions.forEach((version, i) => {
if (version < minVersion) {
return;
}
var versionSupport = {
name: browser,
version: "" + i,
nextVersion: "" + (i + 1),
isOldest: (i == minVersion),
isNewest: (i == maxVersion),
name: browsers[browser].shortName,
version: "" + version,
icon: browsers[browser].icon,
prevVersion: versions[i - 1],
nextVersion: versions[i + 1],
isOldest: (version == minVersion),
isNewest: (version == maxVersion),
features: []
};
this.features.forEach(function (feature) {
versionSupport.features.push({
name: feature.humanReadableName,
support: feature.getBrowserSupport(browser, i)
});
this.features.forEach((feature) => {
let column = null;
if (this.forceUnsupportedList && this.forceUnsupportedList.check(browser, version)) {
column = {
name: feature.humanReadableName,
support: { support: 'unsupported', conditions: feature.getNotes() }
};
} else {
column = {
name: feature.humanReadableName,
support: feature.getBrowserSupport(browser, version, pluginRequirementGenerator)
};
}
// what have I done
if (column.support.support == 'supported' && this.forcePartialList && this.forcePartialList.check(browser, version)) {
column.support.support = 'partial';
}
column.support.conditions = column.support.conditions.concat(this.getBrowserNotesFromSupport(browser, version, column.support.support));
versionSupport.features.push(column);
});
support.push(versionSupport);
}
});

@@ -110,0 +173,0 @@ return support;

@@ -1,2 +0,2 @@

import $ from 'jquery';
import xhr from 'xhr';
import { parseBrowserName } from '../browsers';

@@ -7,38 +7,61 @@

$.ajax({
url: 'http://api.browserfeatures.io/v1/feature/' + featureName,
error: function (xhr, textStatus, errorThrown) {
onComplete({ status: textStatus, error: errorThrown });
},
success: function (data) {
if (data.error) {
onComplete(data);
} else {
// normalise the caniuse browser information into something
// we can deal with
var res = {};
xhr({
uri: 'http://cdn.rawgit.com/Fyrd/caniuse/master/features-json/' + featureName + '.json',
useXDR: true
}, function (err, resp, body) {
if (err) {
onComplete({ statusCode: err.statusCode, error: err.body});
} else {
var data = JSON.parse(body);
for (var key in data.browsers) {
if (!data.browsers.hasOwnProperty(key)) {
continue;
// normalise the caniuse browser information into something
// we can deal with
var res = {};
for (const key of Object.keys(data.stats)) {
const parsed = parseBrowserName(key);
if (!parsed) {
continue;
}
if (res[parsed]) {
throw new Error('duplicate browser was parsed');
}
const browserInfo = data.stats[key];
let supported = false;
let minVersion = Number.MAX_VALUE;
let maxVersion = Number.MIN_VALUE;
for (const versionStr of Object.keys(browserInfo)) {
const value = browserInfo[versionStr];
if (value[0] != 'y' && value[0] != 'a') {
continue; // not supported
}
var parsed = parseBrowserName(key);
if (!parsed) {
// either fully or partially supported, we treat both as
// being supported and rely on users to add blacklist
// entries for the times where this assumption isn't ok
supported = true;
const version = parseFloat(versionStr);
if (isNaN(version)) {
continue;
}
var info = data.browsers[key];
minVersion = Math.min(version, minVersion);
maxVersion = Math.max(version, maxVersion);
}
res[parsed] = {
supported: info.supported,
since: info.since
};
res[parsed] = { supported: supported };
if (supported) {
res[parsed].since = minVersion;
}
}
onComplete(null, res);
}
onComplete(null, res);
}
});
}
}

@@ -0,54 +1,52 @@

import parseProductVersionString from './parseProductVersionString';
import { parseBrowserVersionString } from './browsers';
import { getBrowserNotes } from './notes';
import BrowserSupport from './BrowserSupport';
export default class ProductFeature {
constructor(key, spec, lookupBrowserFeature) {
this.supportedBrowsers = {};
this.unsupportedBrowsers = {};
this.notes = spec.notes;
this.humanReadableName = spec.humanReadableName;
this.key = key;
if (spec.supported) {
this.requiredSupport = new BrowserSupport(spec.supported, lookupBrowserFeature);
}
if (spec.requiredBrowserFeatures) {
spec.requiredBrowserFeatures.forEach((browserFeature) => {
var provider = browserFeature.split(':')[0];
var browserFeatureName = browserFeature.substring(browserFeature.indexOf(':') + 1);
if (spec.partiallySupported) {
this.partialSupport = new BrowserSupport(spec.partiallySupported, lookupBrowserFeature);
}
}
lookupBrowserFeature(provider, browserFeatureName, (support) => {
for (var key in support) {
if (!support.hasOwnProperty(key)) {
continue;
}
getNotes(name, version) {
return getBrowserNotes(this.notes, name, version);
}
if (support[key].supported) {
this.supportedBrowsers[key] = support[key];
} else {
this.unsupportedBrowsers[key] = true;
}
}
});
});
getBrowserSupport(name, version, pluginRequirementGenerator) {
let notes = this.getNotes(name, version);
let support = { support: "unknown" };
if (this.requiredSupport) {
support = this.requiredSupport.getBrowserSupport(name, version, pluginRequirementGenerator);
}
}
getBrowserSupport(name, version) {
// are we blacklisted? if so, we *really* don't support this browser
if (support.support != 'supported' && this.partialSupport) {
let partialSupport = this.partialSupport.getBrowserSupport(name, version, pluginRequirementGenerator);
// are we whitelisted? if so, we do support this browser
if (partialSupport.support == 'supported') {
support = partialSupport;
support.support = 'partial';
}
}
// do all of our browser features work in this browser?
// if so, we do support this browser
// if there is a feature that would work in a newer version of this browser
// or the feature flat-out isn't supported, we don't support this browser
if (this.supportedBrowsers[name] && version >= this.supportedBrowsers[name].since) {
return { support: 'supported' };
} else if (this.unsupportedBrowsers[name] || (this.supportedBrowsers[name] && version < this.supportedBrowsers[name].since)) {
return { support: 'unsupported' };
if (!support.conditions) {
support.conditions = [];
}
// do we require plugins which are supported in this browser?
// if so, we do support this browser but only conditionally on the
// presence of said plugin
// if we've gotten to this point, we have no clue about this browser,
// so we can't say whether we do or don't support it
return { support: 'unknown' };
// add in our notes to the final conditions
support.conditions = support.conditions.concat(notes);
return support;
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet