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

[WIP] Add if directive #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 9 additions & 7 deletions src/compile-node.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
const { default: template } = require('@babel/template');

const { getNode } = require('./context');
const { getNode, moveTemplate, oldParent } = require('./context');
const directives = require('./directives');
const { ELEMENT_NODE, TEXT_NODE } = require('./utils/constants')

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const INVISIBLE_CHAR = '\u200c';

function compileNode(node, context) {
Expand All @@ -17,10 +16,13 @@ function compileNode(node, context) {
}

function compileElementNode(node, context) {
if (node.attributes) {
for (const { name, value } of Array.from(node.attributes)) {
if (name in directives) {
return directives[name](node, value, context);
if (node.hasAttributes()) {
for (const name in directives) {
if (node.hasAttribute(name)) {
console.log(name)
if (node.tagName === 'template')
moveTemplate(node, context);
return directives[name](node, node[oldParent] || node.parentNode, node.getAttribute(name), context);
}
}
}
Expand Down
54 changes: 45 additions & 9 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
const t = require('@babel/types');
const { TEXT_NODE } = require('./utils/constants')

const CHILD_NODES = t.identifier('_');
const DOCUMENT = t.identifier('_doc');
const TEXT_NODE = 3;

const __meta__ = Symbol("__meta__")

function createContext(
{
ownerDocument,
nextSibling,
templateId,
elementCounters = new WeakMap(),
elementCounter: oldElementCounter,
hooks = [],
counters = { i: 0, tId: 0, rId: 0 }
},
root,
getNodeOverride
parent,
{
enableCounter = false,
getNodeOverride
} = {}
) {

const newRoot = !(parent && __meta__ in parent && parent[__meta__].identifier);
const identifier = newRoot ? t.identifier(`_r${counters.rId++}`) : parent[__meta__].identifier;
const elementCounter = newRoot ? (enableCounter ? t.identifier(`_c${counters.i++}`) : null) : oldElementCounter
const newMeta = {
identifier,
elementCounter
}
if (parent)
parent[__meta__] = newMeta;
root[__meta__] = newMeta;
return {
identifier: t.identifier(`_r${counters.rId++}`),
identifier: identifier,
newRoot,
root,
ownerDocument,
hooks,
elementCounters,
elementCounter,
nextSibling,
templateId,
getNodeOverride,
Expand All @@ -35,12 +53,22 @@ function getNode(node, context) {
if (res) return res;
}
if (node === context.root) return context.identifier;
else
else if (__meta__ in node) return node[__meta__].identifier;
else {
let numAst = t.numericLiteral(Array.from(node.parentNode.childNodes).indexOf(node))
if (node.parentNode && __meta__ in node.parentNode && node.parentNode[__meta__].elementCounter) {
const counter = node.parentNode[__meta__].elementCounter;
if (numAst.value === 0)
numAst = counter;
else
numAst = t.binaryExpression("+", counter, numAst);
}
return t.memberExpression(
t.memberExpression(getNode(node.parentNode, context), CHILD_NODES, true),
t.numericLiteral(Array.from(node.parentNode.childNodes).indexOf(node)),
numAst,
true
);
}
}

function mergeTextNodes(elem) {
Expand All @@ -54,7 +82,14 @@ function mergeTextNodes(elem) {
});
}

const oldParent = Symbol("oldParent")

function moveTemplate(elem, context) {
if (oldParent in elem) {
// Already moved
return
}

const eId = `${context.templateId}$${context.counters.tId++}`;
const parent = elem.parentNode;
parent.removeChild(elem);
Expand All @@ -65,13 +100,14 @@ function moveTemplate(elem, context) {
);
mergeTextNodes(parent);
elem.setAttribute('id', eId);
return eId;
elem[oldParent] = parent;
}

Object.assign(exports, {
CHILD_NODES,
DOCUMENT,
createContext,
getNode,
moveTemplate
moveTemplate,
oldParent
});
81 changes: 37 additions & 44 deletions src/directives/for.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,82 @@ const { default: template } = require('@babel/template');

const {
createContext,
moveTemplate,
getNode,
CHILD_NODES,
DOCUMENT
} = require('../context');
const compileNode = require('../compile-node');

module.exports = function compileAFor(node, value, context) {
const parent = node.parentNode;

module.exports = function compileAFor(node, parent, value, context) {
node.removeAttribute('a-for');
const tId = moveTemplate(node, context);

const templateIdentifier = t.identifier(tId);

const { elementCounters, ownerDocument } = context;

const templateIdentifier = t.identifier(node.getAttribute('id'));
const parentAst = getNode(parent, context);

// If parent is just an identifier newRoot = oldRoot;
// Get counter of parent
const createCounter = !t.isIdentifier(parentAst);

const counter = createCounter
? t.identifier(`_c${context.counters.i++}`)
: elementCounters.get(parentAst);

const newContext = createContext(
context,
node,
(node, { root, identifier }) => {
if (node.parentNode === root) {
const numeric = t.numericLiteral(NaN);
const pos = Array.from(node.parentNode.childNodes).indexOf(node);

// Push as hook because `childNodes.length` can change if for example an element is removed or moved (like a template)
context.hooks.push(
() => (numeric.value = node.parentNode.childNodes.length - pos)
);
return template.ast`${identifier}[${CHILD_NODES}][${counter} - ${numeric}]`
.expression;
}
parent,
{
getNodeOverride(node, { root, identifier }) {
if (node.parentNode === root) {
const numeric = t.numericLiteral(NaN);
const pos = Array.from(node.parentNode.childNodes).indexOf(node);

// Push as hook because `childNodes.length` can change if for example an element is removed or moved (like a template)
context.hooks.push(
() => (numeric.value = node.parentNode.childNodes.length - pos)
);
return template.ast`${identifier}[${CHILD_NODES}][${elementCounter} - ${numeric}]`
.expression;
}
},
enableCounter: true
}
);

const { identifier } = newContext;

elementCounters.set(identifier, counter);
const { identifier, elementCounter, ownerDocument, newRoot } = newContext;

const innerAst = compileNode.compileChilds(node, newContext);
const innerAst = compileNode.compileElementNode(node, newContext);

const loop = template(`
for(let ${value})
BODY
`)({
BODY: template.ast`
${counter} += ${t.numericLiteral(node.childNodes.length)}
if (${counter} > ${identifier}[${CHILD_NODES}].length)
${elementCounter} += ${t.numericLiteral(node.childNodes.length)}
if (${elementCounter} > ${identifier}[${CHILD_NODES}].length)
${identifier}.appendChild(${DOCUMENT}.importNode(${templateIdentifier}.content, true));
${innerAst}
`
});

const counterAst = createCounter
const setupAst = newRoot
? template.ast`
let ${counter} = ${t.numericLiteral(parent.childNodes.length)}
const ${identifier} = ${parentAst}
let ${elementCounter} = ${t.numericLiteral(parent.childNodes.length)}
`
: [];

const sibblingCount = t.numericLiteral(NaN);

context.hooks.push(
() => (sibblingCount.value = parent.childNodes.length)
);

// Only the one that created the counter clean the elements
const cleanAst = createCounter
// Only the one that created the root clean the elements
const cleanAst = newRoot
? template.ast`
while(${identifier}[${CHILD_NODES}].length > ${counter})
${identifier}.removeChild(${identifier}[${CHILD_NODES}][${counter}])
while(${identifier}[${CHILD_NODES}].length > ${elementCounter} + ${sibblingCount})
${identifier}.removeChild(${identifier}[${CHILD_NODES}][${elementCounter}])
`
: [];

return template.ast`
const ${identifier} = ${parentAst}
const ${templateIdentifier} = ${ownerDocument}.getElementById(${t.stringLiteral(
tId
node.getAttribute('id')
)})
${counterAst}
${setupAst}
${loop}
${cleanAst}
`;
Expand Down
40 changes: 40 additions & 0 deletions src/directives/if.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const t = require('@babel/types');
const { default: template } = require('@babel/template');

const {
createContext,
getNode,
} = require('../context');
const compileNode = require('../compile-node');

module.exports = function compileAIf(node, parent, value, context) {
node.removeAttribute('a-if');

const getParentAst = getNode(parent, context)

const newContext = createContext(context, node, parent, { enableCounter: true });
const { newRoot, elementCounter, identifier } = newContext;

const innerAst = compileNode.compileElementNode(node, newContext)

const condition = template(`
if (${value}){
COUNTER += ${node.childNodes.length - }
BODY
} else {

}
`)({
BODY: innerAst,
COUNTER: elementCounter
})

const setupAst = newRoot
? template.ast`
const ${identifier} = ${getParentAst}
let ${elementCounter} = ${t.numericLiteral(parent.childNodes.length)}
`
: [];

return setupAst.concat(condition)
};
2 changes: 2 additions & 0 deletions src/directives/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// The order here is used for evaluation order :)
module.exports = {
'a-if': require('./if.js'),
'a-for': require('./for.js')
};
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { default: template } = require('@babel/template');
const assert = require('assert');

const createSetState = require('./set-state');
const removeEmptyTextNodes = require('./utils/remove-empty-text-nodes')

module.exports = function compile(
input,
Expand Down Expand Up @@ -42,6 +43,9 @@ module.exports = function compile(

function transformClass(clazz, id, doc) {
const element = doc.getElementById(id);

removeEmptyTextNodes(element);

const methods = clazz.get('body.body');

const constructor =
Expand Down Expand Up @@ -97,3 +101,4 @@ function attachShadow(constructor, ownerDocument, id) {
)
`);
}

2 changes: 2 additions & 0 deletions src/utils/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
exports.ELEMENT_NODE = 1;
exports.TEXT_NODE = 3;
10 changes: 10 additions & 0 deletions src/utils/remove-empty-text-nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { TEXT_NODE } = require('./constants')

module.exports = function removeEmptyTextNodes(elem) {
if (elem.childNodes)
for(const child of Array.from(elem.childNodes)) {
if (child.nodeType === TEXT_NODE && child.data.trim() === '')
elem.removeChild(child)
removeEmptyTextNodes(child)
}
}