Skip to content

Commit

Permalink
added comments
Browse files Browse the repository at this point in the history
  • Loading branch information
WebReflection committed Mar 13, 2020
1 parent 381825a commit f1da6df
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 77 deletions.
19 changes: 16 additions & 3 deletions cjs/cache.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
'use strict';
const createCache = () => ({
stack: [],
entry: null,
wire: null
stack: [], // each template gets a stack for each interpolation "hole"

entry: null, // each entry contains details, such as:
// * the template that is representing
// * the type of node it represents (html or svg)
// * the content fragment with all nodes
// * the list of updates per each node (template holes)
// * the "wired" node or fragment that will get updates
// if the template or type are different from the previous one
// the entry gets re-created each time

wire: null // each rendered node represent some wired content and
// this reference to the latest one. If different, the node
// will be cleaned up and the new "wire" will be appended
});
exports.createCache = createCache;

// this helper simplifies wm.get(key) || wm.set(key, value).get(key) operation
// enabling wm.get(key) || setCache(wm, key, value); to boost performance too
const setCache = (cache, key, value) => {
cache.set(key, value);
return value;
Expand Down
33 changes: 31 additions & 2 deletions cjs/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {diffable} = require('uwire');

const {reducePath} = require('./node.js');

// this helper avoid code bloat around handleAnything() callback
const diff = (comment, oldNodes, newNodes) => udomdiff(
comment.parentNode,
// TODO: there is a possible edge case where a node has been
Expand All @@ -28,20 +29,27 @@ const diff = (comment, oldNodes, newNodes) => udomdiff(
comment
);

// if an interpolation represents a comment, the whole
// diffing will be related to such comment.
// This helper is in charge of understanding how the new
// content for such interpolation/hole should be updated
const handleAnything = (comment, nodes) => {
let oldValue;
const text = document.createTextNode('');
let oldValue, text;
const anyContent = newValue => {
switch (typeof newValue) {
// primitives are handled as text content
case 'string':
case 'number':
case 'boolean':
if (oldValue !== newValue) {
oldValue = newValue;
if (!text)
text = document.createTextNode('');
text.textContent = newValue;
nodes = diff(comment, nodes, [text]);
}
break;
// null, and undefined are used to cleanup previous content
case 'object':
case 'undefined':
if (newValue == null) {
Expand All @@ -51,16 +59,23 @@ const handleAnything = (comment, nodes) => {
}
break;
}
// arrays and nodes have a special treatment
default:
if (isArray(newValue)) {
oldValue = newValue;
// arrays can be used to cleanup, if empty
if (newValue.length === 0)
nodes = diff(comment, nodes, []);
// or diffed, if these contains nodes or "wires"
else if (typeof newValue[0] === 'object')
nodes = diff(comment, nodes, newValue);
// in all other cases the content is stringified as is
else
anyContent(String(newValue));
}
// if the new value is a DOM node, or a wire, and it's
// different from the one already live, then it's diffed.
// if the node is a fragment, it's appended once via its childNodes
// There is no `else` here, meaning if the content
// is not expected one, nothing happens, as easy as that.
/* istanbul ignore else */
Expand All @@ -80,6 +95,12 @@ const handleAnything = (comment, nodes) => {
return anyContent;
};

// attributes can be:
// * ref=${...} for hooks and other purposes
// * .setter=${...} for Custom Elements setters or nodes with setters
// such as buttons, details, options, select, etc
// * onevent=${...} to automatically handle event listeners
// * generic=${...} to handle an attribute just like an attribute
const handleAttribute = (node, name) => {
// hooks and ref
if (name === 'ref')
Expand Down Expand Up @@ -135,6 +156,10 @@ const handleAttribute = (node, name) => {
};
};

// style and textarea nodes can change only their text
// without any possibility to accept child nodes.
// in these two cases the content is simply updated, or cleaned,
// accordingly with the passed value.
const handleText = node => {
let oldValue;
return newValue => {
Expand All @@ -145,6 +170,10 @@ const handleText = node => {
};
};

// each mapped update carries the update type and its path
// the type is either node, attribute, or text, while
// the path is how to retrieve the related node to update.
// In the attribute case, the attribute name is also carried along.
function handlers(options) {
const {type, path} = options;
const node = path.reduce(reducePath, this);
Expand Down
25 changes: 25 additions & 0 deletions cjs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,40 @@ const {createCache, setCache} = require('./cache.js');
const {Hole, unroll} = require('./rabbit.js');

const {create, defineProperties} = Object;

// each rendered node gets its own cache
const cache = new WeakMap;

// both `html` and `svg` template literal tags are polluted
// with a `for(ref[, id])` and a `node` tag too
const tag = type => {
// both `html` and `svg` tags have their own cache
const keyed = new WeakMap;
// keyed operations always re-use the same cache and unroll
// the template and its interpolations right away
const fixed = cache => (template, ...values) => unroll(
cache,
{type, template, values}
);
return defineProperties(
// non keyed operations are recognized as instance of Hole
// during the "unroll", recursively resolved and updated
(template, ...values) => new Hole(type, template, values),
{
for: {
// keyed operations need a reference object, usually the parent node
// which is showing keyed results, and optionally a unique id per each
// related node, handy with JSON results and mutable list of objects
// that usually carry a unique identifier
value(ref, id) {
const memo = keyed.get(ref) || setCache(keyed, ref, create(null));
return memo[id] || (memo[id] = fixed(createCache()));
}
},
node: {
// it is possible to create one-off content out of the box via node tag
// this might return the single created node, or a fragment with all
// nodes present at the root level and, of course, their child nodes
value: (template, ...values) => unroll(
createCache(),
{type, template, values}
Expand All @@ -36,13 +52,22 @@ exports.html = html;
const svg = tag('svg');
exports.svg = svg;

// rendering means understanding what `html` or `svg` tags returned
// and it relates a specific node to its own unique cache.
// Each time the content to render changes, the node is cleaned up
// and the new new content is appended, and if such content is a Hole
// then it's "unrolled" to resolve all its inner nodes.
const render = (where, what) => {
const hole = typeof what === 'function' ? what() : what;
const info = cache.get(where) || setCache(cache, where, createCache());
const wire = hole instanceof Hole ? unroll(info, hole) : hole;
if (wire !== info.wire) {
info.wire = wire;
where.textContent = '';
// valueOf() simply returns the node itself, but in case it was a "wire"
// it will eventually re-append all nodes to its fragment so that such
// fragment can be re-appended many times in a meaningful way
// (wires are basically persistent fragments facades with special behavior)
where.appendChild(wire.valueOf());
}
return where;
Expand Down
19 changes: 11 additions & 8 deletions cjs/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
const createContent = (m => m.__esModule ? /* istanbul ignore next */ m.default : /* istanbul ignore next */ m)(require('@ungap/create-content'));
const {indexOf} = require('uarray');

// from a generic path, retrieves the exact targeted node
const reducePath = (node, i) => node.childNodes[i];
exports.reducePath = reducePath;

// from a fragment container, create an array of indexes
// related to its child nodes, so that it's possible
// to retrieve later on exact node via reducePath
const createPath = node => {
const path = [];
let {parentNode} = node;
Expand All @@ -21,12 +25,13 @@ const {createTreeWalker, importNode} = document;
exports.createTreeWalker = createTreeWalker;
exports.importNode = importNode;

// basicHTML would never have a false case,
// unless forced, but it has no value for this coverage.
// IE11 and old Edge are passing live tests so we're good.
// this "hack" tells the library if the browser is IE11 or old Edge
const IE = importNode.length != 1;

const createFragment = IE ?
// basicHTML would never have a false case,
// unless forced, but it has no value for this coverage.
// IE11 and old Edge are passing live tests so we're good.
/* istanbul ignore next */
(text, type) => importNode.call(
document,
Expand All @@ -36,11 +41,9 @@ const createFragment = IE ?
createContent;
exports.createFragment = createFragment;

// to support IE10 and IE9 I could pass a callback instead
// with an `acceptNode` mode that's the callback itself
// function acceptNode() { return 1; } acceptNode.acceptNode = acceptNode;
// however, I really don't care anymore about IE10 and IE9, as these would
// require also a WeakMap polyfill, and have no reason to exist whatsoever.
// IE11 and old Edge have a different createTreeWalker signature that
// has been deprecated in other browsers. This export is needed only
// to guarantee the TreeWalker doesn't show warnings and, ultimately, works
const createWalker = IE ?
/* istanbul ignore next */
fragment => createTreeWalker.call(document, fragment, 1 | 128, null, false) :
Expand Down
68 changes: 67 additions & 1 deletion cjs/rabbit.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,50 @@ const {createCache, setCache} = require('./cache.js');
const {handlers} = require('./handlers.js');
const {createFragment, createPath, createWalker, importNode} = require('./node.js');

// the prefix is used to identify either comments, attributes, or nodes
// that contain the related unique id. In the attribute cases
// isµX="attribute-name" will be used to map current X update to that
// attribute name, while comments will be like <!--isµX-->, to map
// the update to that specific comment node, hence its parent.
// style and textarea will have <!--isµX--> text content, and are handled
// directly through text-only updates.
const prefix = 'isµ';

// Template Literals are unique per scope and static, meaning a template
// should be parsed once, and once only, as it will always represent the same
// content, within the exact same amount of updates each time.
// This cache relates each template to its unique content and updates.
const cache = new WeakMap;

// the entry stored in the rendered node cache, and per each "hole"
const createEntry = (type, template) => {
const {content, updates} = mapUpdates(type, template);
return {type, template, content, updates, wire: null};
};

// a template is instrumented to be able to retrieve where updates are needed.
// Each unique template becomes a fragment, cloned once per each other
// operation based on the same template, i.e. data => html`<p>${data}</p>`
const mapTemplate = (type, template) => {
const text = instrument(template, prefix);
const content = createFragment(text, type);
// once instrumented and reproduced as fragment, it's crawled
// to find out where each update is in the fragment tree
const tw = createWalker(content);
const nodes = [];
const length = template.length - 1;
let i = 0;
// updates are searched via unique names, linearly increased across the tree
// <div isµ0="attr" isµ1="other"><!--isµ2--><style><!--isµ3--</style></div>
let search = `${prefix}${i}`;
while (i < length) {
const node = tw.nextNode();
// if not all updates are bound but there's nothing else to crawl
// it means that there is something wrong with the template.
if (!node)
throw `bad template: ${text}`;
// if the current node is a comment, and it contains isµX
// it means the update should take care of any content
if (node.nodeType === 8) {
// The only comments to be considered are those
// which content is exactly the same as the searched one.
Expand All @@ -37,6 +61,10 @@ const mapTemplate = (type, template) => {
}
}
else {
// if the node is not a comment, loop through all its attributes
// named isµX and relate attribute updates to this node and the
// attribute name, retrieved through node.getAttribute("isµX")
// the isµX attribute will be removed as irrelevant for the layout
while (node.hasAttribute(search)) {
nodes.push({
type: 'attr',
Expand All @@ -46,6 +74,8 @@ const mapTemplate = (type, template) => {
node.removeAttribute(search);
search = `${prefix}${++i}`;
}
// if the node was a style or a textarea one, check its content
// and if it is <!--isµX--> then update tex-only this node
if (
/^(?:style|textarea)$/i.test(node.tagName) &&
node.textContent.trim() === `<!--${search}-->`
Expand All @@ -55,46 +85,82 @@ const mapTemplate = (type, template) => {
}
}
}
// once all nodes to update, or their attributes, are known, the content
// will be cloned in the future to represent the template, and all updates
// related to such content retrieved right away without needing to re-crawl
// the exact same template, and its content, more than once.
return {content, nodes};
};

// if a template is unknown, perform the previous mapping, otherwise grab
// its details such as the fragment with all nodes, and updates info.
const mapUpdates = (type, template) => {
const {content, nodes} = (
cache.get(template) ||
setCache(cache, template, mapTemplate(type, template))
);
// clone deeply the fragment
const fragment = importNode.call(document, content, true);
// and relate an update handler per each node that needs one
const updates = nodes.map(handlers, fragment);
// return the fragment and all updates to use within its nodes
return {content: fragment, updates};
};

// as html and svg can be nested calls, but no parent node is known
// until rendered somewhere, the unroll operation is needed to
// discover what to do with each interpolation, which will result
// into an update operation.
const unroll = (info, {type, template, values}) => {
const {length} = values;
// interpolations can contain holes and arrays, so these need
// to be recursively discovered
unrollValues(info, values, length);
let {entry} = info;
// if the cache entry is either null or different from the template
// and the type this unroll should resolve, create a new entry
// assigning a new content fragment and the list of updates.
if (!entry || (entry.template !== template || entry.type !== type))
info.entry = (entry = createEntry(type, template));
const {content, updates, wire} = entry;
// even if the fragment and its nodes is not live yet,
// it is already possible to update via interpolations values.
for (let i = 0; i < length; i++)
updates[i](values[i]);
// if the entry was new, or representing a different template or type,
// create a new persistent entity to use during diffing.
// This is simply a DOM node, when the template has a single container,
// as in `<p></p>`, or a "wire" in `<p></p><p></p>` and similar cases.
return wire || (entry.wire = persistent(content));
};
exports.unroll = unroll;

// the stack retains, per each interpolation value, the cache
// related to each interpolation value, or null, if the render
// was conditional and the value is not special (Array or Hole)
const unrollValues = ({stack}, values, length) => {
for (let i = 0; i < length; i++) {
const hole = values[i];
// each Hole gets unrolled and re-assigned as value
// so that domdiff will deal with a node/wire, not with a hole
if (hole instanceof Hole)
values[i] = unroll(
stack[i] || (stack[i] = createCache()),
hole
);
// arrays are recursively resolved so that each entry will contain
// also a DOM node or a wire, hence it can be diffed if/when needed
else if (isArray(hole))
unrollValues(
stack[i] || (stack[i] = createCache()),
hole,
hole.length
);
// if the value is nothing special, the stack doesn't need to retain data
// this is useful also to cleanup previously retained data, if the value
// was a Hole, or an Array, but not anymore, i.e.:
// const update = content => html`<div>${content}</div>`;
// update(listOfItems); update(null); update(html`hole`)
else
stack[i] = null;
}
Expand All @@ -103,7 +169,7 @@ const unrollValues = ({stack}, values, length) => {
};

/**
* Holds all necessary details needed to render the content further on.
* Holds all details wrappers needed to render the content further on.
* @constructor
* @param {string} type The hole type, either `html` or `svg`.
* @param {string[]} template The template literals used to the define the content.
Expand Down

0 comments on commit f1da6df

Please sign in to comment.