Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit Tests #43

Merged
merged 21 commits into from
Dec 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const redPerfume = {
task.markup.forEach((item) => {
let processedMarkup;
if (item.data) {
processedMarkup = html(item.data, processedStyles.classMap);
processedMarkup = html(options, item.data, processedStyles.classMap);
}
if (item.result) {
item.result(processedMarkup, undefined);
Expand Down
29 changes: 28 additions & 1 deletion src/class-encoding.js → src/css-class-encoding.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const helpers = require('./helpers.js');

// Initially I thought this too verbose, but there is literally no limit on class lengths other than the machine's memory/CPU.
// Firefox actually let me create and reference a classname with 100,000,000 characters. It was slow, but it worked fine.
const propertyValueEncodingMap = {
Expand Down Expand Up @@ -68,10 +70,33 @@ const prefix = 'rp__';
/**
* Encodes propter/value pairs as valid, decodable classnames.
*
* @param {object} options User's passed in options, containing verbose/customLoger
* @param {object} declaration Contains the Property and Value strings
* @return {string} A classname starting with . and a prefix
*/
function encodeClassName (declaration) {
function encodeClassName (options, declaration) {
if (!declaration || declaration.property === undefined || declaration.value === undefined) {
let message = [
'A rule declaration was missing details,',
'such as property or value.',
'This may result in a classname like',
'.rp__width__--COLONundefined,',
'.rp__undefined__--COLON100px,',
'or',
'.rp__undefined__--COLONundefined.',
'If there are multiples of these,',
'they may replace the previous.',
'Please report this error to',
'github.com/red-perfume/red-perfume/issues',
'because I have no idea how to',
'reproduce it with actual CSS input.',
'This was just meant for a safety check.',
'Honestly, if you actually got this',
'error, I\'m kind of impressed.'
].join(' ');
helpers.throwError(options, message);
}
declaration = declaration || {};
let newName = declaration.property + ':' + declaration.value;
let nameArray = newName.split('');
let encoded = nameArray.map(function (character) {
Expand All @@ -84,4 +109,6 @@ function encodeClassName (declaration) {
return '.' + prefix + encoded.join('');
}

// This is exported out too for a unit test
encodeClassName.propertyValueEncodingMap = propertyValueEncodingMap;
module.exports = encodeClassName;
37 changes: 37 additions & 0 deletions src/css-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,43 @@ const cssParser = function (options, input) {
};
const parsed = css.parse(input, parseOptions);

/*
input = '.test { color: #F00 }';
parsed = {
type: 'stylesheet',
stylesheet: {
rules: [
{
type: 'rule',
selectors: [
[
{
action: 'element',
ignoreCase: false,
name: 'class',
namespace: null,
original: '.test',
type: 'attribute',
value: 'test'
}
]
],
declarations: [
{
position: {...},
property: 'color',
type: 'declaration',
value: '#F00'
}
],
position: {...}
}
],
source: undefined,
parsingErrors: []
}
}
*/
if (parsed && parsed.stylesheet && parsed.stylesheet.rules) {
parsed.stylesheet.rules.forEach(function (rule) {
let parsedSelectors = selectorParse(rule.selectors.join(','));
Expand Down
12 changes: 6 additions & 6 deletions src/css-uglifier.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/**
* Increment the Uglifier index. Skips known bad
* numbers when base 36 encoded.
* Increment the Uglifier index if it contains a known bad
* value when base 36 encoded.
*
* @param {number} uglifierIndex Initial value
* @return {number} Incremented value
* @return {number} Incremented or original value (if good)
*/
function incrementUglifier (uglifierIndex) {
uglifierIndex = uglifierIndex + 1;
function incrementIfContainsBad (uglifierIndex) {
let knownBad = [
'ad' // adblockers may hide these elements
];
Expand Down Expand Up @@ -37,10 +36,11 @@ function cssUglifier (uglifierIndex) {
uglifierIndex = 0;
}
uglifierIndex = Math.round(uglifierIndex);
uglifierIndex = incrementIfContainsBad(uglifierIndex);

return {
name: '.rp__' + uglifierIndex.toString(36),
index: incrementUglifier(uglifierIndex)
index: uglifierIndex + 1
};
}

Expand Down
24 changes: 19 additions & 5 deletions src/css.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
const cssParser = require('./css-parser.js');
const cssStringify = require('./css-stringify.js');
const cssUglifier = require('./css-uglifier.js');
const encodeClassName = require('./class-encoding.js');
const encodeClassName = require('./css-class-encoding.js');
const helpers = require('./helpers.js');

const css = function (options, input, uglify) {
const parsed = cssParser(options, input);
options = options || {};
input = input || '';
uglify = uglify || false;
let parsed;
try {
parsed = cssParser(options, input);
} catch (err) {
helpers.throwError(options, 'Error parsing CSS', err);
}
if (!parsed) {
helpers.throwError(options, 'Error parsing CSS', input);
return {
classMap: {},
output: ''
};
}

const output = {
type: 'stylesheet',
Expand Down Expand Up @@ -95,7 +111,7 @@ const css = function (options, input, uglify) {
/* An encoded class name look like:
.rp__padding__--COLON10px
*/
let encodedClassName = encodeClassName(declaration);
let encodedClassName = encodeClassName(options, declaration);

/* A selector looks like:
[{
Expand All @@ -115,8 +131,6 @@ const css = function (options, input, uglify) {
classMap[originalSelectorName].push(encodedClassName);
});



newRules[encodedClassName] = {
type: 'rule',
selectors: [[encodedClassName]],
Expand Down
30 changes: 28 additions & 2 deletions src/html.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const parse5 = require('parse5');

const helpers = require('./helpers.js');

/**
* Helper function used when you want to console.log(JSON.stringify(document)).
*
Expand All @@ -16,8 +18,12 @@ function cleanDocument (document) {
*/
function removeParentNodes (node) {
delete node.parentNode;
// Coverage ignored because this function is only used during development.
/* istanbul ignore next */
if (node.childNodes) {
/* istanbul ignore next */
node.childNodes.forEach(function (child) {
/* istanbul ignore next */
removeParentNodes(child);
});
}
Expand All @@ -28,7 +34,21 @@ function cleanDocument (document) {
}
cleanDocument({});

const html = function (input, classMap) {
/**
* Parse an HTML string.
* Replace the original classnames with the atomized versions.
* Reserialize HTML to string.
*
* @param {object} options Options object from user containing verbose/customLogger
* @param {string} input String of valid HTML
* @param {object} classMap Map generated by css.js containing class names as the key with array of atomized style class name strings as the value
* @return {string} String of HTML with the class names replaced
*/
const html = function (options, input, classMap) {
options = options || {};
input = input || '';
classMap = classMap || {};

// String => Object
const document = parse5.parse(input);

Expand Down Expand Up @@ -92,7 +112,13 @@ const html = function (input, classMap) {
});

// Object => string
return parse5.serialize(document);
let markup = parse5.serialize(document);

if (!markup || markup === '<html><head></head><body></body></html>') {
helpers.throwError(options, 'Error parsing HTML', (markup || document));
}

return markup;
};

module.exports = html;
27 changes: 22 additions & 5 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const validator = {
key = undefined;
helpers.throwError(options, message);
}
if (!key) {
key = undefined;
}
return key;
},
validateBoolean: function (key, value) {
Expand All @@ -25,8 +28,10 @@ const validator = {
* @return {string} Returns a string or undefined if string was invalid
*/
validateFile: function (options, key, extensions, checkIfExists) {
this.validateString(options, key, 'File paths must be a string');
if (key) {
key = this.validateString(options, key, 'File paths must be a string');
extensions = this.validateArray(options, extensions, 'extensions argument must be an array of strings containing file extensions for validation');

if (key && extensions && extensions.length) {
let valid = extensions.some((extension) => {
return key.endsWith(extension);
});
Expand All @@ -36,27 +41,32 @@ const validator = {
extensionsMessage = extensions[0];
} else if (extensions.length === 2) {
extensionsMessage = extensions[0] + ' or ' + extensions[1];
} else if (extensions.length > 2) {
extensionsMessage = 'one of these: ' + extensions.slice(0, -1).join(',') + ' or ' + extensions.slice(-1);
} else {
extensionsMessage = extensions.slice(0, -1).join(', ') + ', or ' + extensions.slice(-1);
}
helpers.throwError(options, 'Expected filepath (' + key + ') to end in ' + extensionsMessage);
key = undefined;
}
}

if (key && checkIfExists) {
let fs = require('fs');
if (!fs.existsSync(key)) {
key = undefined;
helpers.throwError(options, 'Could not find file: ' + key);
key = undefined;
}
}

return key;
},
validateFunction: function (options, key, message) {
if (key && typeof(key) !== 'function') {
key = undefined;
helpers.throwError(options, message);
}
if (!key) {
key = undefined;
}
return key;
},
validateObject: function (options, key, message) {
Expand All @@ -70,13 +80,19 @@ const validator = {
key = undefined;
helpers.throwError(options, message);
}
if (!key) {
key = undefined;
}
return key;
},
validateString: function (options, key, message) {
if (key === '' || (key && typeof(key) !== 'string')) {
key = undefined;
helpers.throwError(options, message);
}
if (!key) {
key = undefined;
}
return key;
},

Expand Down Expand Up @@ -241,6 +257,7 @@ const validator = {
return data;
},
validateTaskScripts: function (options, scripts) {
scripts = scripts || {};
scripts.out = this.validateString(options, scripts.out, 'Optional task.scripts.out must be a string or undefined.');
scripts.result = this.validateFunction(options, scripts.result, 'Optional task.scripts.result must be a function or undefined.');
if (!scripts.out) {
Expand Down
Loading