Skip to content

Commit

Permalink
Virus: Plug-in system (#173)
Browse files Browse the repository at this point in the history
* Implement Virus property
It allows the developer to make custom transformations to the genotype
before being parsed by Cell

* (wip) Fail when a mutation does not adhere to the API

* Add  feature as a plugin mechanism

* Add type by default after virus infection
This fixes the problem that the node was generated with a type
but the virus tried to override it to a div implicitly

* Fix missing semicolon

* Move virus infection up in the pipeline

Infecting in Genotype.build may be too late, as the $node is already
generated. This caused issues when writing viruses that changed the $type
of cells initially declared with "$type: 'input'".
This is also cleaner, as viruses act like dumb pre-processors or macro
expansions.

* Add Virus documentation

* Update VIRUS.md
  • Loading branch information
norchow authored and gliechtenstein committed Mar 27, 2018
1 parent cdb2fa8 commit 6ed18a0
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 32 deletions.
75 changes: 75 additions & 0 deletions VIRUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Virus

## Concept
Viruses are a plug-in system that makes it able to include custom transformations to the cells, extending them as far as wanted.

To use it, you should "infect" a Gene with a Virus, which is a function that takes a Gene-like object and returns another Gene-like object. This returned object _could_ be a real Gene or an object to be transformed by another Virus (pretty much like a Pipeline).

## Examples
The following virus propagates the $update method down the inheritance tree when a variable is changed:

```
function update_propagating_virus(component){
let recursive_update = (node) => {
for(let n of node.children){
n.$update && n.$update()
recursive_update(n)
}
}
let old_update = component.$update
component.$update = function(){
old_update && old_update.call(this)
recursive_update(this)
}
return component
}
window.c = {
$cell: true,
$type: 'ul',
_name: '',
$virus: update_propagating_virus,
$components: [
{ $type: 'li',
$components: [
{ $type: 'p',
$text: '',
$update: function(){
this.$text = this._name;
}
},
{ $type: 'p', $text: 'other' }
]
}
]
}
```

As the virus can be any function that expects a component, you can also send other arguments to it.

The following example shows how to make a virus to call a method every X seconds:

```
var Tickable = function(timer, trigger){
return function(gene) {
gene.$init = function() {
var self = this;
setInterval(function() {
self[trigger]();
}, timer);
}
return gene;
}
}
...
{
$virus: [Tickable(500, '_mutate')]
}
```

Refer to the [tests](test/integration.js) for more examples and details.
17 changes: 16 additions & 1 deletion cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@
Genotype.set($node, key, gene[key]);
}
},
infect: function(gene) {
var virus = gene.$virus;
if (!virus) return gene;
var mutations = Array.isArray(virus) ? virus : [virus];
delete gene.$virus;
return mutations.reduce(function(g, mutate) {
var mutated = mutate(g);
if (mutated === null || typeof mutated !== 'object') {
throw new Error('$virus mutations must return an object');
}
mutated.$type = mutated.$type || 'div';
return mutated;
}, gene);
},
};
var Gene = {
/*
Expand Down Expand Up @@ -522,7 +536,8 @@
// As a result, all HTML elements become autonomous.
if ($context === undefined) $context = $root;
else $root = $context;
$context.DocumentFragment.prototype.$build = $context.Element.prototype.$build = function(gene, inheritance, index, namespace, replace) {
$context.DocumentFragment.prototype.$build = $context.Element.prototype.$build = function(healthy_gene, inheritance, index, namespace, replace) {
var gene = Genotype.infect(healthy_gene);
var $node = Membrane.build(this, gene, index, namespace, replace);
Genotype.build($node, gene, inheritance || [], index);
Nucleus.build($node);
Expand Down
49 changes: 49 additions & 0 deletions test/Genotype.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,54 @@ describe("Genotype", function() {
c: "C"
})
})
describe("$virus", function() {
let ul_mutating_virus = function(component){
component.id = "infected";
component.$components = [{$type: 'li'}];
return component;
}
it("Applies a single virus mutation", function() {
let component = { $type: 'ul', $virus: ul_mutating_virus };

compare(Genotype.infect(component), {
$type: 'ul',
$components: [{$type: 'li'}],
id: 'infected',
})

let $node = root.document.body.$build(component, []);
compare($node.outerHTML, '<ul id="infected"><li></li></ul>');
})
it("Applies multiple virus mutations sequentially", function() {
let id_mutating_virus = function(component){
component._previous_id = component.id;
component.id += "_again";
return component;
}

let component = {
$type: 'ul',
$virus: [ul_mutating_virus, id_mutating_virus]
};

compare(Genotype.infect(component), {
$type: 'ul',
$components: [{$type: 'li'}],
_previous_id: 'infected',
id: 'infected_again',
})

let $node = root.document.body.$build(component, []);
compare($node.outerHTML, '<ul id="infected_again"><li></li></ul>');
})
it("Errors when mutation does not comply with the API", function() {
let wrong_mutation = function(component){
let the_thing = "return nothing";
}

let component = { $type: 'ul', $virus: wrong_mutation };
assert.throws(() => Genotype.infect(component), /return an object/)
})
})
})
})
8 changes: 4 additions & 4 deletions test/Phenotype.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,13 @@ describe("Phenotype", function() {

styleAttr = $node.getAttribute("style");
styleProp = $node.style;

compare(styleAttr, "background-color: red; font-family: Courier;")
compare(styleProp.backgroundColor, "red");
compare(styleProp.fontFamily, "Courier");

compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration");

});
});
describe("string", function() {
Expand All @@ -448,13 +448,13 @@ describe("Phenotype", function() {

styleAttr = $node.getAttribute("style");
styleProp = $node.style;

compare(styleAttr, "background-color: red;")
// even if we initially set the style as string,
// we should be able to access it as an object property
compare(styleProp.backgroundColor, "red");
compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration");

});
it("class", function() {
const $parent = document.createElement("div");
Expand Down
135 changes: 108 additions & 27 deletions test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ const spy = require("./spy.js")
const compare = function(actual, expected) {
assert.equal(stringify(actual), stringify(expected));
}
const cleanup = function(){
require('jsdom-global')();
}
cleanup()
God.plan(window);

describe("DOM prototype overrides", function() {
var cleanup = require('jsdom-global')()
God.plan(window);
it("$snapshot", function() {
cleanup();
cleanup = require('jsdom-global')()
beforeEach(cleanup);

document.body.innerHTML = "";
it("$snapshot", function() {
window.c = {
$cell: true,
_model: [],
Expand All @@ -36,20 +38,13 @@ describe("DOM prototype overrides", function() {

});
describe("Nucleus", function() {
var cleanup = require('jsdom-global')()
God.plan(window);
it("has nothing at the beginning", function() {
cleanup();
cleanup = require('jsdom-global')()
beforeEach(cleanup);

document.body.innerHTML = "";
it("has nothing at the beginning", function() {
God.create(window);
compare(document.body.outerHTML, "<body></body>");
})
it("God.create creates correct markup", function() {
cleanup();
cleanup = require('jsdom-global')()
document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [],
Expand All @@ -69,9 +64,6 @@ describe("Nucleus", function() {
compare(document.body.outerHTML, "<body><div id=\"grandparent\"><div id=\"parent\"><div id=\"child\"></div></div><div id=\"aunt\"></div></div></body>")
})
it("God.create triggers God.detect, the detect correctly detects", function() {
cleanup();
cleanup = require('jsdom-global')()
document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [],
Expand All @@ -93,10 +85,9 @@ describe("Nucleus", function() {
compare(spy.God.detect.callCount, 1);
})
describe("context inheritance", function() {
beforeEach(cleanup);

it("walks up the DOM tree to find the attribute if it doesn't exist on the current node", function() {
cleanup();
cleanup = require('jsdom-global')()
document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [1,2,3],
Expand All @@ -116,9 +107,6 @@ describe("Nucleus", function() {
compare($child._model, [1,2,3]);
})
it("finds the attribute on the current element first", function() {
cleanup();
cleanup = require('jsdom-global')()
document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [1,2,3],
Expand All @@ -139,9 +127,6 @@ describe("Nucleus", function() {
compare($child._model, ["a"]);
})
it("descendants can share an ancestor's variable", function() {
cleanup();
cleanup = require('jsdom-global')()
document.body.innerHTML = "";
window.c = {
$cell: true,
_model: [1,2,3],
Expand Down Expand Up @@ -169,3 +154,99 @@ describe("Nucleus", function() {
})
})
})

describe("Infects with nice viruses", function() {
beforeEach(cleanup);

it("can have an update_propagating_virus", function() {
function update_propagating_virus(component){
let recursive_update = (node) => {
for(let n of node.children){
n.$update && n.$update()
recursive_update(n)
}
}

let old_update = component.$update

component.$update = function(){
old_update && old_update.call(this)
recursive_update(this)
}

return component
}

window.c = {
$cell: true,
$type: 'ul',
_name: '',
$virus: update_propagating_virus,
$components: [
{ $type: 'li',
$components: [
{ $type: 'p',
$text: '',
$update: function(){
this.$text = this._name;
}
},
{ $type: 'p', $text: 'other' }
]
}
]
}

compare(document.body.outerHTML, "<body></body>")
God.create(window)

var $node = document.body.querySelector("ul")
$node._name = "infected"
Phenotype.$update($node)
compare(document.body.querySelector("p").$text, 'infected')
})
it("can have a markup helper virus", function(){
function expand_selector(component, selector){
let parts = selector.match(/([a-zA-Z0-9]*)([#a-zA-Z0-9-_]*)([.a-zA-Z0-9-_]*)/)
if (parts[1]) component.$type = parts[1]
if (parts[2]) component.id = parts[2].substring(1)
if (parts[3]) component['class'] = parts[3].split('.').join(' ').trim()
return component
}

function hamlism(component){
if(component.$components){
component.$components = component.$components.map(hamlism)
}

let tag = component.$tag
if(!tag) return component

selectors = tag.split(' ')
expand_selector(component, selectors.pop())

return selectors.reduceRight(function(child, selector){
return expand_selector({$components: [child]}, selector)
}, component)
}

window.c = {
$cell: true,
$tag: '.class-a span#id-span.class-b',
$virus: [ hamlism ],
$components: [{ $tag: 'li#main.list-item' }]
}

compare(document.body.outerHTML, "<body></body>")
God.create(window)
compare(document.body.outerHTML,
'<body>'+
'<div class="class-a">' +
'<span id="id-span" class="class-b">'+
'<li id="main" class="list-item"></li>'+
'</span>'+
'</div>'+
'</body>'
)
})
})

0 comments on commit 6ed18a0

Please sign in to comment.