Feat: browser support (#19)
Big undertaking to support Mercury in the browser. Builds are working and all tests are passing both for web and node builds. Most code is closely shared.pull/21/head
parent
eaea57461a
commit
60a6861e18
@ -1,3 +1,4 @@
|
||||
**/fixtures/*
|
||||
dist/*
|
||||
coverage/*
|
||||
karma.conf.js
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,74 @@
|
||||
// Karma configuration
|
||||
// Generated on Mon Nov 14 2016 10:21:57 GMT-0800 (PST)
|
||||
// if (process.env.CI) {
|
||||
// require('phantomjs-prebuilt').path = './node_modules/.bin/phantomjs';
|
||||
// }
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine', 'browserify'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
// 'test-main.js',
|
||||
'./node_modules/phantomjs-polyfill-find/find-polyfill.js',
|
||||
'./node_modules/phantomjs-polyfill-string-includes/index.js',
|
||||
{ pattern: 'src/**/*.test.js', included: true },
|
||||
],
|
||||
|
||||
// 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: {
|
||||
'src/**/*.js': ['browserify'],
|
||||
},
|
||||
|
||||
browserify: {
|
||||
debug: true,
|
||||
transform: [
|
||||
'brfs-babel',
|
||||
'babelify',
|
||||
],
|
||||
},
|
||||
|
||||
// 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,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
// browsers: ['PhantomJS'],
|
||||
browsers: [(process.env.CI ? 'PhantomJS' : 'Chrome')],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
});
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import babelrc from 'babelrc-rollup'; // eslint-disable-line import/extensions
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import nodeResolve from 'rollup-plugin-node-resolve';
|
||||
import globals from 'rollup-plugin-node-globals';
|
||||
import uglify from 'rollup-plugin-uglify'; // eslint-disable-line import/extensions
|
||||
|
||||
const babelOpts = babelrc();
|
||||
babelOpts.runtimeHelpers = true;
|
||||
babelOpts.exclude = './node_modules/**';
|
||||
|
||||
export default {
|
||||
entry: 'src/mercury.js',
|
||||
plugins: [
|
||||
babel(babelOpts),
|
||||
commonjs({
|
||||
ignoreGlobal: true,
|
||||
}),
|
||||
globals(),
|
||||
nodeResolve({
|
||||
browser: true,
|
||||
preferBuiltins: false,
|
||||
}),
|
||||
uglify(),
|
||||
],
|
||||
format: 'iife',
|
||||
moduleName: 'Mercury',
|
||||
dest: 'dist/mercury.web.js', // equivalent to --output
|
||||
sourceMap: false,
|
||||
};
|
@ -0,0 +1,76 @@
|
||||
// Karma configuration
|
||||
// Generated on Mon Nov 14 2016 10:21:57 GMT-0800 (PST)
|
||||
// if (process.env.CI) {
|
||||
// require('phantomjs-prebuilt').path = './node_modules/.bin/phantomjs';
|
||||
// }
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine', 'browserify'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
'../node_modules/phantomjs-polyfill-find/find-polyfill.js',
|
||||
'../node_modules/phantomjs-polyfill-string-includes/index.js',
|
||||
'../dist/mercury.web.js',
|
||||
{ pattern: 'check-build.test.js', included: true },
|
||||
],
|
||||
|
||||
// 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: {
|
||||
'./check-build.test.js': ['browserify'],
|
||||
},
|
||||
|
||||
browserify: {
|
||||
debug: true,
|
||||
transform: [
|
||||
'brfs-babel',
|
||||
'babelify',
|
||||
],
|
||||
},
|
||||
|
||||
// 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,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
// browsers: ['PhantomJS'],
|
||||
browsers: [(process.env.CI ? 'PhantomJS' : 'Chrome')],
|
||||
// browsers: ['Chrome'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
|
||||
});
|
||||
};
|
@ -0,0 +1,39 @@
|
||||
/* eslint-disable */
|
||||
const express = require('express'); // eslint-disable-line import/no-extraneous-dependencies
|
||||
const request = require('request');
|
||||
|
||||
const app = express();
|
||||
var server
|
||||
|
||||
const start = () => {
|
||||
app.use('/', (req, res) => {
|
||||
const url = req.url.slice(1);
|
||||
|
||||
const options = {
|
||||
url,
|
||||
// Don't set encoding; fixes issues
|
||||
// w/gzipped responses
|
||||
encoding: null,
|
||||
// Accept cookies
|
||||
jar: true,
|
||||
// Accept and decode gzip
|
||||
gzip: true,
|
||||
// Follow any redirect
|
||||
followAllRedirects: true,
|
||||
};
|
||||
req.pipe(request(options)).pipe(res);
|
||||
});
|
||||
|
||||
server = app.listen(process.env.PORT || 3000);
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
server && server.close()
|
||||
}
|
||||
|
||||
if (!process.env.CI) {
|
||||
start()
|
||||
require('child_process').execSync('./node_modules/karma/bin/karma start ./scripts/karma.conf.js', {stdio:[0,1,2]});
|
||||
stop()
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export const ATTR_RE = /\[([\w-]+)\]/;
|
||||
export const ATTR_RE = /\[([\w-]+)\]/; // eslint-disable-line no-useless-escape
|
||||
|
@ -0,0 +1,126 @@
|
||||
// This module attempts to square cheerio with jquery
|
||||
// so that node-specific quirks/features of cheerio
|
||||
// will also work in the browser. This mostly involves
|
||||
// shimming a few functions and rewriting the jquery
|
||||
// constructor so it sandboxes most of its operations
|
||||
// and doesn't mutate existing dom elements in the page.
|
||||
|
||||
import jQuery from 'jquery';
|
||||
|
||||
const PARSER_CLASS = 'mercury-parsing-container';
|
||||
|
||||
jQuery.noConflict();
|
||||
const $ = (selector, context, rootjQuery, contextOverride = true) => {
|
||||
if (contextOverride) {
|
||||
if (context && typeof context === 'string') {
|
||||
context = `.${PARSER_CLASS} ${context}`;
|
||||
} else if (!context) {
|
||||
context = `.${PARSER_CLASS}`;
|
||||
}
|
||||
}
|
||||
|
||||
return new jQuery.fn.init(selector, context, rootjQuery); // eslint-disable-line new-cap
|
||||
};
|
||||
|
||||
$.fn = $.prototype = jQuery.fn;
|
||||
jQuery.extend($, jQuery); // copy's trim, extend etc to $
|
||||
|
||||
const removeScripts = ($node) => {
|
||||
// remove scripts and stylesheets
|
||||
$node.find('script, style, link[rel="stylesheet"]').remove();
|
||||
|
||||
return $node;
|
||||
};
|
||||
|
||||
$.cloneHtml = () => {
|
||||
const html = removeScripts($('html', null, null, false).clone());
|
||||
|
||||
return html.children().wrap('<div />').wrap('<div />');
|
||||
};
|
||||
|
||||
$.root = () => $('*').first();
|
||||
|
||||
$.browser = true;
|
||||
|
||||
const isContainer = ($node) => {
|
||||
const el = $node.get(0);
|
||||
if (el && el.tagName) {
|
||||
return el.tagName.toLowerCase() === 'container';
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$.html = ($node) => {
|
||||
if ($node) {
|
||||
// we never want to return a parsing container, only its children
|
||||
if (isContainer($node) || isContainer($node.children('container'))) {
|
||||
return $node.children('container').html() || $node.html();
|
||||
}
|
||||
|
||||
return $('<div>').append($node.eq(0).clone()).html();
|
||||
}
|
||||
|
||||
const $body = removeScripts($('body', null, null, false).clone());
|
||||
const $head = removeScripts($('head', null, null, false).clone());
|
||||
const $parsingNode = $body.find(`.${PARSER_CLASS}`);
|
||||
|
||||
if ($parsingNode.length > 0) {
|
||||
return $parsingNode.children().html();
|
||||
}
|
||||
|
||||
const html = $('<container />')
|
||||
.append($(`<container>${$head.html()}</container>`))
|
||||
.append($(`<container>${$body.html()}</container>`))
|
||||
.wrap('<container />')
|
||||
.parent()
|
||||
.html();
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
$.cleanup = () => {
|
||||
$(`.${PARSER_CLASS}`, null, null, false).remove();
|
||||
};
|
||||
|
||||
$.load = (html, opts = {}, returnHtml = false) => {
|
||||
const { normalizeWhitespace } = opts;
|
||||
|
||||
if (!html) {
|
||||
html = $.cloneHtml();
|
||||
} else {
|
||||
if (normalizeWhitespace) {
|
||||
if (typeof html === 'string') {
|
||||
html = html.replace(/[\s\n\r]+/g, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
html = $('<container />').html(html);
|
||||
}
|
||||
|
||||
const $body = $('body', null, null, false);
|
||||
// $('script', null, null, false).remove()
|
||||
let $parsingNode = $body.find(`.${PARSER_CLASS}`);
|
||||
|
||||
if (!$parsingNode[0]) {
|
||||
$body.append(`<div class="${PARSER_CLASS}" style="display: none;" />`);
|
||||
$parsingNode = $body.find(`.${PARSER_CLASS}`);
|
||||
}
|
||||
|
||||
// Strip scripts
|
||||
html = removeScripts(html);
|
||||
|
||||
// Remove comments
|
||||
html.find('*').contents().each(function () {
|
||||
if (this.nodeType === Node.COMMENT_NODE) { // eslint-disable-line no-undef
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
$parsingNode.html(html);
|
||||
|
||||
if (returnHtml) return { $, html: html.html() };
|
||||
|
||||
return $;
|
||||
};
|
||||
|
||||
export default $;
|
@ -1,13 +1,28 @@
|
||||
import { getAttrs } from 'utils/dom';
|
||||
|
||||
export default function convertNodeTo($node, $, tag = 'p') {
|
||||
const node = $node.get(0);
|
||||
if (!node) {
|
||||
return $;
|
||||
}
|
||||
const { attribs } = $node.get(0);
|
||||
const attribString = Reflect.ownKeys(attribs)
|
||||
.map(key => `${key}=${attribs[key]}`)
|
||||
const attrs = getAttrs(node) || {};
|
||||
// console.log(attrs)
|
||||
|
||||
const attribString = Reflect.ownKeys(attrs)
|
||||
.map(key => `${key}=${attrs[key]}`)
|
||||
.join(' ');
|
||||
let html;
|
||||
|
||||
$node.replaceWith(`<${tag} ${attribString}>${$node.contents()}</${tag}>`);
|
||||
if ($.browser) {
|
||||
// In the browser, the contents of noscript tags aren't rendered, therefore
|
||||
// transforms on the noscript tag (commonly used for lazy-loading) don't work
|
||||
// as expected. This test case handles that
|
||||
html = node.tagName.toLowerCase() === 'noscript' ? $node.text() : $node.html();
|
||||
} else {
|
||||
html = $node.contents();
|
||||
}
|
||||
$node.replaceWith(
|
||||
`<${tag} ${attribString}>${html}</${tag}>`
|
||||
);
|
||||
return $;
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
export default function getAttrs(node) {
|
||||
const { attribs, attributes } = node;
|
||||
|
||||
if (!attribs && attributes) {
|
||||
const attrs = Reflect.ownKeys(attributes).reduce((acc, index) => {
|
||||
const attr = attributes[index];
|
||||
|
||||
if (!attr.name || !attr.value) return acc;
|
||||
|
||||
acc[attr.name] = attr.value;
|
||||
return acc;
|
||||
}, {});
|
||||
return attrs;
|
||||
}
|
||||
|
||||
return attribs;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import getAttrs from './get-attrs';
|
||||
|
||||
describe('getAttrs(node)', () => {
|
||||
it('returns attrs for a raw jquery node', () => {
|
||||
const domNode = {
|
||||
attributes: {
|
||||
0: {
|
||||
name: 'class',
|
||||
value: 'foo bar',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const attrs = {
|
||||
class: 'foo bar',
|
||||
};
|
||||
|
||||
assert.deepEqual(getAttrs(domNode), attrs);
|
||||
});
|
||||
|
||||
it('returns attrs for a raw cheerio node', () => {
|
||||
const cheerioNode = {
|
||||
attribs: {
|
||||
class: 'foo bar',
|
||||
id: 'baz bat',
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(getAttrs(cheerioNode), cheerioNode.attribs);
|
||||
});
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
export default function setAttr(node, attr, val) {
|
||||
if (node.attribs) {
|
||||
node.attribs[attr] = val;
|
||||
} else if (node.attributes) {
|
||||
node.setAttribute(attr, val);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { MockDomNode } from 'test-helpers';
|
||||
import setAttr from './set-attr';
|
||||
|
||||
describe('setAttr(node, attr, val)', () => {
|
||||
it('sets attrs for a raw jquery node', () => {
|
||||
const domNode = new MockDomNode();
|
||||
|
||||
const node = setAttr(domNode, 'class', 'foo');
|
||||
|
||||
assert.equal(node.attributes[0].value, 'foo');
|
||||
});
|
||||
|
||||
it('sets attrs for a raw cheerio node', () => {
|
||||
const cheerioNode = {
|
||||
attribs: {
|
||||
class: 'foo bar',
|
||||
id: 'baz bat',
|
||||
},
|
||||
};
|
||||
|
||||
const node = setAttr(cheerioNode, 'class', 'foo');
|
||||
|
||||
assert.equal(node.attribs.class, 'foo');
|
||||
});
|
||||
});
|
@ -0,0 +1,15 @@
|
||||
export default function setAttrs(node, attrs) {
|
||||
if (node.attribs) {
|
||||
node.attribs = attrs;
|
||||
} else if (node.attributes) {
|
||||
while (node.attributes.length > 0) {
|
||||
node.removeAttribute(node.attributes[0].name);
|
||||
}
|
||||
|
||||
Reflect.ownKeys(attrs).forEach((key) => {
|
||||
node.setAttribute(key, attrs[key]);
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { MockDomNode } from 'test-helpers';
|
||||
import setAttrs from './set-attrs';
|
||||
|
||||
describe('setAttrs(node, attrs)', () => {
|
||||
it('sets attrs for a raw jquery node', () => {
|
||||
const attrs = {
|
||||
class: 'baz',
|
||||
};
|
||||
|
||||
const postAttrs = [
|
||||
{
|
||||
name: 'class',
|
||||
value: 'baz',
|
||||
},
|
||||
];
|
||||
|
||||
const domNode = new MockDomNode();
|
||||
const node = setAttrs(domNode, attrs);
|
||||
|
||||
assert.deepEqual(node.attributes, postAttrs);
|
||||
});
|
||||
|
||||
it('sets attrs for a raw cheerio node', () => {
|
||||
const cheerioNode = {
|
||||
attribs: {
|
||||
class: 'foo bar',
|
||||
id: 'baz bat',
|
||||
},
|
||||
};
|
||||
|
||||
const attrs = {
|
||||
class: 'baz',
|
||||
id: 'bar',
|
||||
};
|
||||
|
||||
const node = setAttrs(cheerioNode, attrs);
|
||||
|
||||
assert.deepEqual(node.attribs, attrs);
|
||||
});
|
||||
});
|
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Runs the mocha tests
|
||||
|
||||
if [ $BASH_ARGV ]; then
|
||||
if [ -e "$BASH_ARGV" ]; then
|
||||
FILES=$BASH_ARGV
|
||||
else
|
||||
FILES=$(find src -name "*$BASH_ARGV*.test.js")
|
||||
fi
|
||||
echo Running test for $FILES
|
||||
else
|
||||
echo Running all tests...
|
||||
FILES=$(find src -name "*.test.js")
|
||||
fi
|
||||
|
||||
mocha --reporter spec --compilers js:babel-register $FILES --require babel-polyfill
|
Loading…
Reference in New Issue